From 4b8b17d72f9928220f44820247027ec497e79062 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 2 Dec 2022 15:26:27 -0800 Subject: [PATCH 01/10] Reduce visibility for offer auxiliary types --- lightning/src/offers/offer.rs | 2 +- lightning/src/offers/parse.rs | 4 ++-- lightning/src/util/ser_macros.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 704045f760b..5995c513dab 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -242,7 +242,7 @@ pub struct Offer { /// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. #[derive(Clone, Debug)] -pub(crate) struct OfferContents { +pub(super) struct OfferContents { chains: Option>, metadata: Option>, amount: Option, diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 19d7d74ed61..01397098176 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -20,7 +20,7 @@ use crate::util::ser::SeekReadable; use crate::prelude::*; /// Indicates a message can be encoded using bech32. -pub(crate) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { +pub(super) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { /// Human readable part of the message's bech32 encoding. const BECH32_HRP: &'static str; @@ -78,7 +78,7 @@ impl<'a> AsRef for Bech32String<'a> { /// 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(super) struct ParsedMessage { pub bytes: Vec, pub tlv_stream: T, } diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 3e1d8a9280d..6cc9d947753 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -504,15 +504,15 @@ macro_rules! tlv_stream { $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* }) => { #[derive(Debug)] - pub(crate) struct $name { + pub(super) struct $name { $( $field: Option, )* } - pub(crate) struct $nameref<'a> { + pub(super) struct $nameref<'a> { $( - pub(crate) $field: Option, + pub(super) $field: Option, )* } From 3e17e7f9bbc55faf7c3293b307d237357a2eb3b1 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 28 Nov 2022 12:58:39 -0500 Subject: [PATCH 02/10] Remove unused mut from OfferBuilder::amount_msats Seen when removing `#[allow(unused)]` from `offers` module. --- lightning/src/offers/offer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 5995c513dab..c31ca9bb9c5 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -137,7 +137,7 @@ impl OfferBuilder { /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`]. /// /// Successive calls to this method will override the previous setting. - pub fn amount_msats(mut self, amount_msats: u64) -> Self { + pub fn amount_msats(self, amount_msats: u64) -> Self { self.amount(Amount::Bitcoin { amount_msats }) } From 0c621646a627a0e43e10fdb7717ba0dbc45893c5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Sep 2022 16:57:46 -0500 Subject: [PATCH 03/10] 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/ln/features.rs | 9 ++- lightning/src/offers/invoice_request.rs | 101 ++++++++++++++++++++++++ lightning/src/offers/mod.rs | 2 + lightning/src/offers/offer.rs | 17 ++-- lightning/src/offers/payer.rs | 19 +++++ 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 lightning/src/offers/invoice_request.rs create mode 100644 lightning/src/offers/payer.rs diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index 77d0fa4529f..1f455471a9f 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -158,6 +158,7 @@ mod sealed { BasicMPP, ]); define_context!(OfferContext, []); + define_context!(InvoiceRequestContext, []); // This isn't a "real" feature context, and is only used in the channel_type field in an // `OpenChannel` message. define_context!(ChannelTypeContext, [ @@ -367,7 +368,8 @@ mod sealed { supports_keysend, requires_keysend); #[cfg(test)] - define_feature!(123456789, UnknownFeature, [NodeContext, ChannelContext, InvoiceContext, OfferContext], + define_feature!(123456789, UnknownFeature, + [NodeContext, ChannelContext, InvoiceContext, OfferContext, InvoiceRequestContext], "Feature flags for an unknown feature used in testing.", set_unknown_feature_optional, set_unknown_feature_required, supports_unknown_test_feature, requires_unknown_test_feature); } @@ -426,8 +428,10 @@ pub type NodeFeatures = Features; pub type ChannelFeatures = Features; /// Features used within an invoice. pub type InvoiceFeatures = Features; -/// Features used within an offer. +/// Features used within an `offer`. pub type OfferFeatures = Features; +/// Features used within an `invoice_request`. +pub type InvoiceRequestFeatures = Features; /// Features used within the channel_type field in an OpenChannel message. /// @@ -735,6 +739,7 @@ macro_rules! impl_feature_tlv_write { impl_feature_tlv_write!(ChannelTypeFeatures); impl_feature_tlv_write!(OfferFeatures); +impl_feature_tlv_write!(InvoiceRequestFeatures); #[cfg(test)] mod tests { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs new file mode 100644 index 00000000000..43ae18d2c00 --- /dev/null +++ b/lightning/src/offers/invoice_request.rs @@ -0,0 +1,101 @@ +// 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::InvoiceRequestFeatures; +use crate::offers::offer::OfferContents; +use crate::offers::payer::PayerContents; +use crate::util::string::PrintableString; + +use crate::prelude::*; + +/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. +/// +/// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request +/// specifies these such that its 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: InvoiceRequestFeatures, + 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) -> &[u8] { + &self.contents.payer.0[..] + } + + /// 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) -> &InvoiceRequestFeatures { + &self.contents.features + } + + /// 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 possibly 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 { + self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) + } + + /// 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 c31ca9bb9c5..1ac8b0bdebf 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -224,7 +224,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`]. /// @@ -232,6 +232,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 @@ -240,7 +242,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(super) struct OfferContents { chains: Option>, @@ -263,10 +267,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`. @@ -346,6 +347,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..1705be85ed8 --- /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 +/// [`InvoiceRequest::payer_id`]. +/// +/// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id +#[derive(Clone, Debug)] +pub(crate) struct PayerContents(pub Vec); From 05bf27f7458ed5736f2b16dd027b2722c7852fe6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 24 Jun 2022 16:18:29 -0500 Subject: [PATCH 04/10] 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, 25 insertions(+), 12 deletions(-) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index b5d424efa34..02d4a81b39e 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}; @@ -499,7 +500,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] { @@ -664,7 +665,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)?; @@ -675,7 +676,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)?; @@ -764,20 +765,32 @@ 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) } - #[inline] - fn serialized_length(&self) -> usize { - COMPACT_SIGNATURE_SIZE - } } -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) + } +} + +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 a7adc7602a7865b677095ef2cdb80d290dff1170 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 9 Aug 2022 17:37:02 -0500 Subject: [PATCH 05/10] 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 | 167 +++++++++++++++++++++++++++++++++ lightning/src/offers/mod.rs | 1 + 2 files changed, 168 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..c7aafb0cfef --- /dev/null +++ b/lightning/src/offers/merkle.rs @@ -0,0 +1,167 @@ +// 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().record_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) { + 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; + } + + let left_branches = (0..num_leaves).step_by(step); + let right_branches = (offset..num_leaves).step_by(step); + for (i, j) in left_branches.zip(right_branches) { + 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: u64, + type_bytes: &'a [u8], + // The entire TLV record. + record_bytes: &'a [u8], +} + +impl AsRef<[u8]> for TlvRecord<'_> { + fn as_ref(&self) -> &[u8] { &self.record_bytes } +} + +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 = ::read(&mut self.data).unwrap().0; + let offset = self.data.position(); + let type_bytes = &self.data.get_ref()[start as usize..offset as usize]; + + let length = ::read(&mut self.data).unwrap().0; + let offset = self.data.position(); + let end = offset + length; + + let _value = &self.data.get_ref()[offset as usize..end as usize]; + let record_bytes = &self.data.get_ref()[start as usize..end as usize]; + + self.data.set_position(end); + + Some(TlvRecord { r#type, type_bytes, record_bytes }) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use bitcoin::hashes::{Hash, sha256}; + + #[test] + fn calculates_merkle_root_hash() { + // BOLT 12 test vectors + macro_rules! tlv1 { () => { "010203e8" } } + macro_rules! tlv2 { () => { "02080000010000020003" } } + macro_rules! tlv3 { () => { "03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002" } } + assert_eq!( + super::root_hash(&hex::decode(tlv1!()).unwrap()), + sha256::Hash::from_slice(&hex::decode("b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93").unwrap()).unwrap(), + ); + assert_eq!( + super::root_hash(&hex::decode(concat!(tlv1!(), tlv2!())).unwrap()), + sha256::Hash::from_slice(&hex::decode("c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1").unwrap()).unwrap(), + ); + assert_eq!( + super::root_hash(&hex::decode(concat!(tlv1!(), tlv2!(), tlv3!())).unwrap()), + sha256::Hash::from_slice(&hex::decode("ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d").unwrap()).unwrap(), + ); + } +} 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 59a7bd29fe02f10f6fcdc192c7994629675c7a30 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 23 Aug 2022 17:31:46 -0500 Subject: [PATCH 06/10] 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 | 127 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 21 ++++ lightning/src/offers/offer.rs | 63 +++++++++++- lightning/src/offers/parse.rs | 21 ++++ lightning/src/offers/payer.rs | 8 +- lightning/src/util/ser_macros.rs | 2 +- 6 files changed, 234 insertions(+), 8 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 43ae18d2c00..6cedf3eda59 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::InvoiceRequestFeatures; -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::util::string::PrintableString; use crate::prelude::*; @@ -34,7 +40,7 @@ pub struct InvoiceRequest { /// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. #[derive(Clone, Debug)] -pub(crate) struct InvoiceRequestContents { +pub(super) struct InvoiceRequestContents { payer: PayerContents, offer: OfferContents, chain: Option, @@ -75,9 +81,9 @@ impl InvoiceRequest { &self.contents.features } - /// 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 } @@ -99,3 +105,114 @@ impl InvoiceRequest { self.signature } } + +impl Writeable for InvoiceRequest { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { + (80, chain: ChainHash), + (82, amount: (u64, HighZeroBytesDroppedBigSize)), + (84, features: InvoiceRequestFeatures), + (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(bytes: Vec) -> Result { + let invoice_request = ParsedMessage::::try_from(bytes)?; + 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 = match metadata { + None => return Err(SemanticError::MissingPayerMetadata), + Some(metadata) => 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) { + (None, None) => return Err(SemanticError::MissingAmount), + (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency), + (_, amount_msats) => amount_msats, + }; + + let features = features.unwrap_or_else(InvoiceRequestFeatures::empty); + + 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 c7aafb0cfef..95183bea20d 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,25 @@ 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. +/// +/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. +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 1ac8b0bdebf..1403fbd223e 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -270,6 +270,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`. @@ -279,7 +284,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 @@ -329,6 +334,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() @@ -355,10 +372,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), @@ -572,6 +627,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")); @@ -610,6 +666,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); @@ -617,6 +674,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])); @@ -625,6 +683,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])); @@ -633,6 +692,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 01397098176..b9815b81177 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; @@ -115,23 +116,37 @@ 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, /// The amount exceeded the total bitcoin supply. InvalidAmount, + /// An amount was provided but was not sufficient in value. + InsufficientAmount, /// A currency was provided that is not supported. UnsupportedCurrency, /// A required description was not provided. MissingDescription, /// A signing pubkey was not provided. MissingSigningPubkey, + /// A quantity was expected but was missing. + MissingQuantity, /// An unsupported quantity was provided. InvalidQuantity, + /// A quantity or quantity bounds was provided but was not expected. + UnexpectedQuantity, + /// Payer metadata was expected but was missing. + MissingPayerMetadata, + /// A payer id was expected but was missing. + MissingPayerId, } impl From for ParseError { @@ -151,3 +166,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 1705be85ed8..e389a8f6d5d 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 @@ -16,4 +18,8 @@ use crate::prelude::*; /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id #[derive(Clone, Debug)] -pub(crate) struct PayerContents(pub Vec); +pub(super) struct PayerContents(pub Vec); + +tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { + (0, metadata: (Vec, WithoutLength)), +}); diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 6cc9d947753..231320ac159 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -506,7 +506,7 @@ macro_rules! tlv_stream { #[derive(Debug)] pub(super) struct $name { $( - $field: Option, + pub(super) $field: Option, )* } From 13ba7cc5233bf7ed692adebefee0b352d4800ab3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 31 Aug 2022 10:19:44 -0500 Subject: [PATCH 07/10] Builder for creating invoice requests Add a builder for creating invoice requests for an offer given a payer_id. Other settings may be optional depending on the offer and duplicative settings will override previous settings. Building produces a semantically valid `invoice_request` message for the offer, which then can be signed for the payer_id. --- lightning/src/offers/invoice_request.rs | 315 +++++++++++++++++++++--- lightning/src/offers/merkle.rs | 39 ++- lightning/src/offers/offer.rs | 112 +++++++-- lightning/src/offers/parse.rs | 4 + 4 files changed, 406 insertions(+), 64 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 6cedf3eda59..dc7d590b7eb 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -8,23 +8,208 @@ // 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 present it to the customer. +//! +//! The recipient of the request responds with an `Invoice`. +//! +//! ```ignore +//! extern crate bitcoin; +//! extern crate lightning; +//! +//! use bitcoin::network::constants::Network; +//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use core::convert::Infallible; +//! 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(vec![42; 64], pubkey)? +//! .chain(Network::Testnet)? +//! .amount_msats(1000)? +//! .quantity(5)? +//! .payer_note("foo".to_string()) +//! .build()? +//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .expect("failed verifying signature") +//! .write(&mut buffer) +//! .unwrap(); +//! # Ok(()) +//! # } +//! ``` use bitcoin::blockdata::constants::ChainHash; -use bitcoin::secp256k1::PublicKey; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::{Message, PublicKey}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; use crate::io; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::msgs::DecodeError; -use crate::offers::merkle::{SignatureTlvStream, self}; -use crate::offers::offer::{Amount, OfferContents, OfferTlvStream}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::offer::{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::util::string::PrintableString; 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, metadata: Vec, payer_id: PublicKey) -> Self { + Self { + offer, + invoice_request: InvoiceRequestContents { + payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None, + amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, + payer_id, payer_note: None, + }, + } + } + + /// Sets the [`InvoiceRequest::chain`] of the given [`Network`] for paying an invoice. If not + /// called, [`Network::Bitcoin`] is assumed. Errors if the chain for `network` is not 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 [`InvoiceRequest::amount_msats`] for paying an invoice. Errors if `amount_msats` is + /// not at least the expected 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) -> Result { + self.invoice_request.offer.check_amount_msats_for_quantity( + Some(amount_msats), self.invoice_request.quantity + )?; + self.invoice_request.amount_msats = Some(amount_msats); + Ok(self) + } + + /// Sets [`InvoiceRequest::quantity`] of items. If not set, `1` is assumed. Errors if `quantity` + /// does not conform to [`Offer::is_valid_quantity`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn quantity(mut self, quantity: u64) -> Result { + self.invoice_request.offer.check_quantity(Some(quantity))?; + self.invoice_request.quantity = Some(quantity); + Ok(self) + } + + /// Sets the [`InvoiceRequest::payer_note`]. + /// + /// 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 unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed + /// by [`UnsignedInvoiceRequest::sign`]. + pub fn build(mut self) -> Result, SemanticError> { + #[cfg(feature = "std")] { + if self.offer.is_expired() { + return Err(SemanticError::AlreadyExpired); + } + } + + let chain = self.invoice_request.chain(); + if !self.offer.supports_chain(chain) { + return Err(SemanticError::UnsupportedChain); + } + + if chain == self.offer.implied_chain() { + self.invoice_request.chain = None; + } + + if self.offer.amount().is_none() && self.invoice_request.amount_msats.is_none() { + return Err(SemanticError::MissingAmount); + } + + self.invoice_request.offer.check_quantity(self.invoice_request.quantity)?; + self.invoice_request.offer.check_amount_msats_for_quantity( + self.invoice_request.amount_msats, self.invoice_request.quantity + )?; + + 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) -> Result + { + // 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 provide choices such as quantity, amount, chain, features, etc. An invoice request @@ -61,17 +246,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 } @@ -82,8 +264,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 } @@ -93,7 +273,8 @@ impl InvoiceRequest { self.contents.payer_id } - /// Payer provided note to include in the invoice. + /// A payer-provided note which will be seen by the recipient and reflected back in the invoice + /// response. pub fn payer_note(&self) -> Option { self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } @@ -106,12 +287,48 @@ 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: Some(&self.payer.0), + }; + + let offer = self.offer.as_tlv_stream(); + + let features = { + if self.features == InvoiceRequestFeatures::empty() { None } + else { Some(&self.features) } + }; + + let invoice_request = InvoiceRequestTlvStreamRef { + chain: self.chain.as_ref(), + amount: self.amount_msats, + features, + 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) + } +} + tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { (80, chain: ChainHash), (82, amount: (u64, HighZeroBytesDroppedBigSize)), @@ -137,6 +354,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; @@ -152,8 +375,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 }) @@ -180,31 +402,14 @@ impl TryFrom for InvoiceRequestContents { return Err(SemanticError::UnsupportedChain); } - let amount_msats = match (offer.amount(), amount) { - (None, None) => return Err(SemanticError::MissingAmount), - (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency), - (_, amount_msats) => amount_msats, - }; - - let features = features.unwrap_or_else(InvoiceRequestFeatures::empty); + if offer.amount().is_none() && amount.is_none() { + return Err(SemanticError::MissingAmount); + } - 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, - }; + offer.check_quantity(quantity)?; + offer.check_amount_msats_for_quantity(amount, 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 features = features.unwrap_or_else(InvoiceRequestFeatures::empty); let payer_id = match payer_id { None => return Err(SemanticError::MissingPayerId), @@ -212,7 +417,43 @@ impl TryFrom for InvoiceRequestContents { }; Ok(InvoiceRequestContents { - payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note, + payer, offer, chain, amount_msats: amount, features, quantity, payer_id, payer_note, }) } } + +#[cfg(test)] +mod tests { + use super::InvoiceRequest; + + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; + use core::convert::{Infallible, 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()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], keys.public_key()).unwrap() + .build().unwrap() + .sign::<_, Infallible>(|digest| Ok(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 95183bea20d..d34a2a073f3 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -24,6 +24,35 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { (240, signature: Signature), }); +/// Error when signing messages. +#[derive(Debug)] +pub enum SignError { + /// User-defined error when signing the message. + Signing(E), + /// Error when verifying the produced signature using the given pubkey. + Verification(secp256k1::Error), +} + +/// Signs a message digest consisting of a tagged hash of the given bytes, checking if it can be +/// verified with the supplied pubkey. +/// +/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. +pub(super) fn sign_message( + sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result> +where + F: FnOnce(&Message) -> Result +{ + let digest = message_digest(tag, bytes); + let signature = sign(&digest).map_err(|e| SignError::Signing(e))?; + + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?; + + Ok(signature) +} + /// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message /// digest. /// @@ -31,14 +60,18 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { 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 1403fbd223e..d890ab3f642 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}; @@ -149,15 +150,6 @@ impl OfferBuilder { self } - /// Sets the [`Offer::features`]. - /// - /// Successive calls to this method will override the previous setting. - #[cfg(test)] - pub fn features(mut self, features: OfferFeatures) -> Self { - self.offer.features = features; - self - } - /// Sets the [`Offer::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has /// already passed is valid and can be checked for using [`Offer::is_expired`]. /// @@ -222,6 +214,14 @@ impl OfferBuilder { } } +#[cfg(test)] +impl OfferBuilder { + fn features_unchecked(mut self, features: OfferFeatures) -> Self { + self.offer.features = features; + self + } +} + /// 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 @@ -238,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`. @@ -270,6 +270,10 @@ impl Offer { self.contents.chains() } + pub(super) fn implied_chain(&self) -> ChainHash { + self.contents.implied_chain() + } + /// Returns whether the given chain is supported by the offer. pub fn supports_chain(&self, chain: ChainHash) -> bool { self.contents.supports_chain(chain) @@ -351,6 +355,29 @@ impl Offer { self.contents.signing_pubkey.unwrap() } + /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which + /// will be reflected in the `Invoice` response. + /// + /// The `metadata` is useful for including information about the derivation of `payer_id` such + /// that invoice response handling can be stateless. Also serves as payer-provided entropy while + /// hashing in the signature calculation. + /// + /// This should not leak any information such as by using a simple BIP-32 derivation path. + /// Otherwise, payments may be correlated. + /// + /// Errors if the offer contains unknown required features. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub fn request_invoice( + &self, metadata: Vec, payer_id: PublicKey + ) -> Result { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + Ok(InvoiceRequestBuilder::new(self, metadata, payer_id)) + } + #[cfg(test)] fn as_tlv_stream(&self) -> OfferTlvStreamRef { self.contents.as_tlv_stream() @@ -380,23 +407,48 @@ impl OfferContents { self.amount.as_ref() } - pub fn amount_msats(&self) -> u64 { - match self.amount() { + pub(super) fn check_amount_msats_for_quantity( + &self, amount_msats: Option, quantity: Option + ) -> Result<(), SemanticError> { + let offer_amount_msats = match self.amount { None => 0, - Some(&Amount::Bitcoin { amount_msats }) => amount_msats, - Some(&Amount::Currency { .. }) => unreachable!(), + Some(Amount::Bitcoin { amount_msats }) => amount_msats, + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), + }; + + if !self.expects_quantity() || quantity.is_some() { + let expected_amount_msats = offer_amount_msats * quantity.unwrap_or(1); + let amount_msats = amount_msats.unwrap_or(expected_amount_msats); + + if amount_msats < expected_amount_msats { + return Err(SemanticError::InsufficientAmount); + } + + if amount_msats > MAX_VALUE_MSAT { + return Err(SemanticError::InvalidAmount); + } } - } - pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { - self.amount_msats() * quantity + Ok(()) } pub fn supported_quantity(&self) -> Quantity { self.supported_quantity } - pub fn is_valid_quantity(&self, quantity: u64) -> bool { + pub(super) fn check_quantity(&self, quantity: Option) -> Result<(), SemanticError> { + let expects_quantity = self.expects_quantity(); + match quantity { + None if expects_quantity => Err(SemanticError::MissingQuantity), + Some(_) if !expects_quantity => Err(SemanticError::UnexpectedQuantity), + Some(quantity) if !self.is_valid_quantity(quantity) => { + Err(SemanticError::InvalidQuantity) + }, + _ => Ok(()), + } + } + + fn is_valid_quantity(&self, quantity: u64) -> bool { match self.supported_quantity { Quantity::Bounded(n) => { let n = n.get(); @@ -407,14 +459,14 @@ impl OfferContents { } } - pub fn expects_quantity(&self) -> bool { + fn expects_quantity(&self) -> bool { match self.supported_quantity { Quantity::Bounded(n) => n.get() != 1, Quantity::Unbounded => true, } } - 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)), @@ -760,15 +812,15 @@ mod tests { #[test] fn builds_offer_with_features() { let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .features(OfferFeatures::unknown()) + .features_unchecked(OfferFeatures::unknown()) .build() .unwrap(); assert_eq!(offer.features(), &OfferFeatures::unknown()); assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .features(OfferFeatures::unknown()) - .features(OfferFeatures::empty()) + .features_unchecked(OfferFeatures::unknown()) + .features_unchecked(OfferFeatures::empty()) .build() .unwrap(); assert_eq!(offer.features(), &OfferFeatures::empty()); @@ -890,6 +942,18 @@ mod tests { assert_eq!(tlv_stream.quantity_max, None); } + #[test] + fn fails_requesting_invoice_with_unknown_required_features() { + match OfferBuilder::new("foo".into(), pubkey(42)) + .features_unchecked(OfferFeatures::unknown()) + .build().unwrap() + .request_invoice(vec![1; 32], pubkey(43)) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures), + } + } + #[test] fn parses_offer_with_chains() { let offer = OfferBuilder::new("foo".into(), pubkey(42)) diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index b9815b81177..0b3dda79285 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -123,6 +123,8 @@ pub enum ParseError { /// Error when interpreting a TLV stream as a specific type. #[derive(Debug, PartialEq)] pub enum SemanticError { + /// The current [`std::time::SystemTime`] is past the offer or invoice's expiration. + AlreadyExpired, /// The provided chain hash does not correspond to a supported chain. UnsupportedChain, /// An amount was expected but was missing. @@ -133,6 +135,8 @@ pub enum SemanticError { 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 signing pubkey was not provided. From d666eb670009c370289c60ea741997d605e96f63 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 10 Nov 2022 21:12:58 -0600 Subject: [PATCH 08/10] Invoice request building tests Tests for checking invoice_request message semantics when building as defined by BOLT 12. --- lightning/src/offers/invoice_request.rs | 468 +++++++++++++++++++++++- lightning/src/offers/merkle.rs | 2 +- 2 files changed, 465 insertions(+), 5 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index dc7d590b7eb..356c0ddfa6f 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -171,6 +171,14 @@ impl<'a> InvoiceRequestBuilder<'a> { } } +#[cfg(test)] +impl<'a> InvoiceRequestBuilder<'a> { + fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { + self.invoice_request.features = features; + self + } +} + /// A semantically valid [`InvoiceRequest`] that hasn't been signed. pub struct UnsignedInvoiceRequest<'a> { offer: &'a Offer, @@ -285,6 +293,16 @@ impl InvoiceRequest { pub fn signature(&self) -> Option { self.signature } + + #[cfg(test)] + fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { + let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) = + self.contents.as_tlv_stream(); + let signature_tlv_stream = SignatureTlvStreamRef { + signature: self.signature.as_ref(), + }; + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream) + } } impl InvoiceRequestContents { @@ -341,6 +359,14 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { type FullInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); +#[cfg(test)] +type FullInvoiceRequestTlvStreamRef<'a> = ( + PayerTlvStreamRef<'a>, + OfferTlvStreamRef<'a>, + InvoiceRequestTlvStreamRef<'a>, + SignatureTlvStreamRef<'a>, +); + impl SeekReadable for FullInvoiceRequestTlvStream { fn read(r: &mut R) -> Result { let payer = SeekReadable::read(r)?; @@ -426,12 +452,446 @@ impl TryFrom for InvoiceRequestContents { mod tests { use super::InvoiceRequest; - use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; + use bitcoin::blockdata::constants::ChainHash; + use bitcoin::network::constants::Network; + use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, self}; + use bitcoin::secp256k1::schnorr::Signature; use core::convert::{Infallible, TryFrom}; - use crate::ln::msgs::DecodeError; - use crate::offers::offer::OfferBuilder; - use crate::offers::parse::ParseError; + use core::num::NonZeroU64; + #[cfg(feature = "std")] + use core::time::Duration; + use crate::ln::features::InvoiceRequestFeatures; + use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::merkle::SignError; + use crate::offers::offer::{OfferBuilder, Quantity}; + use crate::offers::parse::{ParseError, SemanticError}; use crate::util::ser::{BigSize, Writeable}; + use crate::util::string::PrintableString; + + fn payer_keys() -> KeyPair { + let secp_ctx = Secp256k1::new(); + KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) + } + + fn payer_sign(digest: &Message) -> Result { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + } + + fn payer_pubkey() -> PublicKey { + payer_keys().public_key() + } + + fn recipient_sign(digest: &Message) -> Result { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + } + + fn recipient_pubkey() -> PublicKey { + let secp_ctx = Secp256k1::new(); + KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()).public_key() + } + + #[test] + fn builds_invoice_request_with_defaults() { + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap().sign(payer_sign).unwrap(); + + let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream) = + invoice_request.as_tlv_stream(); + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + assert_eq!(invoice_request.bytes, buffer.as_slice()); + assert_eq!(invoice_request.metadata(), &[1; 32]); + assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(invoice_request.amount_msats(), None); + assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::empty()); + assert_eq!(invoice_request.quantity(), None); + assert_eq!(invoice_request.payer_id(), payer_pubkey()); + assert_eq!(invoice_request.payer_note(), None); + assert!(invoice_request.signature().is_some()); + + assert_eq!(payer_tlv_stream.metadata, Some(&vec![1; 32])); + assert_eq!(offer_tlv_stream.chains, None); + assert_eq!(offer_tlv_stream.metadata, None); + assert_eq!(offer_tlv_stream.currency, None); + assert_eq!(offer_tlv_stream.amount, Some(1000)); + assert_eq!(offer_tlv_stream.description, Some(&String::from("foo"))); + assert_eq!(offer_tlv_stream.features, None); + assert_eq!(offer_tlv_stream.absolute_expiry, None); + assert_eq!(offer_tlv_stream.paths, None); + assert_eq!(offer_tlv_stream.issuer, None); + assert_eq!(offer_tlv_stream.quantity_max, None); + assert_eq!(offer_tlv_stream.node_id, Some(&recipient_pubkey())); + assert_eq!(invoice_request_tlv_stream.chain, None); + assert_eq!(invoice_request_tlv_stream.amount, None); + assert_eq!(invoice_request_tlv_stream.features, None); + assert_eq!(invoice_request_tlv_stream.quantity, None); + assert_eq!(invoice_request_tlv_stream.payer_id, Some(&payer_pubkey())); + assert_eq!(invoice_request_tlv_stream.payer_note, None); + assert!(signature_tlv_stream.signature.is_some()); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice request: {:?}", e); + } + } + + #[cfg(feature = "std")] + #[test] + fn builds_invoice_request_from_offer_with_expiration() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(future_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build() + { + panic!("error building invoice_request: {:?}", e); + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(past_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::AlreadyExpired), + } + } + + #[test] + fn builds_invoice_request_with_chain() { + let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); + let testnet = ChainHash::using_genesis_block(Network::Testnet); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Bitcoin).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.chain(), mainnet); + assert_eq!(tlv_stream.chain, None); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .chain(Network::Testnet) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Testnet).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.chain(), testnet); + assert_eq!(tlv_stream.chain, Some(&testnet)); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Bitcoin).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.chain(), mainnet); + assert_eq!(tlv_stream.chain, None); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Bitcoin).unwrap() + .chain(Network::Testnet).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.chain(), testnet); + assert_eq!(tlv_stream.chain, Some(&testnet)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .chain(Network::Testnet) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Bitcoin) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnsupportedChain), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .chain(Network::Testnet) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnsupportedChain), + } + } + + #[test] + fn builds_invoice_request_with_amount() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(1000).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(tlv_stream.amount, Some(1000)); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(1001).unwrap() + .amount_msats(1000).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.amount_msats(), Some(1000)); + assert_eq!(tlv_stream.amount, Some(1000)); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(1001).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.amount_msats(), Some(1001)); + assert_eq!(tlv_stream.amount, Some(1001)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(999) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InsufficientAmount), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(2).unwrap() + .amount_msats(1000) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InsufficientAmount), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(MAX_VALUE_MSAT + 1) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(1000).unwrap() + .quantity(2).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InsufficientAmount), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::MissingAmount), + } + } + + #[test] + fn builds_invoice_request_with_features() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .features_unchecked(InvoiceRequestFeatures::unknown()) + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::unknown()); + assert_eq!(tlv_stream.features, Some(&InvoiceRequestFeatures::unknown())); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .features_unchecked(InvoiceRequestFeatures::unknown()) + .features_unchecked(InvoiceRequestFeatures::empty()) + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::empty()); + assert_eq!(tlv_stream.features, None); + } + + #[test] + fn builds_invoice_request_with_quantity() { + let ten = NonZeroU64::new(10).unwrap(); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::one()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.quantity(), None); + assert_eq!(tlv_stream.quantity, None); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::one()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(2_000).unwrap() + .quantity(2) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnexpectedQuantity), + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(10_000).unwrap() + .quantity(10).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.amount_msats(), Some(10_000)); + assert_eq!(tlv_stream.amount, Some(10_000)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(11_000).unwrap() + .quantity(11) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidQuantity), + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(2_000).unwrap() + .quantity(2).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.amount_msats(), Some(2_000)); + assert_eq!(tlv_stream.amount, Some(2_000)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::MissingQuantity), + } + } + + #[test] + fn builds_invoice_request_with_payer_note() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .payer_note("bar".into()) + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.payer_note(), Some(PrintableString("bar"))); + assert_eq!(tlv_stream.payer_note, Some(&String::from("bar"))); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .payer_note("bar".into()) + .payer_note("baz".into()) + .build().unwrap() + .sign(payer_sign).unwrap(); + let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); + assert_eq!(invoice_request.payer_note(), Some(PrintableString("baz"))); + assert_eq!(tlv_stream.payer_note, Some(&String::from("baz"))); + } + + #[test] + fn fails_signing_invoice_request() { + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(|_| Err(())) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SignError::Signing(())), + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(recipient_sign) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SignError::Verification(secp256k1::Error::InvalidSignature)), + } + } #[test] fn fails_parsing_invoice_request_with_extra_tlv_records() { diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index d34a2a073f3..5a388be9717 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -25,7 +25,7 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { }); /// Error when signing messages. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SignError { /// User-defined error when signing the message. Signing(E), From 984c906406a5b3d49bb7e4723dbe36754cff6edd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 16 Nov 2022 16:13:52 -0600 Subject: [PATCH 09/10] Invoice request parsing tests Tests for checking invoice_request message semantics when parsing bytes as defined by BOLT 12. --- lightning/src/offers/invoice_request.rs | 359 +++++++++++++++++++++++- lightning/src/offers/offer.rs | 11 +- 2 files changed, 367 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 356c0ddfa6f..fd3251deacd 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -173,10 +173,31 @@ impl<'a> InvoiceRequestBuilder<'a> { #[cfg(test)] impl<'a> InvoiceRequestBuilder<'a> { + fn chain_unchecked(mut self, network: Network) -> Self { + let chain = ChainHash::using_genesis_block(network); + self.invoice_request.chain = Some(chain); + self + } + + fn amount_msats_unchecked(mut self, amount_msats: u64) -> Self { + self.invoice_request.amount_msats = Some(amount_msats); + self + } + fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self { self.invoice_request.features = features; self } + + fn quantity_unchecked(mut self, quantity: u64) -> Self { + self.invoice_request.quantity = Some(quantity); + self + } + + fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { + let InvoiceRequestBuilder { offer, invoice_request } = self; + UnsignedInvoiceRequest { offer, invoice_request } + } } /// A semantically valid [`InvoiceRequest`] that hasn't been signed. @@ -463,7 +484,7 @@ mod tests { use crate::ln::features::InvoiceRequestFeatures; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::merkle::SignError; - use crate::offers::offer::{OfferBuilder, Quantity}; + use crate::offers::offer::{Amount, OfferBuilder, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; @@ -893,6 +914,342 @@ mod tests { } } + #[test] + fn parses_invoice_request_with_metadata() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![42; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + } + + #[test] + fn parses_invoice_request_with_chain() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain(Network::Bitcoin).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .chain_unchecked(Network::Testnet) + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnsupportedChain)), + } + } + + #[test] + fn parses_invoice_request_with_amount() { + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(1000).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)), + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats_unchecked(999) + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InsufficientAmount)), + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount(Amount::Currency { iso4217_code: *b"USD", amount: 1000 }) + .build_unchecked() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnsupportedCurrency)); + }, + } + } + + #[test] + fn parses_invoice_request_with_quantity() { + let ten = NonZeroU64::new(10).unwrap(); + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::one()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::one()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(2_000).unwrap() + .quantity_unchecked(2) + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity)); + }, + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(10_000).unwrap() + .quantity(10).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(11_000).unwrap() + .quantity_unchecked(11) + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity)), + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .amount_msats(2_000).unwrap() + .quantity(2).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingQuantity)), + } + } + + #[test] + fn fails_parsing_invoice_request_without_metadata() { + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap(); + let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + tlv_stream.0.metadata = None; + + let mut buffer = Vec::new(); + tlv_stream.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPayerMetadata)); + }, + } + } + + #[test] + fn fails_parsing_invoice_request_without_payer_id() { + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap(); + let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + tlv_stream.2.payer_id = None; + + let mut buffer = Vec::new(); + tlv_stream.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPayerId)), + } + } + + #[test] + fn fails_parsing_invoice_request_without_node_id() { + let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap(); + let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap(); + let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + tlv_stream.1.node_id = None; + + let mut buffer = Vec::new(); + tlv_stream.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey)); + }, + } + } + + #[test] + fn parses_invoice_request_without_signature() { + let mut buffer = Vec::new(); + OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .invoice_request + .write(&mut buffer).unwrap(); + + if let Err(e) = InvoiceRequest::try_from(buffer) { + panic!("error parsing invoice_request: {:?}", e); + } + } + + #[test] + fn fails_parsing_invoice_request_with_invalid_signature() { + let mut invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap(); + let last_signature_byte = invoice_request.bytes.last_mut().unwrap(); + *last_signature_byte = last_signature_byte.wrapping_add(1); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSignature(secp256k1::Error::InvalidSignature)); + }, + } + } + #[test] fn fails_parsing_invoice_request_with_extra_tlv_records() { let secp_ctx = Secp256k1::new(); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d890ab3f642..680f4094162 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -145,7 +145,7 @@ impl OfferBuilder { /// Sets the [`Offer::amount`]. /// /// Successive calls to this method will override the previous setting. - fn amount(mut self, amount: Amount) -> Self { + pub(super) fn amount(mut self, amount: Amount) -> Self { self.offer.amount = Some(amount); self } @@ -220,6 +220,13 @@ impl OfferBuilder { self.offer.features = features; self } + + pub(super) fn build_unchecked(self) -> Offer { + let mut bytes = Vec::new(); + self.offer.write(&mut bytes).unwrap(); + + Offer { bytes, contents: self.offer } + } } /// An `Offer` is a potentially long-lived proposal for payment of a good or service. @@ -379,7 +386,7 @@ impl Offer { } #[cfg(test)] - fn as_tlv_stream(&self) -> OfferTlvStreamRef { + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { self.contents.as_tlv_stream() } } From b25c8df648aa749de9b3f8ef42fa6d5b6dad42c8 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 28 Nov 2022 11:20:07 -0500 Subject: [PATCH 10/10] Add BOLT 12 merkle root test for `invoice_request` A BOLT 12 test vector uses an `invoice_request` message that has a currency, which aren't supported, so using OfferBuilder::build_unchecked is required to avoid a panic. --- lightning/src/offers/invoice_request.rs | 4 +- lightning/src/offers/merkle.rs | 51 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index fd3251deacd..90f6c183c0e 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -194,7 +194,7 @@ impl<'a> InvoiceRequestBuilder<'a> { self } - fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { + pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { let InvoiceRequestBuilder { offer, invoice_request } = self; UnsignedInvoiceRequest { offer, invoice_request } } @@ -247,7 +247,7 @@ impl<'a> UnsignedInvoiceRequest<'a> { /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct InvoiceRequest { - bytes: Vec, + pub(super) bytes: Vec, contents: InvoiceRequestContents, signature: Option, } diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 5a388be9717..57e7fe6833c 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -198,6 +198,11 @@ impl<'a> Iterator for TlvStream<'a> { #[cfg(test)] mod tests { use bitcoin::hashes::{Hash, sha256}; + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; + use core::convert::Infallible; + use crate::offers::offer::{Amount, OfferBuilder}; + use crate::offers::invoice_request::InvoiceRequest; + use crate::offers::parse::Bech32Encode; #[test] fn calculates_merkle_root_hash() { @@ -218,4 +223,50 @@ mod tests { sha256::Hash::from_slice(&hex::decode("ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d").unwrap()).unwrap(), ); } + + #[test] + fn calculates_merkle_root_hash_from_invoice_request() { + let secp_ctx = Secp256k1::new(); + let recipient_pubkey = { + let secret_key = SecretKey::from_slice(&hex::decode("4141414141414141414141414141414141414141414141414141414141414141").unwrap()).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key() + }; + let payer_keys = { + let secret_key = SecretKey::from_slice(&hex::decode("4242424242424242424242424242424242424242424242424242424242424242").unwrap()).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key) + }; + + // BOLT 12 test vectors + let invoice_request = OfferBuilder::new("A Mathematical Treatise".into(), recipient_pubkey) + .amount(Amount::Currency { iso4217_code: *b"USD", amount: 100 }) + .build_unchecked() + .request_invoice(vec![0; 8], payer_keys.public_key()).unwrap() + .build_unchecked() + .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &payer_keys))) + .unwrap(); + assert_eq!( + invoice_request.to_string(), + "lnr1qqyqqqqqqqqqqqqqqcp4256ypqqkgzshgysy6ct5dpjk6ct5d93kzmpq23ex2ct5d9ek293pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpjkppqvjx204vgdzgsqpvcp4mldl3plscny0rt707gvpdh6ndydfacz43euzqhrurageg3n7kafgsek6gz3e9w52parv8gs2hlxzk95tzeswywffxlkeyhml0hh46kndmwf4m6xma3tkq2lu04qz3slje2rfthc89vss", + ); + assert_eq!( + super::root_hash(&invoice_request.bytes[..]), + sha256::Hash::from_slice(&hex::decode("608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624").unwrap()).unwrap(), + ); + } + + impl AsRef<[u8]> for InvoiceRequest { + fn as_ref(&self) -> &[u8] { + &self.bytes + } + } + + impl Bech32Encode for InvoiceRequest { + const BECH32_HRP: &'static str = "lnr"; + } + + impl core::fmt::Display for InvoiceRequest { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } + } }