Skip to content

Commit c4b38ef

Browse files
committed
WIP: Offer parsing
1 parent 03812b0 commit c4b38ef

File tree

1 file changed

+295
-3
lines changed

1 file changed

+295
-3
lines changed

lightning/src/offers/offer.rs

Lines changed: 295 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,22 @@
99

1010
//! Data structures and encoding for `offer` messages.
1111
12+
use bitcoin::bech32;
13+
use bitcoin::bech32::FromBase32;
1214
use bitcoin::blockdata::constants::genesis_block;
1315
use bitcoin::hash_types::BlockHash;
1416
use bitcoin::hashes::{Hash, sha256};
1517
use bitcoin::network::constants::Network;
1618
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
1719
use bitcoin::secp256k1::schnorr::Signature;
20+
use core::convert::TryFrom;
1821
use core::num::NonZeroU64;
1922
use core::ops::{Bound, RangeBounds};
23+
use core::str::FromStr;
2024
use core::time::Duration;
2125
use ln::features::OfferFeatures;
22-
use util::ser::WithLength;
26+
use ln::msgs::DecodeError;
27+
use util::ser::{Readable, WithLength};
2328

2429
use prelude::*;
2530
use super::merkle;
@@ -261,8 +266,14 @@ impl Offer {
261266

262267
///
263268
pub fn node_id(&self) -> PublicKey {
264-
self.node_id.unwrap_or_else(||
265-
self.paths.as_ref().unwrap().first().unwrap().path.0.last().unwrap().node_id)
269+
Self::node_id_from_parts(self.node_id, self.paths.as_ref())
270+
}
271+
272+
fn node_id_from_parts(
273+
node_id: Option<PublicKey>, paths: Option<&Vec<BlindedPath>>
274+
) -> PublicKey {
275+
node_id.unwrap_or_else(||
276+
paths.unwrap().first().unwrap().path.0.last().unwrap().node_id)
266277
}
267278

268279
///
@@ -393,6 +404,205 @@ impl_writeable!(OnionMessagePath, { node_id, encrypted_recipient_data });
393404

394405
type Empty = ();
395406

407+
/// An offer parsed from a bech32-encoded string as a TLV stream and the corresponding bytes. The
408+
/// latter is used for signature verification.
409+
struct ParsedOffer(OfferTlvStream, Vec<u8>);
410+
411+
/// Error when parsing a bech32 encoded message using [`str::parse`].
412+
#[derive(Debug, PartialEq)]
413+
pub enum ParseError {
414+
/// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
415+
/// across multiple parts (i.e., '+' followed by whitespace).
416+
InvalidContinuation,
417+
/// The bech32 encoding's human-readable part does not match what was expected for the message
418+
/// being parsed.
419+
InvalidBech32Hrp,
420+
/// The string could not be bech32 decoded.
421+
Bech32(bech32::Error),
422+
/// The bech32 decoded string could not be decoded as the expected message type.
423+
Decode(DecodeError),
424+
/// The parsed message has invalid semantics.
425+
InvalidSemantics(SemanticError),
426+
}
427+
428+
#[derive(Debug, PartialEq)]
429+
///
430+
pub enum SemanticError {
431+
///
432+
UnsupportedChain,
433+
///
434+
UnexpectedCurrency,
435+
///
436+
MissingDescription,
437+
///
438+
MissingDestination,
439+
///
440+
MissingPaths,
441+
///
442+
InvalidQuantity,
443+
///
444+
UnexpectedRefund,
445+
///
446+
InvalidSignature(secp256k1::Error),
447+
}
448+
449+
impl From<bech32::Error> for ParseError {
450+
fn from(error: bech32::Error) -> Self {
451+
Self::Bech32(error)
452+
}
453+
}
454+
455+
impl From<DecodeError> for ParseError {
456+
fn from(error: DecodeError) -> Self {
457+
Self::Decode(error)
458+
}
459+
}
460+
461+
impl From<SemanticError> for ParseError {
462+
fn from(error: SemanticError) -> Self {
463+
Self::InvalidSemantics(error)
464+
}
465+
}
466+
467+
impl From<secp256k1::Error> for SemanticError {
468+
fn from(error: secp256k1::Error) -> Self {
469+
Self::InvalidSignature(error)
470+
}
471+
}
472+
473+
impl FromStr for Offer {
474+
type Err = ParseError;
475+
476+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
477+
Ok(Offer::try_from(ParsedOffer::from_str(s)?)?)
478+
}
479+
}
480+
481+
impl TryFrom<ParsedOffer> for Offer {
482+
type Error = SemanticError;
483+
484+
fn try_from(offer: ParsedOffer) -> Result<Self, Self::Error> {
485+
let ParsedOffer(OfferTlvStream {
486+
chains, currency, amount, description, features, absolute_expiry, paths, issuer,
487+
quantity_min, quantity_max, node_id, send_invoice, refund_for, signature,
488+
}, data) = offer;
489+
490+
let supported_chains = [
491+
genesis_block(Network::Bitcoin).block_hash(),
492+
genesis_block(Network::Testnet).block_hash(),
493+
genesis_block(Network::Signet).block_hash(),
494+
genesis_block(Network::Regtest).block_hash(),
495+
];
496+
let chains = match chains.map(Into::<Vec<_>>::into) {
497+
None => None,
498+
Some(chains) => match chains.first() {
499+
None => Some(chains),
500+
Some(chain) if supported_chains.contains(chain) => Some(chains),
501+
_ => return Err(SemanticError::UnsupportedChain),
502+
},
503+
};
504+
505+
let amount = match (currency, amount.map(Into::into)) {
506+
(None, None) => None,
507+
(None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }),
508+
(Some(_), None) => return Err(SemanticError::UnexpectedCurrency),
509+
(Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
510+
};
511+
512+
let description = match description {
513+
None => return Err(SemanticError::MissingDescription),
514+
Some(description) => description.into(),
515+
};
516+
517+
let absolute_expiry = absolute_expiry
518+
.map(Into::into)
519+
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
520+
521+
let issuer = issuer.map(Into::into);
522+
523+
let (node_id, paths) = match (node_id, paths.map(Into::<Vec<_>>::into)) {
524+
(None, None) => return Err(SemanticError::MissingDestination),
525+
(_, Some(paths)) if paths.is_empty() => return Err(SemanticError::MissingPaths),
526+
(_, paths) => (node_id, paths),
527+
};
528+
529+
let quantity_min = quantity_min.map(Into::into);
530+
let quantity_max = quantity_max.map(Into::into);
531+
if let Some(quantity_min) = quantity_min {
532+
if quantity_min < 1 {
533+
return Err(SemanticError::InvalidQuantity);
534+
}
535+
536+
if let Some(quantity_max) = quantity_max {
537+
if quantity_min > quantity_max {
538+
return Err(SemanticError::InvalidQuantity);
539+
}
540+
}
541+
}
542+
543+
if let Some(quantity_max) = quantity_max {
544+
if quantity_max < 1 {
545+
return Err(SemanticError::InvalidQuantity);
546+
}
547+
}
548+
549+
let send_invoice = match (send_invoice, refund_for) {
550+
(None, None) => None,
551+
(None, Some(_)) => return Err(SemanticError::UnexpectedRefund),
552+
(Some(_), _) => Some(SendInvoice { refund_for }),
553+
};
554+
555+
let id = merkle::root_hash(&data);
556+
if let Some(signature) = &signature {
557+
let digest = Offer::message_digest(id);
558+
let secp_ctx = Secp256k1::verification_only();
559+
let pubkey = Offer::node_id_from_parts(node_id, paths.as_ref());
560+
secp_ctx.verify_schnorr(signature, &digest, &pubkey.into())?;
561+
}
562+
563+
Ok(Offer {
564+
id, chains, amount, description, features, absolute_expiry, issuer, paths, quantity_min,
565+
quantity_max, node_id, send_invoice, signature,
566+
})
567+
}
568+
}
569+
570+
const OFFER_BECH32_HRP: &str = "lno";
571+
572+
impl FromStr for ParsedOffer {
573+
type Err = ParseError;
574+
575+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
576+
// Offer encoding may be split by '+' followed by optional whitespace.
577+
for chunk in s.split('+') {
578+
let chunk = chunk.trim_start();
579+
if chunk.is_empty() || chunk.contains(char::is_whitespace) {
580+
return Err(ParseError::InvalidContinuation);
581+
}
582+
}
583+
584+
let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::<String>();
585+
let (hrp, data) = bech32::decode_without_checksum(&s)?;
586+
587+
if hrp != OFFER_BECH32_HRP {
588+
return Err(ParseError::InvalidBech32Hrp);
589+
}
590+
591+
let data = Vec::<u8>::from_base32(&data)?;
592+
Ok(ParsedOffer(Readable::read(&mut &data[..])?, data))
593+
}
594+
}
595+
596+
impl core::fmt::Display for Offer {
597+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
598+
use bitcoin::bech32::ToBase32;
599+
let data = self.to_bytes().to_base32();
600+
bech32::encode_without_checksum_to_fmt(f, OFFER_BECH32_HRP, data).expect("HRP is valid").unwrap();
601+
602+
Ok(())
603+
}
604+
}
605+
396606
#[cfg(test)]
397607
mod tests {
398608
use super::{Amount, BlindedPath, Destination, OfferBuilder, OnionMessagePath, SendInvoice, merkle};
@@ -791,3 +1001,85 @@ mod tests {
7911001
assert_eq!(tlv_stream.send_invoice, Some(&()));
7921002
}
7931003
}
1004+
1005+
#[cfg(test)]
1006+
mod bolt12_tests {
1007+
use super::{Offer, ParseError, ParsedOffer};
1008+
use bitcoin::bech32;
1009+
use ln::msgs::DecodeError;
1010+
1011+
#[test]
1012+
fn encodes_offer_as_bech32_without_checksum() {
1013+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
1014+
let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
1015+
let reencoded_offer = offer.to_string();
1016+
dbg!(reencoded_offer.parse::<Offer>().unwrap());
1017+
assert_eq!(reencoded_offer, encoded_offer);
1018+
}
1019+
1020+
#[test]
1021+
fn parses_bech32_encoded_offers() {
1022+
let offers = [
1023+
// BOLT 12 test vectors
1024+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1025+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1026+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1027+
"lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y",
1028+
"lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y",
1029+
// Two blinded paths
1030+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1031+
];
1032+
for encoded_offer in &offers {
1033+
// TODO: Use Offer once Destination semantics are finalized.
1034+
if let Err(e) = encoded_offer.parse::<ParsedOffer>() {
1035+
panic!("Invalid offer ({:?}): {}", e, encoded_offer);
1036+
}
1037+
}
1038+
}
1039+
1040+
#[test]
1041+
fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
1042+
let offers = [
1043+
// BOLT 12 test vectors
1044+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+",
1045+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ",
1046+
"+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1047+
"+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1048+
"ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
1049+
];
1050+
for encoded_offer in &offers {
1051+
match encoded_offer.parse::<Offer>() {
1052+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1053+
Err(e) => assert_eq!(e, ParseError::InvalidContinuation),
1054+
}
1055+
}
1056+
1057+
}
1058+
1059+
#[test]
1060+
fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
1061+
let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
1062+
match encoded_offer.parse::<Offer>() {
1063+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1064+
Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp),
1065+
}
1066+
}
1067+
1068+
#[test]
1069+
fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
1070+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso";
1071+
match encoded_offer.parse::<Offer>() {
1072+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1073+
Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))),
1074+
}
1075+
}
1076+
1077+
#[test]
1078+
fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
1079+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq";
1080+
match encoded_offer.parse::<Offer>() {
1081+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1082+
Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
1083+
}
1084+
}
1085+
}

0 commit comments

Comments
 (0)