Skip to content

Commit c786d0f

Browse files
committed
WIP: Offer parsing
1 parent ebb9e8b commit c786d0f

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,15 +9,20 @@
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::network::constants::Network;
1517
use bitcoin::secp256k1::PublicKey;
18+
use core::convert::TryFrom;
1619
use core::num::NonZeroU64;
1720
use core::ops::{Bound, RangeBounds};
21+
use core::str::FromStr;
1822
use core::time::Duration;
1923
use ln::features::OfferFeatures;
20-
use util::ser::WithLength;
24+
use ln::msgs::DecodeError;
25+
use util::ser::{Readable, WithLength};
2126

2227
use prelude::*;
2328

@@ -245,8 +250,14 @@ impl Offer {
245250

246251
///
247252
pub fn node_id(&self) -> PublicKey {
248-
self.node_id.unwrap_or_else(||
249-
self.paths.as_ref().unwrap().first().unwrap().path.0.last().unwrap().node_id)
253+
Self::node_id_from_parts(self.node_id, self.paths.as_ref())
254+
}
255+
256+
fn node_id_from_parts(
257+
node_id: Option<PublicKey>, paths: Option<&Vec<BlindedPath>>
258+
) -> PublicKey {
259+
node_id.unwrap_or_else(||
260+
paths.unwrap().first().unwrap().path.0.last().unwrap().node_id)
250261
}
251262

252263
///
@@ -352,6 +363,205 @@ impl_writeable!(OnionMessagePath, { node_id, encrypted_recipient_data });
352363

353364
type Empty = ();
354365

366+
/// An offer parsed from a bech32-encoded string as a TLV stream and the corresponding bytes. The
367+
/// latter is used for signature verification.
368+
struct ParsedOffer(OfferTlvStream, Vec<u8>);
369+
370+
/// Error when parsing a bech32 encoded message using [`str::parse`].
371+
#[derive(Debug, PartialEq)]
372+
pub enum ParseError {
373+
/// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages
374+
/// across multiple parts (i.e., '+' followed by whitespace).
375+
InvalidContinuation,
376+
/// The bech32 encoding's human-readable part does not match what was expected for the message
377+
/// being parsed.
378+
InvalidBech32Hrp,
379+
/// The string could not be bech32 decoded.
380+
Bech32(bech32::Error),
381+
/// The bech32 decoded string could not be decoded as the expected message type.
382+
Decode(DecodeError),
383+
/// The parsed message has invalid semantics.
384+
InvalidSemantics(SemanticError),
385+
}
386+
387+
#[derive(Debug, PartialEq)]
388+
///
389+
pub enum SemanticError {
390+
///
391+
UnsupportedChain,
392+
///
393+
UnexpectedCurrency,
394+
///
395+
MissingDescription,
396+
///
397+
MissingDestination,
398+
///
399+
MissingPaths,
400+
///
401+
InvalidQuantity,
402+
///
403+
UnexpectedRefund,
404+
///
405+
InvalidSignature(secp256k1::Error),
406+
}
407+
408+
impl From<bech32::Error> for ParseError {
409+
fn from(error: bech32::Error) -> Self {
410+
Self::Bech32(error)
411+
}
412+
}
413+
414+
impl From<DecodeError> for ParseError {
415+
fn from(error: DecodeError) -> Self {
416+
Self::Decode(error)
417+
}
418+
}
419+
420+
impl From<SemanticError> for ParseError {
421+
fn from(error: SemanticError) -> Self {
422+
Self::InvalidSemantics(error)
423+
}
424+
}
425+
426+
impl From<secp256k1::Error> for SemanticError {
427+
fn from(error: secp256k1::Error) -> Self {
428+
Self::InvalidSignature(error)
429+
}
430+
}
431+
432+
impl FromStr for Offer {
433+
type Err = ParseError;
434+
435+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
436+
Ok(Offer::try_from(ParsedOffer::from_str(s)?)?)
437+
}
438+
}
439+
440+
impl TryFrom<ParsedOffer> for Offer {
441+
type Error = SemanticError;
442+
443+
fn try_from(offer: ParsedOffer) -> Result<Self, Self::Error> {
444+
let ParsedOffer(OfferTlvStream {
445+
chains, currency, amount, description, features, absolute_expiry, paths, issuer,
446+
quantity_min, quantity_max, node_id, send_invoice, refund_for, signature,
447+
}, data) = offer;
448+
449+
let supported_chains = [
450+
genesis_block(Network::Bitcoin).block_hash(),
451+
genesis_block(Network::Testnet).block_hash(),
452+
genesis_block(Network::Signet).block_hash(),
453+
genesis_block(Network::Regtest).block_hash(),
454+
];
455+
let chains = match chains.map(Into::<Vec<_>>::into) {
456+
None => None,
457+
Some(chains) => match chains.first() {
458+
None => Some(chains),
459+
Some(chain) if supported_chains.contains(chain) => Some(chains),
460+
_ => return Err(SemanticError::UnsupportedChain),
461+
},
462+
};
463+
464+
let amount = match (currency, amount.map(Into::into)) {
465+
(None, None) => None,
466+
(None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }),
467+
(Some(_), None) => return Err(SemanticError::UnexpectedCurrency),
468+
(Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
469+
};
470+
471+
let description = match description {
472+
None => return Err(SemanticError::MissingDescription),
473+
Some(description) => description.into(),
474+
};
475+
476+
let absolute_expiry = absolute_expiry
477+
.map(Into::into)
478+
.map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch));
479+
480+
let issuer = issuer.map(Into::into);
481+
482+
let (node_id, paths) = match (node_id, paths.map(Into::<Vec<_>>::into)) {
483+
(None, None) => return Err(SemanticError::MissingDestination),
484+
(_, Some(paths)) if paths.is_empty() => return Err(SemanticError::MissingPaths),
485+
(_, paths) => (node_id, paths),
486+
};
487+
488+
let quantity_min = quantity_min.map(Into::into);
489+
let quantity_max = quantity_max.map(Into::into);
490+
if let Some(quantity_min) = quantity_min {
491+
if quantity_min < 1 {
492+
return Err(SemanticError::InvalidQuantity);
493+
}
494+
495+
if let Some(quantity_max) = quantity_max {
496+
if quantity_min > quantity_max {
497+
return Err(SemanticError::InvalidQuantity);
498+
}
499+
}
500+
}
501+
502+
if let Some(quantity_max) = quantity_max {
503+
if quantity_max < 1 {
504+
return Err(SemanticError::InvalidQuantity);
505+
}
506+
}
507+
508+
let send_invoice = match (send_invoice, refund_for) {
509+
(None, None) => None,
510+
(None, Some(_)) => return Err(SemanticError::UnexpectedRefund),
511+
(Some(_), _) => Some(SendInvoice { refund_for }),
512+
};
513+
514+
let id = merkle::root_hash(&data);
515+
if let Some(signature) = &signature {
516+
let digest = Offer::message_digest(id);
517+
let secp_ctx = Secp256k1::verification_only();
518+
let pubkey = Offer::node_id_from_parts(node_id, paths.as_ref());
519+
secp_ctx.verify_schnorr(signature, &digest, &pubkey.into())?;
520+
}
521+
522+
Ok(Offer {
523+
id, chains, amount, description, features, absolute_expiry, issuer, paths, quantity_min,
524+
quantity_max, node_id, send_invoice, signature,
525+
})
526+
}
527+
}
528+
529+
const OFFER_BECH32_HRP: &str = "lno";
530+
531+
impl FromStr for ParsedOffer {
532+
type Err = ParseError;
533+
534+
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
535+
// Offer encoding may be split by '+' followed by optional whitespace.
536+
for chunk in s.split('+') {
537+
let chunk = chunk.trim_start();
538+
if chunk.is_empty() || chunk.contains(char::is_whitespace) {
539+
return Err(ParseError::InvalidContinuation);
540+
}
541+
}
542+
543+
let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::<String>();
544+
let (hrp, data) = bech32::decode_without_checksum(&s)?;
545+
546+
if hrp != OFFER_BECH32_HRP {
547+
return Err(ParseError::InvalidBech32Hrp);
548+
}
549+
550+
let data = Vec::<u8>::from_base32(&data)?;
551+
Ok(ParsedOffer(Readable::read(&mut &data[..])?, data))
552+
}
553+
}
554+
555+
impl core::fmt::Display for Offer {
556+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
557+
use bitcoin::bech32::ToBase32;
558+
let data = self.to_bytes().to_base32();
559+
bech32::encode_without_checksum_to_fmt(f, OFFER_BECH32_HRP, data).expect("HRP is valid").unwrap();
560+
561+
Ok(())
562+
}
563+
}
564+
355565
#[cfg(test)]
356566
mod tests {
357567
use super::{Amount, BlindedPath, Destination, OfferBuilder, OnionMessagePath, SendInvoice};
@@ -729,3 +939,85 @@ mod tests {
729939
assert_eq!(tlv_stream.send_invoice, Some(&()));
730940
}
731941
}
942+
943+
#[cfg(test)]
944+
mod bolt12_tests {
945+
use super::{Offer, ParseError, ParsedOffer};
946+
use bitcoin::bech32;
947+
use ln::msgs::DecodeError;
948+
949+
#[test]
950+
fn encodes_offer_as_bech32_without_checksum() {
951+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
952+
let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
953+
let reencoded_offer = offer.to_string();
954+
dbg!(reencoded_offer.parse::<Offer>().unwrap());
955+
assert_eq!(reencoded_offer, encoded_offer);
956+
}
957+
958+
#[test]
959+
fn parses_bech32_encoded_offers() {
960+
let offers = [
961+
// BOLT 12 test vectors
962+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
963+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
964+
"l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
965+
"lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y",
966+
"lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y",
967+
// Two blinded paths
968+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
969+
];
970+
for encoded_offer in &offers {
971+
// TODO: Use Offer once Destination semantics are finalized.
972+
if let Err(e) = encoded_offer.parse::<ParsedOffer>() {
973+
panic!("Invalid offer ({:?}): {}", e, encoded_offer);
974+
}
975+
}
976+
}
977+
978+
#[test]
979+
fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
980+
let offers = [
981+
// BOLT 12 test vectors
982+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+",
983+
"lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ",
984+
"+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
985+
"+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
986+
"ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy",
987+
];
988+
for encoded_offer in &offers {
989+
match encoded_offer.parse::<Offer>() {
990+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
991+
Err(e) => assert_eq!(e, ParseError::InvalidContinuation),
992+
}
993+
}
994+
995+
}
996+
997+
#[test]
998+
fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
999+
let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
1000+
match encoded_offer.parse::<Offer>() {
1001+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1002+
Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp),
1003+
}
1004+
}
1005+
1006+
#[test]
1007+
fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
1008+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso";
1009+
match encoded_offer.parse::<Offer>() {
1010+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1011+
Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))),
1012+
}
1013+
}
1014+
1015+
#[test]
1016+
fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
1017+
let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq";
1018+
match encoded_offer.parse::<Offer>() {
1019+
Ok(_) => panic!("Valid offer: {}", encoded_offer),
1020+
Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
1021+
}
1022+
}
1023+
}

0 commit comments

Comments
 (0)