Skip to content

Commit bcebc5e

Browse files
committed
Offer parsing from bech32 strings
Add common bech32 parsing for BOLT 12 messages. The encoding is similar to bech32 only without a checksum and with support for continuing messages across multiple parts. Messages implementing Bech32Encode are parsed into a TLV stream, which is converted to the desired message content while performing semantic checks. Checking after conversion allows for more elaborate checks of data composed of multiple TLV records and for more meaningful error messages. The parsed bytes are also saved to allow creating messages with mirrored data, even if TLV records are unknown.
1 parent d5eae96 commit bcebc5e

File tree

4 files changed

+329
-20
lines changed

4 files changed

+329
-20
lines changed

lightning/src/offers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
//! Offers are a flexible protocol for Lightning payments.
1414
1515
pub mod offer;
16+
pub mod parse;

lightning/src/offers/offer.rs

Lines changed: 226 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
//! extern crate core;
1919
//! extern crate lightning;
2020
//!
21+
//! use core::convert::TryFrom;
2122
//! use core::num::NonZeroU64;
2223
//! use core::time::Duration;
2324
//!
2425
//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
25-
//! use lightning::offers::offer::{Amount, OfferBuilder, Quantity};
26+
//! use lightning::offers::offer::{Amount, Offer, OfferBuilder, Quantity};
27+
//! use lightning::offers::parse::ParseError;
28+
//! use lightning::util::ser::{Readable, Writeable};
2629
//!
27-
//! # use bitcoin::secp256k1;
2830
//! # use lightning::onion_message::BlindedPath;
2931
//! # #[cfg(feature = "std")]
3032
//! # use std::time::SystemTime;
@@ -33,9 +35,9 @@
3335
//! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
3436
//! #
3537
//! # #[cfg(feature = "std")]
36-
//! # fn build() -> Result<(), secp256k1::Error> {
38+
//! # fn build() -> Result<(), ParseError> {
3739
//! let secp_ctx = Secp256k1::new();
38-
//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?);
40+
//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
3941
//! let pubkey = PublicKey::from(keys);
4042
//!
4143
//! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60);
@@ -48,20 +50,36 @@
4850
//! .path(create_another_blinded_path())
4951
//! .build()
5052
//! .unwrap();
53+
//!
54+
//! // Encode as a bech32 string for use in a QR code.
55+
//! let encoded_offer = offer.to_string();
56+
//!
57+
//! // Parse from a bech32 string after scanning from a QR code.
58+
//! let offer = encoded_offer.parse::<Offer>()?;
59+
//!
60+
//! // Encode offer as raw bytes.
61+
//! let mut bytes = Vec::new();
62+
//! offer.write(&mut bytes).unwrap();
63+
//!
64+
//! // Decode raw bytes into an offer.
65+
//! let offer = Offer::try_from(bytes)?;
5166
//! # Ok(())
5267
//! # }
5368
//! ```
5469
5570
use bitcoin::blockdata::constants::ChainHash;
5671
use bitcoin::network::constants::Network;
5772
use bitcoin::secp256k1::PublicKey;
73+
use core::convert::TryFrom;
5874
use core::num::NonZeroU64;
75+
use core::str::FromStr;
5976
use core::time::Duration;
6077
use io;
6178
use ln::features::OfferFeatures;
6279
use ln::msgs::MAX_VALUE_MSAT;
80+
use offers::parse::{Bech32Encode, ParseError, SemanticError};
6381
use onion_message::BlindedPath;
64-
use util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
82+
use util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
6583
use util::string::PrintableString;
6684

6785
use prelude::*;
@@ -169,11 +187,9 @@ impl OfferBuilder {
169187

170188
/// Builds an [`Offer`] from the builder's settings.
171189
pub fn build(self) -> Result<Offer, ()> {
190+
// TODO: Also check for Amount::Currency
172191
if let Some(Amount::Currency { .. }) = self.offer.amount {
173-
return Err(());
174-
}
175-
176-
if self.offer.amount_msats() > MAX_VALUE_MSAT {
192+
} else if self.offer.amount_msats() > MAX_VALUE_MSAT {
177193
return Err(());
178194
}
179195

@@ -302,6 +318,12 @@ impl Offer {
302318
}
303319
}
304320

321+
impl AsRef<[u8]> for Offer {
322+
fn as_ref(&self) -> &[u8] {
323+
&self.bytes
324+
}
325+
}
326+
305327
impl OfferContents {
306328
pub fn amount_msats(&self) -> u64 {
307329
self.amount.as_ref().map(Amount::as_msats).unwrap_or(0)
@@ -340,12 +362,27 @@ impl OfferContents {
340362
}
341363
}
342364

365+
impl Writeable for Offer {
366+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
367+
WithoutLength(&self.bytes).write(writer)
368+
}
369+
}
370+
343371
impl Writeable for OfferContents {
344372
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
345373
self.as_tlv_stream().write(writer)
346374
}
347375
}
348376

