From 391e4c2e045cb7df4281ca410153904d6b64b880 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 11 Aug 2022 16:51:06 -0500 Subject: [PATCH 01/12] 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. --- lightning/src/offers/mod.rs | 1 + lightning/src/offers/offer.rs | 229 ++++++++++++++++++++++++++++++- lightning/src/offers/parse.rs | 103 ++++++++++++++ lightning/src/util/ser_macros.rs | 2 +- 4 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 lightning/src/offers/parse.rs diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 2f961a0bb6e..273650285c6 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -13,3 +13,4 @@ //! Offers are a flexible protocol for Lightning payments. pub mod offer; +pub mod parse; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index bf569fbc022..09f9f490ea2 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -18,13 +18,15 @@ //! extern crate core; //! extern crate lightning; //! +//! use core::convert::TryFrom; //! use core::num::NonZeroU64; //! use core::time::Duration; //! //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; -//! use lightning::offers::offer::{OfferBuilder, Quantity}; +//! use lightning::offers::offer::{Offer, OfferBuilder, Quantity}; +//! use lightning::offers::parse::ParseError; +//! use lightning::util::ser::{Readable, Writeable}; //! -//! # use bitcoin::secp256k1; //! # use lightning::onion_message::BlindedPath; //! # #[cfg(feature = "std")] //! # use std::time::SystemTime; @@ -33,9 +35,9 @@ //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() } //! # //! # #[cfg(feature = "std")] -//! # fn build() -> Result<(), secp256k1::Error> { +//! # fn build() -> Result<(), ParseError> { //! let secp_ctx = Secp256k1::new(); -//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); //! let pubkey = PublicKey::from(keys); //! //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); @@ -48,6 +50,19 @@ //! .path(create_another_blinded_path()) //! .build() //! .unwrap(); +//! +//! // Encode as a bech32 string for use in a QR code. +//! let encoded_offer = offer.to_string(); +//! +//! // Parse from a bech32 string after scanning from a QR code. +//! let offer = encoded_offer.parse::()?; +//! +//! // Encode offer as raw bytes. +//! let mut bytes = Vec::new(); +//! offer.write(&mut bytes).unwrap(); +//! +//! // Decode raw bytes into an offer. +//! let offer = Offer::try_from(bytes)?; //! # Ok(()) //! # } //! ``` @@ -55,13 +70,16 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::PublicKey; +use core::convert::TryFrom; use core::num::NonZeroU64; +use core::str::FromStr; use core::time::Duration; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; +use crate::offers::parse::{Bech32Encode, ParseError, SemanticError}; use crate::onion_message::BlindedPath; -use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; use crate::prelude::*; @@ -321,6 +339,12 @@ impl Offer { } } +impl AsRef<[u8]> for Offer { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + impl OfferContents { pub fn implied_chain(&self) -> ChainHash { ChainHash::using_genesis_block(Network::Bitcoin) @@ -359,12 +383,27 @@ impl OfferContents { } } +impl Writeable for Offer { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + impl Writeable for OfferContents { fn write(&self, writer: &mut W) -> Result<(), io::Error> { self.as_tlv_stream().write(writer) } } +impl TryFrom> for Offer { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let tlv_stream: OfferTlvStream = Readable::read(&mut &bytes[..])?; + Offer::try_from((bytes, tlv_stream)) + } +} + /// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or /// another currency. #[derive(Clone, Debug, PartialEq)] @@ -400,6 +439,15 @@ impl Quantity { Quantity::Bounded(NonZeroU64::new(1).unwrap()) } + fn new(quantity: Option) -> Self { + match quantity { + None => Quantity::one(), + Some(0) => Quantity::Unbounded, + Some(1) => unreachable!(), + Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), + } + } + fn to_tlv_record(&self) -> Option { match self { Quantity::Bounded(n) => { @@ -425,13 +473,91 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, { (22, node_id: PublicKey), }); +impl Bech32Encode for Offer { + const BECH32_HRP: &'static str = "lno"; +} + +type ParsedOffer = (Vec, OfferTlvStream); + +impl FromStr for Offer { + type Err = ParseError; + + fn from_str(s: &str) -> Result::Err> { + Self::from_bech32_str(s) + } +} + +impl TryFrom for Offer { + type Error = ParseError; + + fn try_from(offer: ParsedOffer) -> Result { + let (bytes, tlv_stream) = offer; + let contents = OfferContents::try_from(tlv_stream)?; + Ok(Offer { bytes, contents }) + } +} + +impl TryFrom for OfferContents { + type Error = SemanticError; + + fn try_from(tlv_stream: OfferTlvStream) -> Result { + let OfferTlvStream { + chains, metadata, currency, amount, description, features, absolute_expiry, paths, + issuer, quantity_max, node_id, + } = tlv_stream; + + let amount = match (currency, amount) { + (None, None) => None, + (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), + (Some(_), None) => return Err(SemanticError::MissingAmount), + (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }), + }; + + let description = match description { + None => return Err(SemanticError::MissingDescription), + Some(description) => description, + }; + + let features = features.unwrap_or_else(OfferFeatures::empty); + + let absolute_expiry = absolute_expiry + .map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch)); + + let paths = match paths { + Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths), + paths => paths, + }; + + let supported_quantity = match quantity_max { + Some(1) => return Err(SemanticError::InvalidQuantity), + _ => Quantity::new(quantity_max), + }; + + if node_id.is_none() { + return Err(SemanticError::MissingNodeId); + } + + Ok(OfferContents { + chains, metadata, amount, description, features, absolute_expiry, issuer, paths, + supported_quantity, signing_pubkey: node_id, + }) + } +} + +impl core::fmt::Display for Offer { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + #[cfg(test)] mod tests { - use super::{Amount, OfferBuilder, Quantity}; + use super::{Amount, Offer, OfferBuilder, Quantity}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::convert::TryFrom; use core::num::NonZeroU64; use core::time::Duration; use crate::ln::features::OfferFeatures; @@ -454,7 +580,7 @@ mod tests { let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); let tlv_stream = offer.as_tlv_stream(); let mut buffer = Vec::new(); - offer.contents.write(&mut buffer).unwrap(); + offer.write(&mut buffer).unwrap(); assert_eq!(offer.bytes, buffer.as_slice()); assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); @@ -481,6 +607,10 @@ mod tests { assert_eq!(tlv_stream.issuer, None); assert_eq!(tlv_stream.quantity_max, None); assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); + + if let Err(e) = Offer::try_from(buffer) { + panic!("error parsing offer: {:?}", e); + } } #[test] @@ -707,3 +837,88 @@ mod tests { assert_eq!(tlv_stream.quantity_max, None); } } + +#[cfg(test)] +mod bolt12_tests { + use super::{Offer, ParseError}; + use bitcoin::bech32; + use crate::ln::msgs::DecodeError; + + // TODO: Remove once test vectors are updated. + #[ignore] + #[test] + fn encodes_offer_as_bech32_without_checksum() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; + let offer = dbg!(encoded_offer.parse::().unwrap()); + let reencoded_offer = offer.to_string(); + dbg!(reencoded_offer.parse::().unwrap()); + assert_eq!(reencoded_offer, encoded_offer); + } + + // TODO: Remove once test vectors are updated. + #[ignore] + #[test] + fn parses_bech32_encoded_offers() { + let offers = [ + // BOLT 12 test vectors + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y", + "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y", + // Two blinded paths + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + ]; + for encoded_offer in &offers { + if let Err(e) = encoded_offer.parse::() { + panic!("Invalid offer ({:?}): {}", e, encoded_offer); + } + } + } + + #[test] + fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() { + let offers = [ + // BOLT 12 test vectors + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+", + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ", + "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + ]; + for encoded_offer in &offers { + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::InvalidContinuation), + } + } + + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() { + let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp), + } + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))), + } + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } +} diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs new file mode 100644 index 00000000000..5a3e853eb9e --- /dev/null +++ b/lightning/src/offers/parse.rs @@ -0,0 +1,103 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Parsing and formatting for bech32 message encoding. + +use bitcoin::bech32; +use bitcoin::bech32::{FromBase32, ToBase32}; +use core::convert::TryFrom; +use core::fmt; +use crate::ln::msgs::DecodeError; + +use crate::prelude::*; + +/// Indicates a message can be encoded using bech32. +pub(crate) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { + /// Human readable part of the message's bech32 encoding. + const BECH32_HRP: &'static str; + + /// Parses a bech32-encoded message into a TLV stream. + fn from_bech32_str(s: &str) -> Result { + // Offer encoding may be split by '+' followed by optional whitespace. + for chunk in s.split('+') { + let chunk = chunk.trim_start(); + if chunk.is_empty() || chunk.contains(char::is_whitespace) { + return Err(ParseError::InvalidContinuation); + } + } + + let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::(); + let (hrp, data) = bech32::decode_without_checksum(&s)?; + + if hrp != Self::BECH32_HRP { + return Err(ParseError::InvalidBech32Hrp); + } + + let data = Vec::::from_base32(&data)?; + Self::try_from(data) + } + + /// Formats the message using bech32-encoding. + fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) + .expect("HRP is valid").unwrap(); + + Ok(()) + } +} + +/// Error when parsing a bech32 encoded message using [`str::parse`]. +#[derive(Debug, PartialEq)] +pub enum ParseError { + /// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages + /// across multiple parts (i.e., '+' followed by whitespace). + InvalidContinuation, + /// The bech32 encoding's human-readable part does not match what was expected for the message + /// being parsed. + InvalidBech32Hrp, + /// The string could not be bech32 decoded. + Bech32(bech32::Error), + /// The bech32 decoded string could not be decoded as the expected message type. + Decode(DecodeError), + /// The parsed message has invalid semantics. + InvalidSemantics(SemanticError), +} + +/// Error when interpreting a TLV stream as a specific type. +#[derive(Debug, PartialEq)] +pub enum SemanticError { + /// An amount was expected but was missing. + MissingAmount, + /// A required description was not provided. + MissingDescription, + /// A node id was not provided. + MissingNodeId, + /// An empty set of blinded paths was provided. + MissingPaths, + /// An unsupported quantity was provided. + InvalidQuantity, +} + +impl From for ParseError { + fn from(error: bech32::Error) -> Self { + Self::Bech32(error) + } +} + +impl From for ParseError { + fn from(error: DecodeError) -> Self { + Self::Decode(error) + } +} + +impl From for ParseError { + fn from(error: SemanticError) -> Self { + Self::InvalidSemantics(error) + } +} diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 3ec0848680f..129fb8f407b 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -476,7 +476,7 @@ macro_rules! tlv_stream { $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* }) => { #[derive(Debug)] - struct $name { + pub(crate) struct $name { $( $field: Option, )* From 25e3a828920b35bd4cf314dbdad545cabee496bd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 21 Sep 2022 13:09:06 -0500 Subject: [PATCH 02/12] Use SemanticError in OfferBuilder::build --- lightning/src/offers/offer.rs | 16 ++++++++++------ lightning/src/offers/parse.rs | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 09f9f490ea2..c6c48a93788 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -48,8 +48,7 @@ //! .issuer("Foo Bar".to_string()) //! .path(create_blinded_path()) //! .path(create_another_blinded_path()) -//! .build() -//! .unwrap(); +//! .build()?; //! //! // Encode as a bech32 string for use in a QR code. //! let encoded_offer = offer.to_string(); @@ -195,14 +194,14 @@ impl OfferBuilder { } /// Builds an [`Offer`] from the builder's settings. - pub fn build(mut self) -> Result { + pub fn build(mut self) -> Result { match self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { if amount_msats > MAX_VALUE_MSAT { - return Err(()); + return Err(SemanticError::InvalidAmount); } }, - Some(Amount::Currency { .. }) => unreachable!(), + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), None => {}, } @@ -562,6 +561,7 @@ mod tests { use core::time::Duration; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; + use crate::offers::parse::SemanticError; use crate::onion_message::{BlindedHop, BlindedPath}; use crate::util::ser::Writeable; use crate::util::string::PrintableString; @@ -687,6 +687,10 @@ mod tests { assert_eq!(builder.offer.amount, Some(currency_amount.clone())); assert_eq!(tlv_stream.amount, Some(10)); assert_eq!(tlv_stream.currency, Some(b"USD")); + match builder.build() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnsupportedCurrency), + } let offer = OfferBuilder::new("foo".into(), pubkey(42)) .amount(currency_amount.clone()) @@ -700,7 +704,7 @@ mod tests { let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() { Ok(_) => panic!("expected error"), - Err(e) => assert_eq!(e, ()), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), } } diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 5a3e853eb9e..03fdd230a60 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -74,6 +74,10 @@ pub enum ParseError { pub enum SemanticError { /// An amount was expected but was missing. MissingAmount, + /// An amount exceeded the maximum number of bitcoin. + InvalidAmount, + /// A currency was provided that is not supported. + UnsupportedCurrency, /// A required description was not provided. MissingDescription, /// A node id was not provided. From 0af1afff9282dc5f951f37cd0055cee0e83c2967 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 21 Sep 2022 22:38:11 -0500 Subject: [PATCH 03/12] Offer parsing tests Test semantic errors when parsing offer bytes. --- lightning/src/offers/offer.rs | 161 +++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index c6c48a93788..7d5939c633a 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -561,7 +561,7 @@ mod tests { use core::time::Duration; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; - use crate::offers::parse::SemanticError; + use crate::offers::parse::{ParseError, SemanticError}; use crate::onion_message::{BlindedHop, BlindedPath}; use crate::util::ser::Writeable; use crate::util::string::PrintableString; @@ -840,6 +840,165 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::one()); assert_eq!(tlv_stream.quantity_max, None); } + + #[test] + fn parses_offer_with_chains() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + } + + #[test] + fn parses_offer_with_amount() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .amount(Amount::Bitcoin { amount_msats: 1000 }) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = Some(1000); + tlv_stream.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + if let Err(e) = Offer::try_from(encoded_offer) { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = None; + tlv_stream.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)), + } + } + + #[test] + fn parses_offer_with_description() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.description = None; + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingDescription)); + }, + } + } + + #[test] + fn parses_offer_with_paths() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .path(BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }) + .path(BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + builder.offer.paths = Some(vec![]); + + let offer = builder.build().unwrap(); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + } + + #[test] + fn parses_offer_with_quantity() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::one()) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Unbounded) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap())) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.quantity_max = Some(1); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity)); + }, + } + } + + #[test] + fn parses_offer_with_node_id() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + builder.offer.signing_pubkey = None; + + let offer = builder.build().unwrap(); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingNodeId)), + } + } } #[cfg(test)] From c15b93d762e2d481335d0106a5fc7c096839987c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 30 Sep 2022 15:50:12 -0500 Subject: [PATCH 04/12] Limit TLV stream decoding to type ranges BOLT 12 messages are limited to a range of TLV record types. Refactor decode_tlv_stream into a decode_tlv_stream_range macro for limiting which types are parsed. Requires a SeekReadable trait for rewinding when a type outside of the range is seen. This allows for composing TLV streams of different ranges. Updates offer parsing accordingly and adds a test demonstrating failure if a type outside of the range is included. --- lightning/src/offers/offer.rs | 38 ++++++++++++++++++++++---------- lightning/src/offers/parse.rs | 25 +++++++++++++++++++++ lightning/src/util/ser.rs | 9 +++++++- lightning/src/util/ser_macros.rs | 32 ++++++++++++++++++++++----- 4 files changed, 85 insertions(+), 19 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 7d5939c633a..36db2dc23c4 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -76,9 +76,9 @@ use core::time::Duration; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; -use crate::offers::parse::{Bech32Encode, ParseError, SemanticError}; +use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::onion_message::BlindedPath; -use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; use crate::prelude::*; @@ -398,8 +398,8 @@ impl TryFrom> for Offer { type Error = ParseError; fn try_from(bytes: Vec) -> Result { - let tlv_stream: OfferTlvStream = Readable::read(&mut &bytes[..])?; - Offer::try_from((bytes, tlv_stream)) + let parsed_message = ParsedMessage::::try_from(bytes)?; + Offer::try_from(parsed_message) } } @@ -458,7 +458,7 @@ impl Quantity { } } -tlv_stream!(OfferTlvStream, OfferTlvStreamRef, { +tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, { (2, chains: (Vec, WithoutLength)), (4, metadata: (Vec, WithoutLength)), (6, currency: CurrencyCode), @@ -476,8 +476,6 @@ impl Bech32Encode for Offer { const BECH32_HRP: &'static str = "lno"; } -type ParsedOffer = (Vec, OfferTlvStream); - impl FromStr for Offer { type Err = ParseError; @@ -486,11 +484,11 @@ impl FromStr for Offer { } } -impl TryFrom for Offer { +impl TryFrom> for Offer { type Error = ParseError; - fn try_from(offer: ParsedOffer) -> Result { - let (bytes, tlv_stream) = offer; + fn try_from(offer: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = offer; let contents = OfferContents::try_from(tlv_stream)?; Ok(Offer { bytes, contents }) } @@ -560,10 +558,10 @@ mod tests { use core::num::NonZeroU64; use core::time::Duration; use crate::ln::features::OfferFeatures; - use crate::ln::msgs::MAX_VALUE_MSAT; + use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::parse::{ParseError, SemanticError}; use crate::onion_message::{BlindedHop, BlindedPath}; - use crate::util::ser::Writeable; + use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; fn pubkey(byte: u8) -> PublicKey { @@ -999,6 +997,22 @@ mod tests { Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingNodeId)), } } + + #[test] + fn fails_parsing_offer_with_extra_tlv_records() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(80).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } } #[cfg(test)] diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 03fdd230a60..4aa973b4c3e 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -13,7 +13,9 @@ use bitcoin::bech32; use bitcoin::bech32::{FromBase32, ToBase32}; use core::convert::TryFrom; use core::fmt; +use crate::io; use crate::ln::msgs::DecodeError; +use crate::util::ser::SeekReadable; use crate::prelude::*; @@ -52,6 +54,29 @@ pub(crate) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> } } +/// A wrapper for reading a message as a TLV stream `T` from a byte sequence, while still +/// maintaining ownership of the bytes for later use. +pub(crate) struct ParsedMessage { + pub bytes: Vec, + pub tlv_stream: T, +} + +impl TryFrom> for ParsedMessage { + type Error = DecodeError; + + fn try_from(bytes: Vec) -> Result { + let mut cursor = crate::io::Cursor::new(bytes); + let tlv_stream: T = SeekReadable::read(&mut cursor)?; + + if cursor.position() < cursor.get_ref().len() as u64 { + return Err(DecodeError::InvalidValue); + } + + let bytes = cursor.into_inner(); + Ok(Self { bytes, tlv_stream }) + } +} + /// Error when parsing a bech32 encoded message using [`str::parse`]. #[derive(Debug, PartialEq)] pub enum ParseError { diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 7c4ba7fd50f..04dc04e55b0 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -11,7 +11,7 @@ //! as ChannelsManagers and ChannelMonitors. use crate::prelude::*; -use crate::io::{self, Read, Write}; +use crate::io::{self, Read, Seek, Write}; use crate::io_extras::{copy, sink}; use core::hash::Hash; use crate::sync::Mutex; @@ -219,6 +219,13 @@ pub trait Readable fn read(reader: &mut R) -> Result; } +/// A trait that various rust-lightning types implement allowing them to be read in from a +/// `Read + Seek`. +pub(crate) trait SeekReadable where Self: Sized { + /// Reads a Self in from the given Read + fn read(reader: &mut R) -> Result; +} + /// A trait that various higher-level rust-lightning types implement allowing them to be read in /// from a Read given some additional set of arguments which is required to deserialize. /// diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 129fb8f407b..4e3198006a7 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -201,6 +201,17 @@ macro_rules! decode_tlv { // `Ok(false)` if the message type is unknown, and `Err(DecodeError)` if parsing fails. macro_rules! decode_tlv_stream { ($stream: expr, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} + $(, $decode_custom_tlv: expr)?) => { { + let rewind = |_, _| { unreachable!() }; + use core::ops::RangeBounds; + decode_tlv_stream_range!( + $stream, .., rewind, {$(($type, $field, $fieldty)),*} $(, $decode_custom_tlv)? + ); + } } +} + +macro_rules! decode_tlv_stream_range { + ($stream: expr, $range: expr, $rewind: ident, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} $(, $decode_custom_tlv: expr)?) => { { use $crate::ln::msgs::DecodeError; let mut last_seen_type: Option = None; @@ -215,7 +226,7 @@ macro_rules! decode_tlv_stream { // UnexpectedEof. This should in every case be largely cosmetic, but its nice to // pass the TLV test vectors exactly, which requre this distinction. let mut tracking_reader = ser::ReadTrackingReader::new(&mut stream_ref); - match $crate::util::ser::Readable::read(&mut tracking_reader) { + match <$crate::util::ser::BigSize as $crate::util::ser::Readable>::read(&mut tracking_reader) { Err(DecodeError::ShortRead) => { if !tracking_reader.have_read { break 'tlv_read; @@ -224,7 +235,13 @@ macro_rules! decode_tlv_stream { } }, Err(e) => return Err(e), - Ok(t) => t, + Ok(t) => if $range.contains(&t.0) { t } else { + use $crate::util::ser::Writeable; + drop(tracking_reader); + let bytes_read = t.serialized_length(); + $rewind(stream_ref, bytes_read); + break 'tlv_read; + }, } }; @@ -472,7 +489,7 @@ macro_rules! impl_writeable_tlv_based { /// [`Readable`]: crate::util::ser::Readable /// [`Writeable`]: crate::util::ser::Writeable macro_rules! tlv_stream { - ($name:ident, $nameref:ident, { + ($name:ident, $nameref:ident, $range:expr, { $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* }) => { #[derive(Debug)] @@ -497,12 +514,15 @@ macro_rules! tlv_stream { } } - impl $crate::util::ser::Readable for $name { - fn read(reader: &mut R) -> Result { + impl $crate::util::ser::SeekReadable for $name { + fn read(reader: &mut R) -> Result { $( init_tlv_field_var!($field, option); )* - decode_tlv_stream!(reader, { + let rewind = |cursor: &mut R, offset: usize| { + cursor.seek($crate::io::SeekFrom::Current(-(offset as i64))).expect(""); + }; + decode_tlv_stream_range!(reader, $range, rewind, { $(($type, $field, (option, encoding: $fieldty))),* }); From cd9431fb0c6fc783830ea34394bebcd89416f320 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Sep 2022 16:57:46 -0500 Subject: [PATCH 05/12] Invoice request message interface and data format Define an interface for BOLT 12 `invoice_request` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed when constructing an `invoice` message. This is because it must mirror all the `offer` and `invoice_request` TLV records, including unknown ones, which aren't represented in the contents. The contents will be used in `invoice` messages to avoid duplication. Some fields while required in a typical user-pays-merchant flow may not be necessary in the merchant-pays-user flow (e.g., refund, ATM). --- lightning/src/offers/invoice_request.rs | 100 ++++++++++++++++++++++++ lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 17 ++-- lightning/src/offers/payer.rs | 19 +++++ 4 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 lightning/src/offers/invoice_request.rs create mode 100644 lightning/src/offers/payer.rs diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs new file mode 100644 index 00000000000..92329ac136a --- /dev/null +++ b/lightning/src/offers/invoice_request.rs @@ -0,0 +1,100 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice_request` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::schnorr::Signature; +use crate::ln::features::OfferFeatures; +use crate::offers::offer::OfferContents; +use crate::offers::payer::PayerContents; + +use crate::prelude::*; + +/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. +/// +/// An offer may provided choices such as quantity, amount, chain, features, etc. An invoice request +/// specifies these such that the recipient can send an invoice for payment. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +pub struct InvoiceRequest { + bytes: Vec, + contents: InvoiceRequestContents, + signature: Option, +} + +/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. +#[derive(Clone, Debug)] +pub(crate) struct InvoiceRequestContents { + payer: PayerContents, + offer: OfferContents, + chain: Option, + amount_msats: Option, + features: Option, + quantity: Option, + payer_id: PublicKey, + payer_note: Option, +} + +impl InvoiceRequest { + /// An unpredictable series of bytes, typically containing information about the derivation of + /// [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn metadata(&self) -> Option<&Vec> { + self.contents.payer.0.as_ref() + } + + /// A chain from [`Offer::chains`] that the offer is valid for. + /// + /// [`Offer::chains`]: crate::offers::offer::Offer::chains + pub fn chain(&self) -> ChainHash { + self.contents.chain.unwrap_or_else(|| self.contents.offer.implied_chain()) + } + + /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which + /// must be greater than or equal to [`Offer::amount`], converted if necessary. + /// + /// [`chain`]: Self::chain + /// [`Offer::amount`]: crate::offers::offer::Offer::amount + pub fn amount_msats(&self) -> Option { + self.contents.amount_msats + } + + /// Features for paying the invoice. + pub fn features(&self) -> Option<&OfferFeatures> { + self.contents.features.as_ref() + } + + /// The quantity of the offer's item conforming to [`Offer::supported_quantity`]. + /// + /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + pub fn quantity(&self) -> Option { + self.contents.quantity + } + + /// A transient pubkey used to sign the invoice request. + pub fn payer_id(&self) -> PublicKey { + self.contents.payer_id + } + + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option<&String> { + self.contents.payer_note.as_ref() + } + + /// Signature of the invoice request using [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn signature(&self) -> Option { + self.signature + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 273650285c6..a58903f70dc 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,5 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice_request; pub mod offer; pub mod parse; +mod payer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 36db2dc23c4..f543307641d 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -223,7 +223,7 @@ impl OfferBuilder { /// An `Offer` is a potentially long-lived proposal for payment of a good or service. /// -/// An offer is a precursor to an `InvoiceRequest`. A merchant publishes an offer from which a +/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a /// customer may request an `Invoice` for a specific quantity and using an amount sufficient to /// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. /// @@ -231,6 +231,8 @@ impl OfferBuilder { /// latter. /// /// Through the use of [`BlindedPath`]s, offers provide recipient privacy. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown @@ -239,7 +241,9 @@ pub struct Offer { contents: OfferContents, } -/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. +/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub(crate) struct OfferContents { chains: Option>, @@ -262,10 +266,7 @@ impl Offer { /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) /// for the selected chain. pub fn chains(&self) -> Vec { - self.contents.chains - .as_ref() - .cloned() - .unwrap_or_else(|| vec![self.contents.implied_chain()]) + self.contents.chains() } // TODO: Link to corresponding method in `InvoiceRequest`. @@ -345,6 +346,10 @@ impl AsRef<[u8]> for Offer { } impl OfferContents { + pub fn chains(&self) -> Vec { + self.chains.as_ref().cloned().unwrap_or_else(|| vec![self.implied_chain()]) + } + pub fn implied_chain(&self) -> ChainHash { ChainHash::using_genesis_block(Network::Bitcoin) } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs new file mode 100644 index 00000000000..a36a3841754 --- /dev/null +++ b/lightning/src/offers/payer.rs @@ -0,0 +1,19 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice_request_metadata` records. + +use crate::prelude::*; + +/// An unpredictable sequence of bytes typically containing information needed to derive +/// [`InvoiceRequestContents::payer_id`]. +/// +/// [`InvoiceRequestContents::payer_id`]: invoice_request::InvoiceRequestContents::payer_id +#[derive(Clone, Debug)] +pub(crate) struct PayerContents(pub Option>); From 2e467007aebee7febd193c63d1549392750c16bd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 24 Jun 2022 16:18:29 -0500 Subject: [PATCH 06/12] Schnorr Signature serialization BOLT 12 uses Schnorr signatures for signing offers messages, which need to be serialized. --- lightning/src/util/ser.rs | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 04dc04e55b0..fbb04aa8c50 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -20,8 +20,9 @@ use core::convert::TryFrom; use core::ops::Deref; use bitcoin::secp256k1::{PublicKey, SecretKey}; -use bitcoin::secp256k1::constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, COMPACT_SIGNATURE_SIZE}; -use bitcoin::secp256k1::ecdsa::Signature; +use bitcoin::secp256k1::constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, COMPACT_SIGNATURE_SIZE, SCHNORR_SIGNATURE_SIZE}; +use bitcoin::secp256k1::ecdsa; +use bitcoin::secp256k1::schnorr; use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; @@ -498,7 +499,7 @@ impl_array!(12); // for OnionV2 impl_array!(16); // for IPv6 impl_array!(32); // for channel id & hmac impl_array!(PUBLIC_KEY_SIZE); // for PublicKey -impl_array!(COMPACT_SIGNATURE_SIZE); // for Signature +impl_array!(64); // for ecdsa::Signature and schnorr::Signature impl_array!(1300); // for OnionPacket.hop_data impl Writeable for [u16; 8] { @@ -663,7 +664,7 @@ impl Readable for Vec { Ok(ret) } } -impl Writeable for Vec { +impl Writeable for Vec { #[inline] fn write(&self, w: &mut W) -> Result<(), io::Error> { (self.len() as u16).write(w)?; @@ -674,7 +675,7 @@ impl Writeable for Vec { } } -impl Readable for Vec { +impl Readable for Vec { #[inline] fn read(r: &mut R) -> Result { let len: u16 = Readable::read(r)?; @@ -763,7 +764,7 @@ impl Readable for Sha256dHash { } } -impl Writeable for Signature { +impl Writeable for ecdsa::Signature { fn write(&self, w: &mut W) -> Result<(), io::Error> { self.serialize_compact().write(w) } @@ -773,10 +774,30 @@ impl Writeable for Signature { } } -impl Readable for Signature { +impl Readable for ecdsa::Signature { fn read(r: &mut R) -> Result { let buf: [u8; COMPACT_SIGNATURE_SIZE] = Readable::read(r)?; - match Signature::from_compact(&buf) { + match ecdsa::Signature::from_compact(&buf) { + Ok(sig) => Ok(sig), + Err(_) => return Err(DecodeError::InvalidValue), + } + } +} + +impl Writeable for schnorr::Signature { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.as_ref().write(w) + } + #[inline] + fn serialized_length(&self) -> usize { + SCHNORR_SIGNATURE_SIZE + } +} + +impl Readable for schnorr::Signature { + fn read(r: &mut R) -> Result { + let buf: [u8; SCHNORR_SIGNATURE_SIZE] = Readable::read(r)?; + match schnorr::Signature::from_slice(&buf) { Ok(sig) => Ok(sig), Err(_) => return Err(DecodeError::InvalidValue), } From 0f60c81ec13fb12748c4421f10c178449727cc4f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 9 Aug 2022 17:37:02 -0500 Subject: [PATCH 07/12] Merkle root hash computation Offers uses a merkle root hash construction for signature calculation and verification. Add a submodule implementing this so that it can be used when parsing and signing invoice_request and invoice messages. --- lightning/src/offers/merkle.rs | 139 +++++++++++++++++++++++++++++++++ lightning/src/offers/mod.rs | 1 + 2 files changed, 140 insertions(+) create mode 100644 lightning/src/offers/merkle.rs diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs new file mode 100644 index 00000000000..c16d1f0d2f2 --- /dev/null +++ b/lightning/src/offers/merkle.rs @@ -0,0 +1,139 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Tagged hashes for use in signature calculation and verification. + +use bitcoin::hashes::{Hash, HashEngine, sha256}; +use crate::io; +use crate::util::ser::{BigSize, Readable}; + +use crate::prelude::*; + +/// Valid type range for signature TLV records. +const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; + +/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream +/// containing at least one TLV record. +fn root_hash(data: &[u8]) -> sha256::Hash { + let mut tlv_stream = TlvStream::new(&data[..]).peekable(); + let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({ + let mut engine = sha256::Hash::engine(); + engine.input("LnNonce".as_bytes()); + engine.input(tlv_stream.peek().unwrap().type_bytes); + engine + })); + let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes())); + let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); + + let mut leaves = Vec::new(); + for record in tlv_stream { + if !SIGNATURE_TYPES.contains(&record.r#type.0) { + leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record)); + leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); + } + } + + // Calculate the merkle root hash in place. + let num_leaves = leaves.len(); + for level in 0.. { + let step = 2 << level; + let offset = step / 2; + if offset >= num_leaves { + break; + } + + for (i, j) in (0..num_leaves).step_by(step).zip((offset..num_leaves).step_by(step)) { + leaves[i] = tagged_branch_hash_from_engine(branch_tag.clone(), leaves[i], leaves[j]); + } + } + + *leaves.first().unwrap() +} + +fn tagged_hash>(tag: sha256::Hash, msg: T) -> sha256::Hash { + let engine = tagged_hash_engine(tag); + tagged_hash_from_engine(engine, msg) +} + +fn tagged_hash_engine(tag: sha256::Hash) -> sha256::HashEngine { + let mut engine = sha256::Hash::engine(); + engine.input(tag.as_ref()); + engine.input(tag.as_ref()); + engine +} + +fn tagged_hash_from_engine>(mut engine: sha256::HashEngine, msg: T) -> sha256::Hash { + engine.input(msg.as_ref()); + sha256::Hash::from_engine(engine) +} + +fn tagged_branch_hash_from_engine( + mut engine: sha256::HashEngine, leaf1: sha256::Hash, leaf2: sha256::Hash, +) -> sha256::Hash { + if leaf1 < leaf2 { + engine.input(leaf1.as_ref()); + engine.input(leaf2.as_ref()); + } else { + engine.input(leaf2.as_ref()); + engine.input(leaf1.as_ref()); + }; + sha256::Hash::from_engine(engine) +} + +/// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a +/// well-formed TLV stream. +struct TlvStream<'a> { + data: io::Cursor<&'a [u8]>, +} + +impl<'a> TlvStream<'a> { + fn new(data: &'a [u8]) -> Self { + Self { + data: io::Cursor::new(data), + } + } +} + +/// A slice into a [`TlvStream`] for a record. +struct TlvRecord<'a> { + r#type: BigSize, + type_bytes: &'a [u8], + data: &'a [u8], +} + +impl AsRef<[u8]> for TlvRecord<'_> { + fn as_ref(&self) -> &[u8] { &self.data } +} + +impl<'a> Iterator for TlvStream<'a> { + type Item = TlvRecord<'a>; + + fn next(&mut self) -> Option { + if self.data.position() < self.data.get_ref().len() as u64 { + let start = self.data.position(); + + let r#type: BigSize = Readable::read(&mut self.data).unwrap(); + let offset = self.data.position(); + let type_bytes = &self.data.get_ref()[start as usize..offset as usize]; + + let length: BigSize = Readable::read(&mut self.data).unwrap(); + let offset = self.data.position(); + let end = offset + length.0; + + let _value = &self.data.get_ref()[offset as usize..end as usize]; + let data = &self.data.get_ref()[start as usize..end as usize]; + + self.data.set_position(end); + + Some(TlvRecord { r#type, type_bytes, data }) + } else { + None + } + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index a58903f70dc..be0eb2da522 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -13,6 +13,7 @@ //! Offers are a flexible protocol for Lightning payments. pub mod invoice_request; +mod merkle; pub mod offer; pub mod parse; mod payer; From 221a2f311806added3b28420772b81f238fc8b0b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Aug 2022 17:31:46 -0500 Subject: [PATCH 08/12] Invoice request raw byte encoding and decoding When reading an offer, an `invoice_request` message is sent over the wire. Implement Writeable for encoding the message and TryFrom for decoding it by defining in terms of TLV streams. These streams represent content for the payer metadata (0), reflected `offer` (1-79), `invoice_request` (80-159), and signature (240). --- lightning/src/offers/invoice_request.rs | 137 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 19 ++++ lightning/src/offers/offer.rs | 63 ++++++++++- lightning/src/offers/parse.rs | 21 ++++ lightning/src/offers/payer.rs | 6 ++ lightning/src/util/ser.rs | 18 ++++ lightning/src/util/ser_macros.rs | 2 +- 7 files changed, 260 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 92329ac136a..cb3c6325d90 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -12,9 +12,15 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::secp256k1::PublicKey; use bitcoin::secp256k1::schnorr::Signature; +use core::convert::TryFrom; +use crate::io; use crate::ln::features::OfferFeatures; -use crate::offers::offer::OfferContents; -use crate::offers::payer::PayerContents; +use crate::ln::msgs::DecodeError; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::{Amount, OfferContents, OfferTlvStream}; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::{PayerContents, PayerTlvStream}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::prelude::*; @@ -74,9 +80,9 @@ impl InvoiceRequest { self.contents.features.as_ref() } - /// The quantity of the offer's item conforming to [`Offer::supported_quantity`]. + /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. /// - /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + /// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity pub fn quantity(&self) -> Option { self.contents.quantity } @@ -98,3 +104,126 @@ impl InvoiceRequest { self.signature } } + +impl Writeable for InvoiceRequest { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for InvoiceRequest { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice_request = ParsedMessage::::try_from(bytes)?; + InvoiceRequest::try_from(parsed_invoice_request) + } +} + +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { + (80, chain: ChainHash), + (82, amount: (u64, HighZeroBytesDroppedBigSize)), + (84, features: OfferFeatures), + (86, quantity: (u64, HighZeroBytesDroppedBigSize)), + (88, payer_id: PublicKey), + (89, payer_note: (String, WithoutLength)), +}); + +type FullInvoiceRequestTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceRequestTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, signature)) + } +} + +type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); + +impl TryFrom> for InvoiceRequest { + type Error = ParseError; + + fn try_from(invoice_request: ParsedMessage) + -> Result + { + let ParsedMessage {bytes, tlv_stream } = invoice_request; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + if let Some(signature) = &signature { + let tag = concat!("lightning", "invoice_request", "signature"); + merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?; + } + + Ok(InvoiceRequest { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceRequestContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result { + let ( + PayerTlvStream { metadata }, + offer_tlv_stream, + InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note }, + ) = tlv_stream; + + let payer = PayerContents(metadata); + let offer = OfferContents::try_from(offer_tlv_stream)?; + + if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) { + return Err(SemanticError::UnsupportedChain); + } + + let amount_msats = match (offer.amount(), amount) { + (Some(_), None) => return Err(SemanticError::MissingAmount), + (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency), + (_, amount_msats) => amount_msats, + }; + + if let Some(features) = &features { + if features.requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + } + + let expects_quantity = offer.expects_quantity(); + let quantity = match quantity { + None if expects_quantity => return Err(SemanticError::MissingQuantity), + Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity), + Some(quantity) if !offer.is_valid_quantity(quantity) => { + return Err(SemanticError::InvalidQuantity); + } + quantity => quantity, + }; + + { + let amount_msats = amount_msats.unwrap_or(offer.amount_msats()); + let quantity = quantity.unwrap_or(1); + if amount_msats < offer.expected_invoice_amount_msats(quantity) { + return Err(SemanticError::InsufficientAmount); + } + } + + + let payer_id = match payer_id { + None => return Err(SemanticError::MissingPayerId), + Some(payer_id) => payer_id, + }; + + Ok(InvoiceRequestContents { + payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note, + }) + } +} diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index c16d1f0d2f2..a4ad2fed74c 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -10,6 +10,8 @@ //! Tagged hashes for use in signature calculation and verification. use bitcoin::hashes::{Hash, HashEngine, sha256}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::schnorr::Signature; use crate::io; use crate::util::ser::{BigSize, Readable}; @@ -18,6 +20,23 @@ use crate::prelude::*; /// Valid type range for signature TLV records. const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; +tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { + (240, signature: Signature), +}); + +/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message +/// digest. +pub(super) fn verify_signature( + signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result<(), secp256k1::Error> { + let tag = sha256::Hash::hash(tag.as_bytes()); + let merkle_root = root_hash(bytes); + let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap(); + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(signature, &digest, &pubkey) +} + /// Computes a merkle root hash for the given data, which must be a well-formed TLV stream /// containing at least one TLV record. fn root_hash(data: &[u8]) -> sha256::Hash { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index f543307641d..7bd738a4bf8 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -269,6 +269,11 @@ impl Offer { self.contents.chains() } + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.contents.supports_chain(chain) + } + // TODO: Link to corresponding method in `InvoiceRequest`. /// Opaque bytes set by the originator. Useful for authentication and validating fields since it /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. @@ -278,7 +283,7 @@ impl Offer { /// The minimum amount required for a successful payment of a single item. pub fn amount(&self) -> Option<&Amount> { - self.contents.amount.as_ref() + self.contents.amount() } /// A complete description of the purpose of the payment. Intended to be displayed to the user @@ -328,6 +333,18 @@ impl Offer { self.contents.supported_quantity() } + /// Returns whether the given quantity is valid for the offer. + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + self.contents.is_valid_quantity(quantity) + } + + /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub fn expects_quantity(&self) -> bool { + self.contents.expects_quantity() + } + /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { self.contents.signing_pubkey.unwrap() @@ -354,10 +371,48 @@ impl OfferContents { ChainHash::using_genesis_block(Network::Bitcoin) } + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.chains().contains(&chain) + } + + pub fn amount(&self) -> Option<&Amount> { + self.amount.as_ref() + } + + pub fn amount_msats(&self) -> u64 { + match self.amount() { + None => 0, + Some(&Amount::Bitcoin { amount_msats }) => amount_msats, + Some(&Amount::Currency { .. }) => unreachable!(), + } + } + + pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { + self.amount_msats() * quantity + } + pub fn supported_quantity(&self) -> Quantity { self.supported_quantity } + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => { + let n = n.get(); + if n == 1 { false } + else { quantity > 0 && quantity <= n } + }, + Quantity::Unbounded => quantity > 0, + } + } + + pub fn expects_quantity(&self) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => n.get() != 1, + Quantity::Unbounded => true, + } + } + fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), @@ -587,6 +642,7 @@ mod tests { assert_eq!(offer.bytes, buffer.as_slice()); assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); assert_eq!(offer.description(), PrintableString("foo")); @@ -625,6 +681,7 @@ mod tests { .chain(Network::Bitcoin) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().chains, None); @@ -632,6 +689,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -640,6 +698,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -648,6 +707,8 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![mainnet, testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet])); } diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 4aa973b4c3e..722471e0a74 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -11,6 +11,7 @@ use bitcoin::bech32; use bitcoin::bech32::{FromBase32, ToBase32}; +use bitcoin::secp256k1; use core::convert::TryFrom; use core::fmt; use crate::io; @@ -92,25 +93,39 @@ pub enum ParseError { Decode(DecodeError), /// The parsed message has invalid semantics. InvalidSemantics(SemanticError), + /// The parsed message has an invalid signature. + InvalidSignature(secp256k1::Error), } /// Error when interpreting a TLV stream as a specific type. #[derive(Debug, PartialEq)] pub enum SemanticError { + /// The provided chain hash does not correspond to a supported chain. + UnsupportedChain, /// An amount was expected but was missing. MissingAmount, /// An amount exceeded the maximum number of bitcoin. InvalidAmount, + /// An amount was provided but was not sufficient in value. + InsufficientAmount, /// A currency was provided that is not supported. UnsupportedCurrency, + /// A feature was required but is unknown. + UnknownRequiredFeatures, /// A required description was not provided. MissingDescription, /// A node id was not provided. MissingNodeId, /// An empty set of blinded paths was provided. MissingPaths, + /// A quantity was not provided. + MissingQuantity, /// An unsupported quantity was provided. InvalidQuantity, + /// A quantity or quantity bounds was provided but was not expected. + UnexpectedQuantity, + /// A payer id was expected but was missing. + MissingPayerId, } impl From for ParseError { @@ -130,3 +145,9 @@ impl From for ParseError { Self::InvalidSemantics(error) } } + +impl From for ParseError { + fn from(error: secp256k1::Error) -> Self { + Self::InvalidSignature(error) + } +} diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index a36a3841754..a8ff43bb6e2 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -9,6 +9,8 @@ //! Data structures and encoding for `invoice_request_metadata` records. +use crate::util::ser::WithoutLength; + use crate::prelude::*; /// An unpredictable sequence of bytes typically containing information needed to derive @@ -17,3 +19,7 @@ use crate::prelude::*; /// [`InvoiceRequestContents::payer_id`]: invoice_request::InvoiceRequestContents::payer_id #[derive(Clone, Debug)] pub(crate) struct PayerContents(pub Option>); + +tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { + (0, metadata: (Vec, WithoutLength)), +}); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index fbb04aa8c50..2156581df6e 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1012,6 +1012,24 @@ impl Writeable for (A, B, C) { } } +impl Readable for (A, B, C, D) { + fn read(r: &mut R) -> Result { + let a: A = Readable::read(r)?; + let b: B = Readable::read(r)?; + let c: C = Readable::read(r)?; + let d: D = Readable::read(r)?; + Ok((a, b, c, d)) + } +} +impl Writeable for (A, B, C, D) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w)?; + self.1.write(w)?; + self.2.write(w)?; + self.3.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 4e3198006a7..31978d5e4c7 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -495,7 +495,7 @@ macro_rules! tlv_stream { #[derive(Debug)] pub(crate) struct $name { $( - $field: Option, + pub(crate) $field: Option, )* } From 3685b4740087aa7805faec6bcee9e0212d898c27 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 31 Aug 2022 10:19:44 -0500 Subject: [PATCH 09/12] WIP: InvoiceRequestBuilder --- lightning/src/offers/invoice_request.rs | 290 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 26 ++- lightning/src/offers/offer.rs | 22 +- 3 files changed, 320 insertions(+), 18 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index cb3c6325d90..90b60665264 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -8,22 +8,220 @@ // licenses. //! Data structures and encoding for `invoice_request` messages. +//! +//! An [`InvoiceRequest`] can be either built from a parsed [`Offer`] as an "offer to be paid" or +//! built directly as an "offer for money" (e.g., refund, ATM withdrawal). In the former case, it is +//! typically constructed by a customer and sent to the merchant who had published the corresponding +//! offer. In the latter case, an offer doesn't exist as a precursor to the request. Rather the +//! merchant would typically construct the invoice request and presents it to the customer. +//! +//! The recipient of the request responds with an `Invoice`. +//! ``` +//! extern crate bitcoin; +//! extern crate lightning; +//! +//! use bitcoin::network::constants::Network; +//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use lightning::ln::features::OfferFeatures; +//! use lightning::offers::offer::Offer; +//! use lightning::util::ser::Writeable; +//! +//! # fn parse() -> Result<(), lightning::offers::parse::ParseError> { +//! let secp_ctx = Secp256k1::new(); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let pubkey = PublicKey::from(keys); +//! let mut buffer = Vec::new(); +//! +//! // "offer to be paid" flow +//! "lno1qcp4256ypq" +//! .parse::()? +//! .request_invoice(pubkey) +//! .metadata(vec![42; 64]) +//! .chain(Network::Testnet)? +//! .amount_msats(1000) +//! .quantity(5)? +//! .payer_note("foo".to_string()) +//! .build()? +//! .sign(|digest| secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))? +//! .write(&mut buffer) +//! .unwrap(); +//! # Ok(()) +//! # } +//! ``` use bitcoin::blockdata::constants::ChainHash; -use bitcoin::secp256k1::PublicKey; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::{Message, PublicKey, self}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::DecodeError; -use crate::offers::merkle::{SignatureTlvStream, self}; -use crate::offers::offer::{Amount, OfferContents, OfferTlvStream}; +use crate::offers::merkle::{SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::{PayerContents, PayerTlvStream}; +use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::prelude::*; +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); + +/// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [module-level documentation]: self +pub struct InvoiceRequestBuilder<'a> { + offer: &'a Offer, + invoice_request: InvoiceRequestContents, +} + +impl<'a> InvoiceRequestBuilder<'a> { + pub(super) fn new(offer: &'a Offer, payer_id: PublicKey) -> Self { + Self { + offer, + invoice_request: InvoiceRequestContents { + payer: PayerContents(None), offer: offer.contents.clone(), chain: None, + amount_msats: None, features: None, quantity: None, payer_id, payer_note: None, + }, + } + } + + /// Sets the metadata for the invoice request. Useful for containing information about the + /// derivation of [`InvoiceRequest::payer_id`]. This should not leak any information such as + /// using a simple BIP-32 derivation path. + /// + /// Successive calls to this method will override the previous setting. + pub fn metadata(mut self, metadata: Vec) -> Self { + self.invoice_request.payer = PayerContents(Some(metadata)); + self + } + + /// Sets the chain hash of the given [`Network`] for paying an invoice. If not called, + /// [`Network::Bitcoin`] is assumed. Must be supported by the offer. + /// + /// Successive calls to this method will override the previous setting. + pub fn chain(mut self, network: Network) -> Result { + let chain = ChainHash::using_genesis_block(network); + if !self.offer.supports_chain(chain) { + return Err(SemanticError::UnsupportedChain) + } + + self.invoice_request.chain = Some(chain); + Ok(self) + } + + /// Sets the amount for paying an invoice. Must be at least the base invoice amount (i.e., + /// [`Offer::amount`] times [`quantity`]). + /// + /// Successive calls to this method will override the previous setting. + /// + /// [`quantity`]: Self::quantity + pub fn amount_msats(mut self, amount_msats: u64) -> Self { + self.invoice_request.amount_msats = Some(amount_msats); + self + } + + /// Sets the features for the invoice request. + /// + /// Successive calls to this method will override the previous setting. + #[cfg(test)] + pub fn features(mut self, features: OfferFeatures) -> Self { + self.invoice_request.features = Some(features); + self + } + + /// Sets a quantity of items for the invoice request. If not set, `1` is assumed. Must conform + /// to [`Offer::is_valid_quantity`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn quantity(mut self, quantity: u64) -> Result { + if !self.offer.is_valid_quantity(quantity) { + return Err(SemanticError::InvalidQuantity); + } + + self.invoice_request.quantity = Some(quantity); + Ok(self) + } + + /// Sets a note for the invoice request. + /// + /// Successive calls to this method will override the previous setting. + pub fn payer_note(mut self, payer_note: String) -> Self { + self.invoice_request.payer_note = Some(payer_note); + self + } + + /// Builds an [`InvoiceRequest`] after checking for valid semantics. + pub fn build(self) -> Result, SemanticError> { + if !self.offer.supports_chain(self.invoice_request.chain()) { + return Err(SemanticError::UnsupportedChain); + } + + if let Some(amount) = self.offer.amount() { + if self.invoice_request.amount_msats.is_none() { + return Err(SemanticError::MissingAmount); + } + + if let Amount::Currency { .. } = amount { + return Err(SemanticError::UnsupportedCurrency); + } + } + + if self.offer.expects_quantity() && self.invoice_request.quantity.is_none() { + return Err(SemanticError::InvalidQuantity); + } + + let amount_msats = self.invoice_request.amount_msats.unwrap_or(self.offer.amount_msats()); + let quantity = self.invoice_request.quantity.unwrap_or(1); + if amount_msats < self.offer.expected_invoice_amount_msats(quantity) { + return Err(SemanticError::InsufficientAmount); + } + + let InvoiceRequestBuilder { offer, invoice_request } = self; + Ok(UnsignedInvoiceRequest { offer, invoice_request }) + } +} + +/// A semantically valid [`InvoiceRequest`] that hasn't been signed. +pub struct UnsignedInvoiceRequest<'a> { + offer: &'a Offer, + invoice_request: InvoiceRequestContents, +} + +impl<'a> UnsignedInvoiceRequest<'a> { + /// Signs the invoice request using the given function. + pub fn sign(self, sign: F) -> Result + where F: FnOnce(&Message) -> Signature + { + // Use the offer bytes instead of the offer TLV stream as the offer may have contained + // unknown TLV records, which are not stored in `OfferContents`. + let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = + self.invoice_request.as_tlv_stream(); + let offer_bytes = WithoutLength(&self.offer.bytes); + let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); + + let mut bytes = Vec::new(); + unsigned_tlv_stream.write(&mut bytes).unwrap(); + + let pubkey = self.invoice_request.payer_id; + let signature = Some(merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?); + + // Append the signature TLV record to the bytes. + let signature_tlv_stream = SignatureTlvStreamRef { + signature: signature.as_ref(), + }; + signature_tlv_stream.write(&mut bytes).unwrap(); + + Ok(InvoiceRequest { + bytes, + contents: self.invoice_request, + signature, + }) + } +} + /// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. /// /// An offer may provided choices such as quantity, amount, chain, features, etc. An invoice request @@ -60,17 +258,14 @@ impl InvoiceRequest { } /// A chain from [`Offer::chains`] that the offer is valid for. - /// - /// [`Offer::chains`]: crate::offers::offer::Offer::chains pub fn chain(&self) -> ChainHash { - self.contents.chain.unwrap_or_else(|| self.contents.offer.implied_chain()) + self.contents.chain() } /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which /// must be greater than or equal to [`Offer::amount`], converted if necessary. /// /// [`chain`]: Self::chain - /// [`Offer::amount`]: crate::offers::offer::Offer::amount pub fn amount_msats(&self) -> Option { self.contents.amount_msats } @@ -81,8 +276,6 @@ impl InvoiceRequest { } /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. - /// - /// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity pub fn quantity(&self) -> Option { self.contents.quantity } @@ -105,12 +298,43 @@ impl InvoiceRequest { } } +impl InvoiceRequestContents { + fn chain(&self) -> ChainHash { + self.chain.unwrap_or_else(|| self.offer.implied_chain()) + } + + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { + let payer = PayerTlvStreamRef { + metadata: self.payer.0.as_ref(), + }; + + let offer = self.offer.as_tlv_stream(); + + let invoice_request = InvoiceRequestTlvStreamRef { + chain: self.chain.as_ref(), + amount: self.amount_msats, + features: self.features.as_ref(), + quantity: self.quantity, + payer_id: Some(&self.payer_id), + payer_note: self.payer_note.as_ref(), + }; + + (payer, offer, invoice_request) + } +} + impl Writeable for InvoiceRequest { fn write(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) } } +impl Writeable for InvoiceRequestContents { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.as_tlv_stream().write(writer) + } +} + impl TryFrom> for InvoiceRequest { type Error = ParseError; @@ -145,6 +369,12 @@ impl SeekReadable for FullInvoiceRequestTlvStream { type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); +type PartialInvoiceRequestTlvStreamRef<'a> = ( + PayerTlvStreamRef<'a>, + OfferTlvStreamRef<'a>, + InvoiceRequestTlvStreamRef<'a>, +); + impl TryFrom> for InvoiceRequest { type Error = ParseError; @@ -161,8 +391,7 @@ impl TryFrom> for InvoiceRequest { )?; if let Some(signature) = &signature { - let tag = concat!("lightning", "invoice_request", "signature"); - merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?; + merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, contents.payer_id)?; } Ok(InvoiceRequest { bytes, contents, signature }) @@ -227,3 +456,40 @@ impl TryFrom for InvoiceRequestContents { }) } } + +#[cfg(test)] +mod tests { + use super::InvoiceRequest; + + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; + use core::convert::TryFrom; + use crate::ln::msgs::DecodeError; + use crate::offers::offer::OfferBuilder; + use crate::offers::parse::ParseError; + use crate::util::ser::{BigSize, Writeable}; + + #[test] + fn fails_parsing_invoice_request_with_extra_tlv_records() { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let invoice_request = OfferBuilder::new("foo".into(), keys.public_key()) + .build() + .unwrap() + .request_invoice(keys.public_key()) + .build() + .unwrap() + .sign(|digest| secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + BigSize(1002).write(&mut encoded_invoice_request).unwrap(); + BigSize(32).write(&mut encoded_invoice_request).unwrap(); + [42u8; 32].write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } +} diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index a4ad2fed74c..1b38d60bc2d 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -24,19 +24,39 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { (240, signature: Signature), }); +pub(super) fn sign_message( + sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result +where + F: FnOnce(&Message) -> Signature +{ + let digest = message_digest(tag, bytes); + let signature = sign(&digest); + + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(&signature, &digest, &pubkey)?; + + Ok(signature) +} + /// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message /// digest. pub(super) fn verify_signature( signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey, ) -> Result<(), secp256k1::Error> { - let tag = sha256::Hash::hash(tag.as_bytes()); - let merkle_root = root_hash(bytes); - let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap(); + let digest = message_digest(tag, bytes); let pubkey = pubkey.into(); let secp_ctx = Secp256k1::verification_only(); secp_ctx.verify_schnorr(signature, &digest, &pubkey) } +fn message_digest(tag: &str, bytes: &[u8]) -> Message { + let tag = sha256::Hash::hash(tag.as_bytes()); + let merkle_root = root_hash(bytes); + Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap() +} + /// Computes a merkle root hash for the given data, which must be a well-formed TLV stream /// containing at least one TLV record. fn root_hash(data: &[u8]) -> sha256::Hash { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 7bd738a4bf8..9ee85fa7352 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -76,6 +76,7 @@ use core::time::Duration; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; +use crate::offers::invoice_request::InvoiceRequestBuilder; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; @@ -237,8 +238,8 @@ impl OfferBuilder { pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. - bytes: Vec, - contents: OfferContents, + pub(super) bytes: Vec, + pub(super) contents: OfferContents, } /// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`. @@ -286,6 +287,16 @@ impl Offer { self.contents.amount() } + /// The minimum amount in msats required for a successful payment. + pub fn amount_msats(&self) -> u64 { + self.contents.amount_msats() + } + + /// Returns the minimum amount in msats required for the given quantity. + pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { + self.contents.expected_invoice_amount_msats(quantity) + } + /// A complete description of the purpose of the payment. Intended to be displayed to the user /// but with the caveat that it has not been verified in any way. pub fn description(&self) -> PrintableString { @@ -350,6 +361,11 @@ impl Offer { self.contents.signing_pubkey.unwrap() } + /// + pub fn request_invoice(&self, payer_id: PublicKey) -> InvoiceRequestBuilder { + InvoiceRequestBuilder::new(self, payer_id) + } + #[cfg(test)] fn as_tlv_stream(&self) -> OfferTlvStreamRef { self.contents.as_tlv_stream() @@ -413,7 +429,7 @@ impl OfferContents { } } - fn as_tlv_stream(&self) -> OfferTlvStreamRef { + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)), From 2cb1ef7d4153d927a99bfad0333ac8f3d472729a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 21 Sep 2022 09:32:23 -0500 Subject: [PATCH 10/12] WIP: send invoice request encoding --- lightning/src/offers/invoice_request.rs | 7 +++- lightning/src/offers/offer.rs | 51 +++++++++++++++++++++++++ lightning/src/offers/parse.rs | 8 +++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 90b60665264..11a2adbfb9b 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -58,7 +58,7 @@ use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::DecodeError; use crate::offers::merkle::{SignatureTlvStream, SignatureTlvStreamRef, self}; -use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, SendInvoiceOfferContents}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; @@ -409,7 +409,10 @@ impl TryFrom for InvoiceRequestContents { ) = tlv_stream; let payer = PayerContents(metadata); - let offer = OfferContents::try_from(offer_tlv_stream)?; + let offer = match offer_tlv_stream.node_id { + Some(_) => OfferContents::try_from(offer_tlv_stream)?, + None => SendInvoiceOfferContents::try_from(offer_tlv_stream)?.0, + }; if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) { return Err(SemanticError::UnsupportedChain); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 9ee85fa7352..efe89fa5760 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -617,6 +617,57 @@ impl TryFrom for OfferContents { } } +/// [`OfferContents`] used with a "send invoice" offer (i.e., a published [`InvoiceRequest`]). +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub(super) struct SendInvoiceOfferContents(pub OfferContents); + +impl TryFrom for SendInvoiceOfferContents { + type Error = SemanticError; + + fn try_from(tlv_stream: OfferTlvStream) -> Result { + let OfferTlvStream { + chains, metadata, currency, amount, description, features, absolute_expiry, paths, + issuer, quantity_max, node_id, + } = tlv_stream; + assert!(node_id.is_none()); + + if chains.is_some() { + return Err(SemanticError::UnexpectedChain); + } + + if currency.is_some() || amount.is_some() { + return Err(SemanticError::UnexpectedAmount); + } + + let description = match description { + None => return Err(SemanticError::MissingDescription), + Some(description) => description, + }; + + let features = match features { + None => OfferFeatures::empty(), + Some(_) => return Err(SemanticError::UnexpectedFeatures), + }; + + let absolute_expiry = absolute_expiry.map(Duration::from_secs); + + let paths = match paths { + Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths), + paths => paths, + }; + + if quantity_max.is_some() { + return Err(SemanticError::UnexpectedQuantity); + } + + Ok(SendInvoiceOfferContents(OfferContents { + chains: None, metadata, amount: None, description, features, absolute_expiry, issuer, + paths, supported_quantity: Quantity::one(), signing_pubkey: None, + })) + } +} + impl core::fmt::Display for Offer { fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { self.fmt_bech32_str(f) diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 722471e0a74..6ffdfceb342 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -102,16 +102,22 @@ pub enum ParseError { pub enum SemanticError { /// The provided chain hash does not correspond to a supported chain. UnsupportedChain, - /// An amount was expected but was missing. + /// A chain was provided but was not expected. + UnexpectedChain, + /// An amount was not provided. MissingAmount, /// An amount exceeded the maximum number of bitcoin. InvalidAmount, /// An amount was provided but was not sufficient in value. InsufficientAmount, + /// An amount was provided but was not expected. + UnexpectedAmount, /// A currency was provided that is not supported. UnsupportedCurrency, /// A feature was required but is unknown. UnknownRequiredFeatures, + /// Features were provided but were not expected. + UnexpectedFeatures, /// A required description was not provided. MissingDescription, /// A node id was not provided. From b8601f495d591a66204d2ac95820cee88472481a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Aug 2022 17:31:46 -0500 Subject: [PATCH 11/12] Invoice request parsing from bech32 strings Implement Bech32Encode for InvoiceRequest, which supports creating and parsing QR codes for the merchant-pays-user (e.g., refund, ATM) flow. --- lightning/src/offers/invoice_request.rs | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 11a2adbfb9b..cfd9b3dbef2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -54,12 +54,13 @@ use bitcoin::network::constants::Network; use bitcoin::secp256k1::{Message, PublicKey, self}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; +use core::str::FromStr; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::DecodeError; use crate::offers::merkle::{SignatureTlvStream, SignatureTlvStreamRef, self}; use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, SendInvoiceOfferContents}; -use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; @@ -323,6 +324,12 @@ impl InvoiceRequestContents { } } +impl AsRef<[u8]> for InvoiceRequest { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + impl Writeable for InvoiceRequest { fn write(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) @@ -353,6 +360,10 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { (89, payer_note: (String, WithoutLength)), }); +impl Bech32Encode for InvoiceRequest { + const BECH32_HRP: &'static str = "lnr"; +} + type FullInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); @@ -375,6 +386,14 @@ type PartialInvoiceRequestTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, ); +impl FromStr for InvoiceRequest { + type Err = ParseError; + + fn from_str(s: &str) -> Result::Err> { + InvoiceRequest::from_bech32_str(s) + } +} + impl TryFrom> for InvoiceRequest { type Error = ParseError; @@ -460,6 +479,12 @@ impl TryFrom for InvoiceRequestContents { } } +impl core::fmt::Display for InvoiceRequest { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + #[cfg(test)] mod tests { use super::InvoiceRequest; From cd8cafb3d19d1b9dd5685de0494864422ce9185a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 12 Sep 2022 09:30:06 -0500 Subject: [PATCH 12/12] WIP: Invoice encoding --- lightning/src/offers/invoice.rs | 241 ++++++++++++++++++++++++ lightning/src/offers/invoice_request.rs | 17 +- lightning/src/offers/mod.rs | 1 + lightning/src/offers/offer.rs | 6 +- lightning/src/offers/parse.rs | 6 + lightning/src/util/ser.rs | 37 ++++ 6 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 lightning/src/offers/invoice.rs diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs new file mode 100644 index 00000000000..62ef8ca7476 --- /dev/null +++ b/lightning/src/offers/invoice.rs @@ -0,0 +1,241 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Data structures and encoding for `invoice_request` messages. + +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::util::address::{Address, Payload, WitnessVersion}; +use core::convert::TryFrom; +use core::time::Duration; +use crate::io; +use crate::ln::PaymentHash; +use crate::ln::features::OfferFeatures; +use crate::ln::msgs::DecodeError; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::OfferTlvStream; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::PayerTlvStream; +use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream}; +use crate::onion_message::BlindedPath; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; + +use crate::prelude::*; + +/// +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); + +/// +pub struct Invoice { + bytes: Vec, + contents: InvoiceContents, + signature: Option, +} + +/// +pub(crate) struct InvoiceContents { + invoice_request: InvoiceRequestContents, + paths: Option>, + payinfo: Option>, + created_at: Duration, + relative_expiry: Option, + payment_hash: PaymentHash, + amount_msats: u64, + fallbacks: Option>, + features: Option, + code: Option, +} + +impl Invoice { + /// + pub fn fallbacks(&self) -> Vec<&Address> { + let is_valid = |address: &&Address| { + if let Address { payload: Payload::WitnessProgram { program, .. }, .. } = address { + if address.is_standard() { + return true; + } else if program.len() < 2 || program.len() > 40 { + return false; + } else { + return true; + } + } + + unreachable!() + }; + self.contents.fallbacks + .as_ref() + .map(|fallbacks| fallbacks.iter().filter(is_valid).collect()) + .unwrap_or_else(Vec::new) + } +} + +impl Writeable for Invoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice = ParsedMessage::::try_from(bytes)?; + Invoice::try_from(parsed_invoice) + } +} + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { + (160, paths: (Vec, WithoutLength)), + (162, payinfo: (Vec, WithoutLength)), + (164, created_at: (u64, HighZeroBytesDroppedBigSize)), + (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), + (168, payment_hash: PaymentHash), + (170, amount: (u64, HighZeroBytesDroppedBigSize)), + (172, fallbacks: (Vec, WithoutLength)), + (174, features: OfferFeatures), + (176, code: (String, WithoutLength)), +}); + +/// +#[derive(Debug)] +pub struct BlindedPayInfo { + fee_base_msat: u32, + fee_proportional_millionths: u32, + cltv_expiry_delta: u16, + htlc_minimum_msat: u64, + htlc_maximum_msat: u64, + features_len: u16, + features: OfferFeatures, +} + +impl_writeable!(BlindedPayInfo, { + fee_base_msat, + fee_proportional_millionths, + cltv_expiry_delta, + htlc_minimum_msat, + htlc_maximum_msat, + features_len, + features +}); + +/// +#[derive(Debug)] +pub struct FallbackAddress { + version: WitnessVersion, + program: Vec, +} + +impl_writeable!(FallbackAddress, { version, program }); + +type FullInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let invoice = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, invoice, signature)) + } +} + +type PartialInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(invoice: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = invoice; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) + )?; + + if let Some(signature) = &signature { + let pubkey = contents.invoice_request.offer.signing_pubkey(); + merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, pubkey)?; + } + + Ok(Invoice { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result { + let ( + payer_tlv_stream, + offer_tlv_stream, + invoice_request_tlv_stream, + InvoiceTlvStream { + paths, payinfo, created_at, relative_expiry, payment_hash, amount, fallbacks, + features, code, + }, + ) = tlv_stream; + + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + let (paths, payinfo) = match (paths, payinfo) { + (None, _) => return Err(SemanticError::MissingPaths), + (_, None) => return Err(SemanticError::InvalidPayInfo), + (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths), + (Some(paths), Some(payinfo)) if paths.len() != payinfo.len() => { + return Err(SemanticError::InvalidPayInfo); + }, + (paths, payinfo) => (paths, payinfo), + }; + + let created_at = match created_at { + None => return Err(SemanticError::MissingCreationTime), + Some(timestamp) => Duration::from_secs(timestamp), + }; + + let relative_expiry = relative_expiry + .map(Into::::into) + .map(Duration::from_secs); + + let payment_hash = match payment_hash { + None => return Err(SemanticError::MissingPaymentHash), + Some(payment_hash) => payment_hash, + }; + + let amount_msats = match amount { + None => return Err(SemanticError::MissingAmount), + Some(amount) => amount, + }; + + let fallbacks = match fallbacks { + None => None, + Some(fallbacks) => { + let mut addresses = Vec::with_capacity(fallbacks.len()); + for FallbackAddress { version, program } in fallbacks { + addresses.push(Address { + payload: Payload::WitnessProgram { version, program }, + network: invoice_request.network(), + }); + } + Some(addresses) + }, + }; + + Ok(InvoiceContents { + invoice_request, paths, payinfo, created_at, relative_expiry, payment_hash, + amount_msats, fallbacks, features, code, + }) + } +} diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index cfd9b3dbef2..c26cf5e2d6c 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -240,7 +240,7 @@ pub struct InvoiceRequest { #[derive(Clone, Debug)] pub(crate) struct InvoiceRequestContents { payer: PayerContents, - offer: OfferContents, + pub(super) offer: OfferContents, chain: Option, amount_msats: Option, features: Option, @@ -304,6 +304,21 @@ impl InvoiceRequestContents { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } + pub fn network(&self) -> Network { + let chain = self.chain(); + if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Network::Bitcoin + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Network::Testnet + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Network::Signet + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Network::Regtest + } else { + unreachable!() + } + } + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_ref(), diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index be0eb2da522..e479d1e1d8e 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,6 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice; pub mod invoice_request; mod merkle; pub mod offer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index efe89fa5760..232c8f12dc7 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -358,7 +358,7 @@ impl Offer { /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey.unwrap() + self.contents.signing_pubkey() } /// @@ -429,6 +429,10 @@ impl OfferContents { } } + pub fn signing_pubkey(&self) -> PublicKey { + self.signing_pubkey.unwrap() + } + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 6ffdfceb342..73192a81435 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -132,6 +132,12 @@ pub enum SemanticError { UnexpectedQuantity, /// A payer id was expected but was missing. MissingPayerId, + /// + InvalidPayInfo, + /// + MissingCreationTime, + /// + MissingPaymentHash, } impl From for ParseError { diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 2156581df6e..f0f00ca1165 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -30,6 +30,7 @@ use bitcoin::consensus; use bitcoin::consensus::Encodable; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash}; +use bitcoin::util::address::WitnessVersion; use core::marker::Sized; use core::time::Duration; use crate::ln::msgs::DecodeError; @@ -970,6 +971,22 @@ macro_rules! impl_consensus_ser { impl_consensus_ser!(Transaction); impl_consensus_ser!(TxOut); +impl Writeable for WitnessVersion { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_num().write(w) + } +} + +impl Readable for WitnessVersion { + fn read(r: &mut R) -> Result { + let num: u8 = Readable::read(r)?; + match WitnessVersion::try_from(num) { + Ok(version) => Ok(version), + Err(_) => Err(DecodeError::InvalidValue), + } + } +} + impl Readable for Mutex { fn read(r: &mut R) -> Result { let t: T = Readable::read(r)?; @@ -1030,6 +1047,26 @@ impl Writeable for (A, B } } +impl Readable for (A, B, C, D, E) { + fn read(r: &mut R) -> Result { + let a: A = Readable::read(r)?; + let b: B = Readable::read(r)?; + let c: C = Readable::read(r)?; + let d: D = Readable::read(r)?; + let e: E = Readable::read(r)?; + Ok((a, b, c, d, e)) + } +} +impl Writeable for (A, B, C, D, E) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w)?; + self.1.write(w)?; + self.2.write(w)?; + self.3.write(w)?; + self.4.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(())