Skip to content

Commit 53024ae

Browse files
committed
WIP: Offer parsing
1 parent 814619c commit 53024ae

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,18 +9,23 @@
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::PaymentHash;
2226
use ln::features::OfferFeatures;
23-
use util::ser::WithLength;
27+
use ln::msgs::DecodeError;
28+
use util::ser::{Readable, WithLength};
2429

2530
use prelude::*;
2631
use super::merkle;
@@ -250,8 +255,14 @@ impl Offer {
250255

251256
///
252257
pub fn node_id(&self) -> PublicKey {
253-
self.node_id.unwrap_or_else(||
254-
self.paths.as_ref().unwrap().first().unwrap().path.0.last().unwrap().node_id)
258+
Self::node_id_from_parts(self.node_id, self.paths.as_ref())
259+
}
260+
261+
fn node_id_from_parts(
262+
node_id: Option<PublicKey>, paths: Option<&Vec<BlindedPath>>
263+
) -> PublicKey {
264+
node_id.unwrap_or_else(||
265+
paths.unwrap().first().unwrap().path.0.last().unwrap().node_id)
255266
}
256267

257268
///
@@ -388,6 +399,205 @@ impl_writeable!(OnionMessagePath, { node_id, encrypted_recipient_data });
388399

389400
type Empty = ();
390401

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

0 commit comments

Comments
 (0)