377+
impl TryFrom<Vec<u8>> for Offer {
378+
type Error = ParseError;
379+
380+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
381+
let tlv_stream: OfferTlvStream = Readable::read(&mut &bytes[..])?;
382+
Offer::try_from((bytes, tlv_stream))
383+
}
384+
}
385+
349386
/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or
350387
/// another currency.
351388
#[derive(Clone, Debug, PartialEq)]
@@ -389,6 +426,14 @@ pub enum Quantity {
389426
}
390427

391428
impl Quantity {
429+
fn new(quantity: Option<u64>) -> Self {
430+
match quantity {
431+
Some(1) | None => Quantity::One,
432+
Some(0) => Quantity::Many,
433+
Some(n) => Quantity::Max(NonZeroU64::new(n).unwrap()),
434+
}
435+
}
436+
392437
fn to_tlv_record(&self) -> Option<u64> {
393438
match self {
394439
Quantity::One => None,
@@ -412,13 +457,88 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, {
412457
(22, node_id: PublicKey),
413458
});
414459

460+
impl Bech32Encode for Offer {
461+
const BECH32_HRP: &'static str = "lno";
462+
}
463+
464+
type ParsedOffer = (Vec<u8>, OfferTlvStream);
465+
466+
impl FromStr for Offer {
467+
type Err = ParseError;
468+
469+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
470+
Self::from_bech32_str(s)
471+
}
472+
}
473+
474+
impl TryFrom<ParsedOffer> for Offer {
475+
type Error = ParseError;
476+
477+
fn try_from(offer: ParsedOffer) -> Result<Self, Self::Error> {
478+
let (bytes, tlv_stream) = offer;
479+
let contents = OfferContents::try_from(tlv_stream)?;
480+
Ok(Offer { bytes, contents })
481+
}
482+
}
483+
484+
impl TryFrom<OfferTlvStream> for OfferContents {
485+
type Error = SemanticError;
486+
487+
fn try_from(tlv_stream: OfferTlvStream) -> Result<Self, Self::Error> {
488+
let OfferTlvStream {
489+
chains, metadata, currency, amount, description, features, absolute_expiry, paths,
490+
issuer, quantity_max, node_id,
491+
} = tlv_stream;
492+
493+
let amount = match (currency, amount) {
494+
(None, None) => None,
495+
(None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }),
496+
(Some(_), None) => return Err(SemanticError::MissingAmount),
497+
(Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
498+
};
499+
500+
let description = match description {
501+
None => return Err(SemanticError::MissingDescription),
502+
Some(description) => description,
503+
};
504+
505+
let features = features.unwrap_or_else(OfferFeatures::empty);
506+
507+
let absolute_expiry = absolute_expiry
508+
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
509+
510+
let paths = match paths {
511+
Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths),
512+
paths => paths,
513+
};
514+
515+
let supported_quantity = Quantity::new(quantity_max);
516+
517+
if node_id.is_none() {
518+
return Err(SemanticError::MissingNodeId);
519+
}
520+
521+
Ok(OfferContents {
522+
chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
523+
supported_quantity, signing_pubkey: node_id,
524+
})
525+
}
526+
}
527+
528+
impl core::fmt::Display for Offer {
529+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
530+
self.fmt_bech32_str(f)
531+
}
532+
}
533+
415534
#[cfg(test)]
416535
mod tests {
417-
use super::{Amount, OfferBuilder, Quantity};
536+
use super::{Amount, Offer, OfferBuilder, Quantity};
418537

419538
use bitcoin::blockdata::constants::ChainHash;
420539
use bitcoin::network::constants::Network;
421540
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
541+
use core::convert::TryFrom;
422542
use core::num::NonZeroU64;
423543
use core::time::Duration;
424544
use ln::features::OfferFeatures;
@@ -441,7 +561,7 @@ mod tests {
441561
let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
442562
let tlv_stream = offer.as_tlv_stream();
443563
let mut buffer = Vec::new();
444-
offer.contents.write(&mut buffer).unwrap();
564+
offer.write(&mut buffer).unwrap();
445565

446566
assert_eq!(offer.bytes, buffer.as_slice());
447567
assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]);
@@ -468,6 +588,10 @@ mod tests {
468588
assert_eq!(tlv_stream.issuer, None);
469589
assert_eq!(tlv_stream.quantity_max, None);
470590
assert_eq!(tlv_stream.node_id, Some(&pubkey(42)));
591+
592+
if let Err(e) = Offer::try_from(buffer) {
593+
panic!("error parsing offer: {:?}", e);
594+
}
471595
}
472596

473597
#[test]
@@ -534,16 +658,14 @@ mod tests {
534658
assert_eq!(tlv_stream.amount, Some(1000));
535659
assert_eq!(tlv_stream.currency, None);
536660

537-
let builder = OfferBuilder::new("foo".into(), pubkey(42))
538-
.amount(currency_amount.clone());
539-
let tlv_stream = builder.offer.as_tlv_stream();
540-
assert_eq!(builder.offer.amount.as_ref(), Some(&currency_amount));
661+
let offer = OfferBuilder::new("foo".into(), pubkey(42))
662+
.amount(currency_amount.clone())
663+
.build()
664+
.unwrap();
665+
let tlv_stream = offer.as_tlv_stream();
666+
assert_eq!(offer.amount(), Some(&currency_amount));
541667
assert_eq!(tlv_stream.amount, Some(10));
542668
assert_eq!(tlv_stream.currency, Some(b"USD"));
543-
match builder.build() {
544-
Ok(_) => panic!("expected error"),
545-
Err(e) => assert_eq!(e, ()),
546-
}
547669

548670
let offer = OfferBuilder::new("foo".into(), pubkey(42))
549671
.amount(currency_amount.clone())
@@ -694,3 +816,88 @@ mod tests {
694816
assert_eq!(tlv_stream.quantity_max, None);
695817
}
696818
}
819+
820+
#[cfg(test)]
821+
mod bolt12_tests {
822+
use super::{Offer, ParseError};
823+
use bitcoin::bech32;
824+
use ln::msgs::DecodeError;
825+
826+
// TODO: Remove once test vectors are updated.
827+
#[ignore]
828+
#[test]
829+
fn encodes_offer_as_bech32_without_checksum() {
830+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
831+
let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
832+
let reencoded_offer = offer.to_string();
833+
dbg!(reencoded_offer.parse::<Offer>().unwrap());
834+
assert_eq!(reencoded_offer, encoded_offer);
835+
}
836+
837+
// TODO: Remove once test vectors are updated.
838+
#[ignore]
839+
#[test]
840+
fn parses_bech32_encoded_offers() {
841+
let offers = [
842+
// BOLT 12 test vectors
843+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
844+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
845+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
846+
"lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y",
847+
"lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y",
848+
// Two blinded paths
849+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
850+
];
851+
for encoded_offer in &offers {
852+
if let Err(e) = encoded_offer.parse::<Offer>() {
853+
panic!("Invalid offer ({:?}): {}", e, encoded_offer);
854+
}
855+
}
856+
}
857+
858+
#[test]
859+
fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
860+
let offers = [
861+
// BOLT 12 test vectors
862+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+",
863+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ",
864+
"+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
865+
"+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
866+
"ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
867+
];
868+
for encoded_offer in &offers {
869+
match encoded_offer.parse::<Offer>() {
870+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
871+
Err(e) => assert_eq!(e, ParseError::InvalidContinuation),
872+
}
873+
}
874+
875+
}
876+
877+
#[test]
878+
fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
879+
let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
880+
match encoded_offer.parse::<Offer>() {
881+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
882+
Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp),
883+
}
884+
}
885+
886+
#[test]
887+
fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
888+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso";
889+
match encoded_offer.parse::<Offer>() {
890+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
891+
Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))),
892+
}
893+
}
894+
895+
#[test]
896+
fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
897+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq";
898+
match encoded_offer.parse::<Offer>() {
899+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
900+
Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
901+
}
902+
}
903+
}

0 commit comments

Comments
 (0)