From bd0040a02bb7d4884ea4c3b5d731a9fb90c65b84 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 22 Dec 2022 09:04:48 -0600 Subject: [PATCH 01/11] Drop AsRef<[u8]> implementation for TlvRecord Explicitly using TlvRecord::record_bytes makes reading the code more obvious than hiding which bytes are used in AsRef<[u8]>::as_ref. --- lightning/src/offers/merkle.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 57e7fe6833c..9031e0eb540 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -88,7 +88,7 @@ fn root_hash(data: &[u8]) -> sha256::Hash { 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(leaf_tag.clone(), &record.record_bytes)); leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); } } @@ -164,10 +164,6 @@ struct TlvRecord<'a> { 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>; From 6b3535451e6243611d3435f9eb195706b9ca1ba7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 22 Dec 2022 09:10:21 -0600 Subject: [PATCH 02/11] Define TlvStream::skip_signatures Provide a helper for skipping signature TLV records from a TLV stream. This prevents needing to duplicate the check for signature TLV records when writing a TLV stream without signatures in an upcoming commit. --- lightning/src/offers/merkle.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 9031e0eb540..1a505aae8f0 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -75,22 +75,21 @@ fn message_digest(tag: &str, bytes: &[u8]) -> Message { /// 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 first_tlv_record = TlvStream::new(&data[..]).next().unwrap(); let mut engine = sha256::Hash::engine(); engine.input("LnNonce".as_bytes()); - engine.input(tlv_stream.peek().unwrap().record_bytes); + engine.input(first_tlv_record.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.record_bytes)); - leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); - } + let tlv_stream = TlvStream::new(&data[..]); + for record in tlv_stream.skip_signatures() { + leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes)); + leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); } // Calculate the merkle root hash in place. @@ -154,6 +153,10 @@ impl<'a> TlvStream<'a> { data: io::Cursor::new(data), } } + + fn skip_signatures(self) -> core::iter::Filter, fn(&TlvRecord) -> bool> { + self.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) + } } /// A slice into a [`TlvStream`] for a record. From 243f4487017f43712ebb1aa76806329f7e147593 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 22 Dec 2022 09:33:41 -0600 Subject: [PATCH 03/11] Encoding for TLV stream without signature records When using bytes from an InvoiceRequest to constructing bytes for an Invoice, any signature TLV records in the bytes must be excluded. Define a wrapper for encoding such pre-serialized bytes in this manner. This will allow the forthcoming InvoiceBuilder to construct bytes for an Invoice properly. --- lightning/src/offers/merkle.rs | 51 +++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 1a505aae8f0..9782dc7d1e8 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -13,7 +13,7 @@ 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}; +use crate::util::ser::{BigSize, Readable, Writeable, Writer}; use crate::prelude::*; @@ -194,14 +194,33 @@ impl<'a> Iterator for TlvStream<'a> { } } +/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records. +/// +/// Panics if the wrapped bytes are not a well-formed TLV stream. +pub(super) struct WithoutSignatures<'a>(pub &'a Vec); + +impl<'a> Writeable for WithoutSignatures<'a> { + #[inline] + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + let tlv_stream = TlvStream::new(&self.0[..]); + for record in tlv_stream.skip_signatures() { + writer.write_all(record.record_bytes)?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { + use super::{TlvStream, WithoutSignatures}; + 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; + use crate::util::ser::Writeable; #[test] fn calculates_merkle_root_hash() { @@ -253,6 +272,36 @@ mod tests { ); } + #[test] + fn skips_encoding_signature_tlv_records() { + let secp_ctx = Secp256k1::new(); + let recipient_pubkey = { + let secret_key = SecretKey::from_slice(&[41; 32]).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key).public_key() + }; + let payer_keys = { + let secret_key = SecretKey::from_slice(&[42; 32]).unwrap(); + KeyPair::from_secret_key(&secp_ctx, &secret_key) + }; + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey) + .amount_msats(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(); + + let mut bytes_without_signature = Vec::new(); + WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap(); + + assert_ne!(bytes_without_signature, invoice_request.bytes); + assert_eq!( + TlvStream::new(&bytes_without_signature).count(), + TlvStream::new(&invoice_request.bytes).count() - 1, + ); + } + impl AsRef<[u8]> for InvoiceRequest { fn as_ref(&self) -> &[u8] { &self.bytes From e1aa18aed800d575da555df1d84a468cd585f3f3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 12 Sep 2022 09:30:06 -0500 Subject: [PATCH 04/11] Invoice encoding and parsing Define an interface for BOLT 12 `invoice` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed for serialization. This is because it must mirror all the `offer` and `invoice_request` TLV records, including unknown ones, which aren't represented in the contents. Invoices may be created for an Offer (from an InvoiceRequest) or for a Refund. The primary difference is how the signing pubkey is given -- by the writer of the offer or the reader of the refund. --- lightning/src/offers/invoice.rs | 391 ++++++++++++++++++++++++ lightning/src/offers/invoice_request.rs | 14 +- lightning/src/offers/mod.rs | 1 + lightning/src/offers/offer.rs | 12 +- lightning/src/offers/parse.rs | 10 + lightning/src/offers/refund.rs | 14 +- 6 files changed, 429 insertions(+), 13 deletions(-) create mode 100644 lightning/src/offers/invoice.rs diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs new file mode 100644 index 00000000000..8264aa15fb3 --- /dev/null +++ b/lightning/src/offers/invoice.rs @@ -0,0 +1,391 @@ +// 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` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::util::address::{Address, Payload, WitnessVersion}; +use core::convert::TryFrom; +use core::time::Duration; +use crate::io; +use crate::ln::PaymentHash; +use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; +use crate::ln::msgs::DecodeError; +use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream}; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::OfferTlvStream; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::PayerTlvStream; +use crate::offers::refund::RefundContents; +use crate::onion_message::BlindedPath; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; + +use crate::prelude::*; + +#[cfg(feature = "std")] +use std::time::SystemTime; + +const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); + +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); + +/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`]. +/// +/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent +/// directly after scanning a refund. It includes all the information needed to pay a recipient. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`Refund`]: crate::offers::refund::Refund +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub struct Invoice { + bytes: Vec, + contents: InvoiceContents, + signature: Signature, +} + +/// The contents of an [`Invoice`] for responding to either an [`Offer`] or a [`Refund`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// [`Refund`]: crate::offers::refund::Refund +enum InvoiceContents { + /// Contents for an [`Invoice`] corresponding to an [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + ForOffer { + invoice_request: InvoiceRequestContents, + fields: InvoiceFields, + }, + /// Contents for an [`Invoice`] corresponding to a [`Refund`]. + /// + /// [`Refund`]: crate::offers::refund::Refund + ForRefund { + refund: RefundContents, + fields: InvoiceFields, + }, +} + +/// Invoice-specific fields for an `invoice` message. +struct InvoiceFields { + payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, + created_at: Duration, + relative_expiry: Option, + payment_hash: PaymentHash, + amount_msats: u64, + fallbacks: Option>, + features: Bolt12InvoiceFeatures, + signing_pubkey: PublicKey, +} + +impl Invoice { + /// Paths to the recipient originating from publicly reachable nodes, including information + /// needed for routing payments across them. Blinded paths provide recipient privacy by + /// obfuscating its node id. + pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] { + &self.contents.fields().payment_paths[..] + } + + /// Duration since the Unix epoch when the invoice was created. + pub fn created_at(&self) -> Duration { + self.contents.fields().created_at + } + + /// Duration since [`Invoice::created_at`] when the invoice has expired and therefore should no + /// longer be paid. + pub fn relative_expiry(&self) -> Duration { + self.contents.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY) + } + + /// Whether the invoice has expired. + #[cfg(feature = "std")] + pub fn is_expired(&self) -> bool { + let absolute_expiry = self.created_at().checked_add(self.relative_expiry()); + match absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + + /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. + pub fn payment_hash(&self) -> PaymentHash { + self.contents.fields().payment_hash + } + + /// The minimum amount required for a successful payment of the invoice. + pub fn amount_msats(&self) -> u64 { + self.contents.fields().amount_msats + } + + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to + /// least-preferred. + pub fn fallbacks(&self) -> Vec
{ + let network = match self.network() { + None => return Vec::new(), + Some(network) => network, + }; + + let to_valid_address = |address: &FallbackAddress| { + let version = match WitnessVersion::try_from(address.version) { + Ok(version) => version, + Err(_) => return None, + }; + + let program = &address.program; + if program.len() < 2 || program.len() > 40 { + return None; + } + + let address = Address { + payload: Payload::WitnessProgram { + version, + program: address.program.clone(), + }, + network, + }; + + if !address.is_standard() && version == WitnessVersion::V0 { + return None; + } + + Some(address) + }; + + self.contents.fields().fallbacks + .as_ref() + .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect()) + .unwrap_or_else(Vec::new) + } + + fn network(&self) -> Option { + let chain = self.contents.chain(); + if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Some(Network::Bitcoin) + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Some(Network::Testnet) + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Some(Network::Signet) + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Some(Network::Regtest) + } else { + None + } + } + + /// Features pertaining to paying an invoice. + pub fn features(&self) -> &Bolt12InvoiceFeatures { + &self.contents.fields().features + } + + /// The public key used to sign invoices. + pub fn signing_pubkey(&self) -> PublicKey { + self.contents.fields().signing_pubkey + } + + /// Signature of the invoice using [`Invoice::signing_pubkey`]. + pub fn signature(&self) -> Signature { + self.signature + } +} + +impl InvoiceContents { + fn chain(&self) -> ChainHash { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(), + InvoiceContents::ForRefund { refund, .. } => refund.chain(), + } + } + + fn fields(&self) -> &InvoiceFields { + match self { + InvoiceContents::ForOffer { fields, .. } => fields, + InvoiceContents::ForRefund { fields, .. } => fields, + } + } +} + +impl Writeable for Invoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice = ParsedMessage::::try_from(bytes)?; + Invoice::try_from(parsed_invoice) + } +} + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { + (160, paths: (Vec, WithoutLength)), + (162, blindedpay: (Vec, WithoutLength)), + (164, created_at: (u64, HighZeroBytesDroppedBigSize)), + (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), + (168, payment_hash: PaymentHash), + (170, amount: (u64, HighZeroBytesDroppedBigSize)), + (172, fallbacks: (Vec, WithoutLength)), + (174, features: (Bolt12InvoiceFeatures, WithoutLength)), + (176, node_id: PublicKey), +}); + +/// Information needed to route a payment across a [`BlindedPath`] hop. +#[derive(Debug, PartialEq)] +pub struct BlindedPayInfo { + fee_base_msat: u32, + fee_proportional_millionths: u32, + cltv_expiry_delta: u16, + htlc_minimum_msat: u64, + htlc_maximum_msat: u64, + features: BlindedHopFeatures, +} + +impl_writeable!(BlindedPayInfo, { + fee_base_msat, + fee_proportional_millionths, + cltv_expiry_delta, + htlc_minimum_msat, + htlc_maximum_msat, + features +}); + +/// Wire representation for an on-chain fallback address. +#[derive(Debug, PartialEq)] +pub(super) struct FallbackAddress { + version: u8, + program: Vec, +} + +impl_writeable!(FallbackAddress, { version, program }); + +type FullInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let invoice = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, invoice, signature)) + } +} + +type PartialInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(invoice: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = invoice; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) + )?; + + let signature = match signature { + None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)), + Some(signature) => signature, + }; + let pubkey = contents.fields().signing_pubkey; + merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?; + + Ok(Invoice { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result { + let ( + payer_tlv_stream, + offer_tlv_stream, + invoice_request_tlv_stream, + InvoiceTlvStream { + paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, + features, node_id, + }, + ) = tlv_stream; + + let payment_paths = match (paths, blindedpay) { + (None, _) => return Err(SemanticError::MissingPaths), + (_, None) => return Err(SemanticError::InvalidPayInfo), + (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths), + (Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => { + return Err(SemanticError::InvalidPayInfo); + }, + (Some(paths), Some(blindedpay)) => { + paths.into_iter().zip(blindedpay.into_iter()).collect::>() + }, + }; + + let created_at = match created_at { + None => return Err(SemanticError::MissingCreationTime), + Some(timestamp) => Duration::from_secs(timestamp), + }; + + let relative_expiry = relative_expiry + .map(Into::::into) + .map(Duration::from_secs); + + let payment_hash = match payment_hash { + None => return Err(SemanticError::MissingPaymentHash), + Some(payment_hash) => payment_hash, + }; + + let amount_msats = match amount { + None => return Err(SemanticError::MissingAmount), + Some(amount) => amount, + }; + + let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty); + + let signing_pubkey = match node_id { + None => return Err(SemanticError::MissingSigningPubkey), + Some(node_id) => node_id, + }; + + let fields = InvoiceFields { + payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks, + features, signing_pubkey, + }; + + match offer_tlv_stream.node_id { + Some(expected_signing_pubkey) => { + if fields.signing_pubkey != expected_signing_pubkey { + return Err(SemanticError::InvalidSigningPubkey); + } + + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForOffer { invoice_request, fields }) + }, + None => { + let refund = RefundContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForRefund { refund, fields }) + }, + } + } +} diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index fd5ecda558a..21c11bcbfc6 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -11,11 +11,12 @@ //! //! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is //! typically constructed by a customer and sent to the merchant who had published the corresponding -//! offer. The recipient of the request responds with an `Invoice`. +//! offer. The recipient of the request responds with an [`Invoice`]. //! //! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a //! precursor, see [`Refund`]. //! +//! [`Invoice`]: crate::offers::invoice::Invoice //! [`Refund`]: crate::offers::refund::Refund //! //! ```ignore @@ -239,11 +240,12 @@ impl<'a> UnsignedInvoiceRequest<'a> { } } -/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. +/// 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. /// +/// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct InvoiceRequest { @@ -252,11 +254,13 @@ pub struct InvoiceRequest { signature: Signature, } -/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. +/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`]. +/// +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub(super) struct InvoiceRequestContents { payer: PayerContents, - offer: OfferContents, + pub(super) offer: OfferContents, chain: Option, amount_msats: Option, features: InvoiceRequestFeatures, @@ -327,7 +331,7 @@ impl InvoiceRequest { } impl InvoiceRequestContents { - fn chain(&self) -> ChainHash { + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.offer.implied_chain()) } diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 11df5ca1f8a..2da6fac08ff 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,6 +12,7 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice; pub mod invoice_request; mod merkle; pub mod offer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index e6550044600..d28a0bdd90e 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -232,7 +232,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 -/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to +/// 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`]. /// /// Offers may be denominated in currency other than bitcoin but are ultimately paid using the @@ -241,6 +241,7 @@ impl OfferBuilder { /// Through the use of [`BlindedPath`]s, offers provide recipient privacy. /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown @@ -249,9 +250,10 @@ pub struct Offer { pub(super) 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 +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] pub(super) struct OfferContents { chains: Option>, @@ -359,7 +361,7 @@ impl Offer { /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey + self.contents.signing_pubkey() } /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which @@ -473,6 +475,10 @@ impl OfferContents { } } + pub(super) fn signing_pubkey(&self) -> PublicKey { + self.signing_pubkey + } + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index deada66b05c..a7d13e57050 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -147,6 +147,8 @@ pub enum SemanticError { MissingDescription, /// A signing pubkey was not provided. MissingSigningPubkey, + /// A signing pubkey was provided but a different one was expected. + InvalidSigningPubkey, /// A signing pubkey was provided but was not expected. UnexpectedSigningPubkey, /// A quantity was expected but was missing. @@ -159,6 +161,14 @@ pub enum SemanticError { MissingPayerMetadata, /// A payer id was expected but was missing. MissingPayerId, + /// Blinded paths were expected but were missing. + MissingPaths, + /// The blinded payinfo given does not match the number of blinded path hops. + InvalidPayInfo, + /// An invoice creation time was expected but was missing. + MissingCreationTime, + /// An invoice payment hash was expected but was missing. + MissingPaymentHash, /// A signature was expected but was missing. MissingSignature, } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index d3798463b44..f864e73a3d3 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -10,10 +10,11 @@ //! Data structures and encoding for refunds. //! //! A [`Refund`] is an "offer for money" and is typically constructed by a merchant and presented -//! directly to the customer. The recipient responds with an `Invoice` to be paid. +//! directly to the customer. The recipient responds with an [`Invoice`] to be paid. //! //! This is an [`InvoiceRequest`] produced *not* in response to an [`Offer`]. //! +//! [`Invoice`]: crate::offers::invoice::Invoice //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest //! [`Offer`]: crate::offers::offer::Offer //! @@ -191,12 +192,13 @@ impl RefundBuilder { } } -/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`]. +/// A `Refund` is a request to send an [`Invoice`] without a preceding [`Offer`]. /// /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to /// recoup their funds. A refund may be used more generally as an "offer for money", such as with a /// bitcoin ATM. /// +/// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct Refund { @@ -204,9 +206,11 @@ pub struct Refund { contents: RefundContents, } -/// The contents of a [`Refund`], which may be shared with an `Invoice`. +/// The contents of a [`Refund`], which may be shared with an [`Invoice`]. +/// +/// [`Invoice`]: crate::offers::invoice::Invoice #[derive(Clone, Debug)] -struct RefundContents { +pub(super) struct RefundContents { payer: PayerContents, // offer fields metadata: Option>, @@ -311,7 +315,7 @@ impl AsRef<[u8]> for Refund { } impl RefundContents { - fn chain(&self) -> ChainHash { + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.implied_chain()) } From 88c5197e4445166c4dd8307b9c6903ef4990c70f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Dec 2022 22:23:39 -0600 Subject: [PATCH 05/11] Builder for creating invoices for offers Add a builder for creating invoices for an offer from a given request and required fields. Other settings are optional and duplicative settings will override previous settings. Building produces a semantically valid `invoice` message for the offer, which then can be signed with the key associated with the offer's signing pubkey. --- lightning/src/offers/invoice.rs | 240 +++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 29 ++- lightning/src/offers/offer.rs | 19 +- lightning/src/offers/refund.rs | 19 +- lightning/src/util/ser.rs | 20 ++ lightning/src/util/ser_macros.rs | 8 +- 6 files changed, 310 insertions(+), 25 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 8264aa15fb3..a937a5483c9 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -10,24 +10,27 @@ //! Data structures and encoding for `invoice` messages. use bitcoin::blockdata::constants::ChainHash; +use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; +use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{Message, PublicKey}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::util::address::{Address, Payload, WitnessVersion}; +use bitcoin::util::schnorr::TweakedPublicKey; use core::convert::TryFrom; use core::time::Duration; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::ln::msgs::DecodeError; -use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream}; -use crate::offers::merkle::{SignatureTlvStream, self}; -use crate::offers::offer::OfferTlvStream; +use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, WithoutSignatures, self}; +use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; -use crate::offers::payer::PayerTlvStream; +use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef}; use crate::offers::refund::RefundContents; use crate::onion_message::BlindedPath; -use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer}; use crate::prelude::*; @@ -38,6 +41,161 @@ const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); +/// Builds an [`Invoice`] from either: +/// - an [`InvoiceRequest`] for the "offer to be paid" flow or +/// - a [`Refund`] for the "offer for money" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Refund`]: crate::offers::refund::Refund +/// [module-level documentation]: self +pub struct InvoiceBuilder<'a> { + invreq_bytes: &'a Vec, + invoice: InvoiceContents, +} + +impl<'a> InvoiceBuilder<'a> { + pub(super) fn for_offer( + invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, + created_at: Duration, payment_hash: PaymentHash + ) -> Result { + if payment_paths.is_empty() { + return Err(SemanticError::MissingPaths); + } + + let amount_msats = match invoice_request.amount_msats() { + Some(amount_msats) => amount_msats, + None => match invoice_request.contents.offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => { + amount_msats * invoice_request.quantity().unwrap_or(1) + }, + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), + None => return Err(SemanticError::MissingAmount), + }, + }; + + Ok(Self { + invreq_bytes: &invoice_request.bytes, + invoice: InvoiceContents::ForOffer { + invoice_request: invoice_request.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, + fallbacks: None, features: Bolt12InvoiceFeatures::empty(), + signing_pubkey: invoice_request.contents.offer.signing_pubkey(), + }, + }, + }) + } + + /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry + /// that has already passed is valid and can be checked for using [`Invoice::is_expired`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn relative_expiry(mut self, relative_expiry_secs: u32) -> Self { + let relative_expiry = Duration::from_secs(relative_expiry_secs as u64); + self.invoice.fields_mut().relative_expiry = Some(relative_expiry); + self + } + + /// Adds a P2WSH address to [`Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. + pub fn fallback_v0_p2wsh(mut self, script_hash: &WScriptHash) -> Self { + let address = FallbackAddress { + version: WitnessVersion::V0.to_num(), + program: Vec::from(&script_hash.into_inner()[..]), + }; + self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); + self + } + + /// Adds a P2WPKH address to [`Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. + pub fn fallback_v0_p2wpkh(mut self, pubkey_hash: &WPubkeyHash) -> Self { + let address = FallbackAddress { + version: WitnessVersion::V0.to_num(), + program: Vec::from(&pubkey_hash.into_inner()[..]), + }; + self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); + self + } + + /// Adds a P2TR address to [`Invoice::fallbacks`]. + /// + /// Successive calls to this method will add another address. Caller is responsible for not + /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses. + pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self { + let address = FallbackAddress { + version: WitnessVersion::V1.to_num(), + program: Vec::from(&output_key.serialize()[..]), + }; + self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); + self + } + + /// Sets [`Invoice::features`] to indicate MPP may be used. Otherwise, MPP is disallowed. + pub fn allow_mpp(mut self) -> Self { + self.invoice.fields_mut().features.set_basic_mpp_optional(); + self + } + + /// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by + /// [`UnsignedInvoice::sign`]. + pub fn build(self) -> Result, SemanticError> { + #[cfg(feature = "std")] { + if self.invoice.is_offer_or_refund_expired() { + return Err(SemanticError::AlreadyExpired); + } + } + + let InvoiceBuilder { invreq_bytes, invoice } = self; + Ok(UnsignedInvoice { invreq_bytes, invoice }) + } +} + +/// A semantically valid [`Invoice`] that hasn't been signed. +pub struct UnsignedInvoice<'a> { + invreq_bytes: &'a Vec, + invoice: InvoiceContents, +} + +impl<'a> UnsignedInvoice<'a> { + /// Signs the invoice using the given function. + pub fn sign(self, sign: F) -> Result> + where + F: FnOnce(&Message) -> Result + { + // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may + // have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or + // `RefundContents`. + let (_, _, _, invoice_tlv_stream) = self.invoice.as_tlv_stream(); + let invoice_request_bytes = WithoutSignatures(self.invreq_bytes); + let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream); + + let mut bytes = Vec::new(); + unsigned_tlv_stream.write(&mut bytes).unwrap(); + + let pubkey = self.invoice.fields().signing_pubkey; + let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?; + + // Append the signature TLV record to the bytes. + let signature_tlv_stream = SignatureTlvStreamRef { + signature: Some(&signature), + }; + signature_tlv_stream.write(&mut bytes).unwrap(); + + Ok(Invoice { + bytes, + contents: self.invoice, + signature, + }) + } +} + /// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`]. /// /// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent @@ -199,6 +357,15 @@ impl Invoice { } impl InvoiceContents { + /// Whether the original offer or refund has expired. + #[cfg(feature = "std")] + fn is_offer_or_refund_expired(&self) -> bool { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.offer.is_expired(), + InvoiceContents::ForRefund { refund, .. } => refund.is_expired(), + } + } + fn chain(&self) -> ChainHash { match self { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(), @@ -212,6 +379,44 @@ impl InvoiceContents { InvoiceContents::ForRefund { fields, .. } => fields, } } + + fn fields_mut(&mut self) -> &mut InvoiceFields { + match self { + InvoiceContents::ForOffer { fields, .. } => fields, + InvoiceContents::ForRefund { fields, .. } => fields, + } + } + + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { + let (payer, offer, invoice_request) = match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(), + InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(), + }; + let invoice = self.fields().as_tlv_stream(); + + (payer, offer, invoice_request, invoice) + } +} + +impl InvoiceFields { + fn as_tlv_stream(&self) -> InvoiceTlvStreamRef { + let features = { + if self.features == Bolt12InvoiceFeatures::empty() { None } + else { Some(&self.features) } + }; + + InvoiceTlvStreamRef { + paths: Some(Iterable(self.payment_paths.iter().map(|(path, _)| path))), + blindedpay: Some(Iterable(self.payment_paths.iter().map(|(_, payinfo)| payinfo))), + created_at: Some(self.created_at.as_secs()), + relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), + payment_hash: Some(&self.payment_hash), + amount: Some(self.amount_msats), + fallbacks: self.fallbacks.as_ref(), + features, + node_id: Some(&self.signing_pubkey), + } + } } impl Writeable for Invoice { @@ -230,8 +435,8 @@ impl TryFrom> for Invoice { } tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { - (160, paths: (Vec, WithoutLength)), - (162, blindedpay: (Vec, WithoutLength)), + (160, paths: (Vec, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)), + (162, blindedpay: (Vec, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)), (164, created_at: (u64, HighZeroBytesDroppedBigSize)), (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), (168, payment_hash: PaymentHash), @@ -241,7 +446,17 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (176, node_id: PublicKey), }); -/// Information needed to route a payment across a [`BlindedPath`] hop. +type BlindedPathIter<'a> = core::iter::Map< + core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>, + for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPath, +>; + +type BlindedPayInfoIter<'a> = core::iter::Map< + core::slice::Iter<'a, (BlindedPath, BlindedPayInfo)>, + for<'r> fn(&'r (BlindedPath, BlindedPayInfo)) -> &'r BlindedPayInfo, +>; + +/// Information needed to route a payment across a [`BlindedPath`]. #[derive(Debug, PartialEq)] pub struct BlindedPayInfo { fee_base_msat: u32, @@ -288,6 +503,13 @@ impl SeekReadable for FullInvoiceTlvStream { type PartialInvoiceTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStreamRef<'a> = ( + PayerTlvStreamRef<'a>, + OfferTlvStreamRef<'a>, + InvoiceRequestTlvStreamRef<'a>, + InvoiceTlvStreamRef<'a>, +); + impl TryFrom> for Invoice { type Error = ParseError; diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 21c11bcbfc6..126d9b552de 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -57,13 +57,17 @@ use bitcoin::network::constants::Network; use bitcoin::secp256k1::{Message, PublicKey}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; +use core::time::Duration; use crate::io; +use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::msgs::DecodeError; +use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; 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, PayerTlvStreamRef}; +use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -250,7 +254,7 @@ impl<'a> UnsignedInvoiceRequest<'a> { #[derive(Clone, Debug)] pub struct InvoiceRequest { pub(super) bytes: Vec, - contents: InvoiceRequestContents, + pub(super) contents: InvoiceRequestContents, signature: Signature, } @@ -319,6 +323,29 @@ impl InvoiceRequest { self.signature } + /// Creates an [`Invoice`] for the request with the given required fields. + /// + /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after + /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to + /// claim a payment for the invoice. + /// + /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It + /// must contain one or more elements. + /// + /// Errors if the request contains unknown required features. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash + ) -> Result { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) + } + #[cfg(test)] fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef { let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) = diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d28a0bdd90e..d92d0d8bb4c 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -321,13 +321,7 @@ impl Offer { /// Whether the offer has expired. #[cfg(feature = "std")] pub fn is_expired(&self) -> bool { - match self.absolute_expiry() { - Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { - Ok(elapsed) => elapsed > seconds_from_epoch, - Err(_) => false, - }, - None => false, - } + self.contents.is_expired() } /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be @@ -412,6 +406,17 @@ impl OfferContents { self.chains().contains(&chain) } + #[cfg(feature = "std")] + pub(super) fn is_expired(&self) -> bool { + match self.absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + pub fn amount(&self) -> Option<&Amount> { self.amount.as_ref() } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index f864e73a3d3..fdf763ecb95 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -243,13 +243,7 @@ impl Refund { /// Whether the refund has expired. #[cfg(feature = "std")] pub fn is_expired(&self) -> bool { - match self.absolute_expiry() { - Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { - Ok(elapsed) => elapsed > seconds_from_epoch, - Err(_) => false, - }, - None => false, - } + self.contents.is_expired() } /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be @@ -315,6 +309,17 @@ impl AsRef<[u8]> for Refund { } impl RefundContents { + #[cfg(feature = "std")] + pub(super) fn is_expired(&self) -> bool { + match self.absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + pub(super) fn chain(&self) -> ChainHash { self.chain.unwrap_or_else(|| self.implied_chain()) } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 84d1a2e084f..ebe20677cda 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -624,6 +624,26 @@ impl<'a, T> From<&'a Vec> for WithoutLength<&'a Vec> { fn from(v: &'a Vec) -> Self { Self(v) } } +#[derive(Debug)] +pub(crate) struct Iterable<'a, I: Iterator + Clone, T: 'a>(pub I); + +impl<'a, I: Iterator + Clone, T: 'a + Writeable> Writeable for Iterable<'a, I, T> { + #[inline] + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + for ref v in self.0.clone() { + v.write(writer)?; + } + Ok(()) + } +} + +#[cfg(test)] +impl<'a, I: Iterator + Clone, T: 'a + PartialEq> PartialEq for Iterable<'a, I, T> { + fn eq(&self, other: &Self) -> bool { + self.0.clone().collect::>() == other.0.clone().collect::>() + } +} + macro_rules! impl_for_map { ($ty: ident, $keybound: ident, $constr: expr) => { impl Writeable for $ty diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index afd7fcb2e0f..373a64e3e0e 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -286,6 +286,9 @@ macro_rules! _decode_tlv { ($reader: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{ $field = Some($trait::read(&mut $reader $(, $read_arg)*)?); }}; + ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident, $encoder:ty))) => {{ + $crate::_decode_tlv!($reader, $field, (option, encoding: ($fieldty, $encoding))); + }}; ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident))) => {{ $field = { let field: $encoding<$fieldty> = ser::Readable::read(&mut $reader)?; @@ -730,7 +733,8 @@ macro_rules! tlv_stream { )* } - #[derive(Debug, PartialEq)] + #[cfg_attr(test, derive(PartialEq))] + #[derive(Debug)] pub(super) struct $nameref<'a> { $( pub(super) $field: Option, @@ -770,6 +774,7 @@ macro_rules! tlv_stream { macro_rules! tlv_record_type { (($type:ty, $wrapper:ident)) => { $type }; + (($type:ty, $wrapper:ident, $encoder:ty)) => { $type }; ($type:ty) => { $type }; } @@ -780,6 +785,7 @@ macro_rules! tlv_record_ref_type { ((u32, $wrapper: ident)) => { u32 }; ((u64, $wrapper: ident)) => { u64 }; (($type:ty, $wrapper:ident)) => { &'a $type }; + (($type:ty, $wrapper:ident, $encoder:ty)) => { $encoder }; ($type:ty) => { &'a $type }; } From f779bc066fe9a0da409502cd0c4655ba71c50a93 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 20 Dec 2022 09:33:11 -0600 Subject: [PATCH 06/11] Builder for creating invoices for refunds Add a builder for creating invoices for a refund and required fields. Other settings are optional and duplicative settings will override previous settings. Building produces a semantically valid `invoice` message for the refund, which then can be signed with the key associated with the provided signing pubkey. --- lightning/src/offers/invoice.rs | 49 +++++++++++++++++++++++---------- lightning/src/offers/refund.rs | 32 +++++++++++++++++++-- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index a937a5483c9..24bf160e56c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -28,7 +28,7 @@ use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef}; -use crate::offers::refund::RefundContents; +use crate::offers::refund::{Refund, RefundContents}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer}; @@ -60,10 +60,6 @@ impl<'a> InvoiceBuilder<'a> { invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, payment_hash: PaymentHash ) -> Result { - if payment_paths.is_empty() { - return Err(SemanticError::MissingPaths); - } - let amount_msats = match invoice_request.amount_msats() { Some(amount_msats) => amount_msats, None => match invoice_request.contents.offer.amount() { @@ -75,17 +71,40 @@ impl<'a> InvoiceBuilder<'a> { }, }; - Ok(Self { - invreq_bytes: &invoice_request.bytes, - invoice: InvoiceContents::ForOffer { - invoice_request: invoice_request.contents.clone(), - fields: InvoiceFields { - payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, - fallbacks: None, features: Bolt12InvoiceFeatures::empty(), - signing_pubkey: invoice_request.contents.offer.signing_pubkey(), - }, + let contents = InvoiceContents::ForOffer { + invoice_request: invoice_request.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats, + fallbacks: None, features: Bolt12InvoiceFeatures::empty(), + signing_pubkey: invoice_request.contents.offer.signing_pubkey(), }, - }) + }; + + Self::new(&invoice_request.bytes, contents) + } + + pub(super) fn for_refund( + refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, signing_pubkey: PublicKey + ) -> Result { + let contents = InvoiceContents::ForRefund { + refund: refund.contents.clone(), + fields: InvoiceFields { + payment_paths, created_at, relative_expiry: None, payment_hash, + amount_msats: refund.amount_msats(), fallbacks: None, + features: Bolt12InvoiceFeatures::empty(), signing_pubkey, + }, + }; + + Self::new(&refund.bytes, contents) + } + + fn new(invreq_bytes: &'a Vec, contents: InvoiceContents) -> Result { + if contents.fields().payment_paths.is_empty() { + return Err(SemanticError::MissingPaths); + } + + Ok(Self { invreq_bytes, invoice: contents }) } /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index fdf763ecb95..e5d8e78f079 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -78,8 +78,10 @@ use core::convert::TryFrom; use core::str::FromStr; use core::time::Duration; use crate::io; +use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder}; use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; @@ -202,8 +204,8 @@ impl RefundBuilder { /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] pub struct Refund { - bytes: Vec, - contents: RefundContents, + pub(super) bytes: Vec, + pub(super) contents: RefundContents, } /// The contents of a [`Refund`], which may be shared with an [`Invoice`]. @@ -296,6 +298,32 @@ impl Refund { self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } + /// Creates an [`Invoice`] for the refund with the given required fields. + /// + /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after + /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to + /// claim a payment for the invoice. + /// + /// The `signing_pubkey` is required to sign the invoice since refunds are not in response to an + /// offer, which does have a `signing_pubkey`. + /// + /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It + /// must contain one or more elements. + /// + /// Errors if the request contains unknown required features. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, signing_pubkey: PublicKey + ) -> Result { + if self.features().requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + + InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) + } + #[cfg(test)] fn as_tlv_stream(&self) -> RefundTlvStreamRef { self.contents.as_tlv_stream() From 7f52d26e6ed3e9ae3c4f413ada3f6c73e8fbc908 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 12 Jan 2023 23:02:39 -0600 Subject: [PATCH 07/11] Use SystemTime::now() for Invoice creation time For std builds, Invoice::created_at can be automatically set upon construction using SystemTime::now() offset by SystemTime::UNIX_EPOCH. Change InvoiceRequest::respond_with and Refund::respond_with to only take a created_at parameter in no-std builds. --- lightning/src/offers/invoice_request.rs | 21 ++++++++++++++++----- lightning/src/offers/refund.rs | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 126d9b552de..4185c95ded9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -57,7 +57,6 @@ use bitcoin::network::constants::Network; use bitcoin::secp256k1::{Message, PublicKey}; use bitcoin::secp256k1::schnorr::Signature; use core::convert::TryFrom; -use core::time::Duration; use crate::io; use crate::ln::PaymentHash; use crate::ln::features::InvoiceRequestFeatures; @@ -326,23 +325,35 @@ impl InvoiceRequest { /// Creates an [`Invoice`] for the request with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to - /// claim a payment for the invoice. + /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter + /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] + /// is not available. + /// + /// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment + /// for the invoice. /// /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It /// must contain one or more elements. /// /// Errors if the request contains unknown required features. /// + /// [`Duration`]: core::time::Duration /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at pub fn respond_with( - &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, - payment_hash: PaymentHash + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + #[cfg(not(feature = "std"))] + created_at: core::time::Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } + #[cfg(feature = "std")] + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index e5d8e78f079..48be9774aec 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -301,7 +301,11 @@ impl Refund { /// Creates an [`Invoice`] for the refund with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// `created_at`. The caller is expected to remember the preimage of `payment_hash` in order to + /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter + /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] + /// is not available. + /// + /// The caller is expected to remember the preimage of `payment_hash` in order to /// claim a payment for the invoice. /// /// The `signing_pubkey` is required to sign the invoice since refunds are not in response to an @@ -313,14 +317,22 @@ impl Refund { /// Errors if the request contains unknown required features. /// /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at pub fn respond_with( - &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, - payment_hash: PaymentHash, signing_pubkey: PublicKey + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + signing_pubkey: PublicKey, + #[cfg(not(feature = "std"))] + created_at: Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } + #[cfg(feature = "std")] + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) } From fe83aede0c7c4e7d9adf098c07f55e52c6e38f63 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 18 Jan 2023 09:54:13 -0600 Subject: [PATCH 08/11] Expand invoice module docs and include an example --- lightning/src/offers/invoice.rs | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 24bf160e56c..eca4796b670 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -8,6 +8,80 @@ // licenses. //! Data structures and encoding for `invoice` messages. +//! +//! An [`Invoice`] can be built from a parsed [`InvoiceRequest`] for the "offer to be paid" flow or +//! from a [`Refund`] as an "offer for money" flow. The expected recipient of the payment then sends +//! the invoice to the intended payer, who will then pay it. +//! +//! The payment recipient must include a [`PaymentHash`], so as to reveal the preimage upon payment +//! receipt, and one or more [`BlindedPath`]s for the payer to use when sending the payment. +//! +//! ```ignore +//! extern crate bitcoin; +//! extern crate lightning; +//! +//! use bitcoin::hashes::Hash; +//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use core::convert::{Infallible, TryFrom}; +//! use lightning::offers::invoice_request::InvoiceRequest; +//! use lightning::offers::refund::Refund; +//! use lightning::util::ser::Writeable; +//! +//! # use lightning::ln::PaymentHash; +//! # use lightning::offers::invoice::BlindedPayInfo; +//! # use lightning::onion_message::BlindedPath; +//! # +//! # fn create_payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { unimplemented!() } +//! # fn create_payment_hash() -> PaymentHash { unimplemented!() } +//! # +//! # fn parse_invoice_request(bytes: Vec) -> Result<(), lightning::offers::parse::ParseError> { +//! let payment_paths = create_payment_paths(); +//! let payment_hash = create_payment_hash(); +//! let secp_ctx = Secp256k1::new(); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let pubkey = PublicKey::from(keys); +//! let wpubkey_hash = bitcoin::util::key::PublicKey::new(pubkey).wpubkey_hash().unwrap(); +//! let mut buffer = Vec::new(); +//! +//! // Invoice for the "offer to be paid" flow. +//! InvoiceRequest::try_from(bytes)? +//! .respond_with(payment_paths, payment_hash)? +//! .relative_expiry(3600) +//! .allow_mpp() +//! .fallback_v0_p2wpkh(&wpubkey_hash) +//! .build()? +//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .expect("failed verifying signature") +//! .write(&mut buffer) +//! .unwrap(); +//! # Ok(()) +//! # } +//! +//! # fn parse_refund(bytes: Vec) -> Result<(), lightning::offers::parse::ParseError> { +//! # let payment_paths = create_payment_paths(); +//! # let payment_hash = create_payment_hash(); +//! # let secp_ctx = Secp256k1::new(); +//! # let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! # let pubkey = PublicKey::from(keys); +//! # let wpubkey_hash = bitcoin::util::key::PublicKey::new(pubkey).wpubkey_hash().unwrap(); +//! # let mut buffer = Vec::new(); +//! +//! // Invoice for the "offer for money" flow. +//! "lnr1qcp4256ypq" +//! .parse::()? +//! .respond_with(payment_paths, payment_hash, pubkey)? +//! .relative_expiry(3600) +//! .allow_mpp() +//! .fallback_v0_p2wpkh(&wpubkey_hash) +//! .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::hash_types::{WPubkeyHash, WScriptHash}; From bf1147f23f95fa52f555b28e300d9f6851caef62 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sun, 15 Jan 2023 22:26:52 -0600 Subject: [PATCH 09/11] Invoice building tests Tests for checking invoice message semantics when building an invoice as defined by BOLT 12. --- lightning/src/offers/invoice.rs | 473 +++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 4 +- lightning/src/offers/refund.rs | 4 +- 3 files changed, 476 insertions(+), 5 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index eca4796b670..1ca2c06cefd 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -447,6 +447,17 @@ impl Invoice { pub fn signature(&self) -> Signature { self.signature } + + #[cfg(test)] + fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef { + let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) = + self.contents.as_tlv_stream(); + let signature_tlv_stream = SignatureTlvStreamRef { + signature: Some(&self.signature), + }; + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + signature_tlv_stream) + } } impl InvoiceContents { @@ -550,7 +561,7 @@ type BlindedPayInfoIter<'a> = core::iter::Map< >; /// Information needed to route a payment across a [`BlindedPath`]. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct BlindedPayInfo { fee_base_msat: u32, fee_proportional_millionths: u32, @@ -581,6 +592,15 @@ impl_writeable!(FallbackAddress, { version, program }); type FullInvoiceTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); +#[cfg(test)] +type FullInvoiceTlvStreamRef<'a> = ( + PayerTlvStreamRef<'a>, + OfferTlvStreamRef<'a>, + InvoiceRequestTlvStreamRef<'a>, + InvoiceTlvStreamRef<'a>, + SignatureTlvStreamRef<'a>, +); + impl SeekReadable for FullInvoiceTlvStream { fn read(r: &mut R) -> Result { let payer = SeekReadable::read(r)?; @@ -704,3 +724,454 @@ impl TryFrom for InvoiceContents { } } } + +#[cfg(test)] +mod tests { + use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; + + use bitcoin::blockdata::script::Script; + use bitcoin::hashes::Hash; + use bitcoin::network::constants::Network; + use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, self}; + use bitcoin::secp256k1::schnorr::Signature; + use bitcoin::util::address::{Address, WitnessVersion}; + use bitcoin::util::schnorr::TweakedPublicKey; + use core::convert::{Infallible, TryFrom}; + use core::time::Duration; + use crate::ln::PaymentHash; + use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; + use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; + use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef}; + use crate::offers::parse::SemanticError; + use crate::offers::payer::PayerTlvStreamRef; + use crate::offers::refund::RefundBuilder; + use crate::onion_message::{BlindedHop, BlindedPath}; + use crate::util::ser::{Iterable, Writeable}; + + 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_keys() -> KeyPair { + let secp_ctx = Secp256k1::new(); + KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) + } + + 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 { + recipient_keys().public_key() + } + + fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) + } + + fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() + } + + fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { + let paths = vec![ + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }, + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }, + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + paths.into_iter().zip(payinfo.into_iter()).collect() + } + + fn payment_hash() -> PaymentHash { + PaymentHash([42; 32]) + } + + fn now() -> Duration { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") + } + + #[test] + fn builds_invoice_for_offer_with_defaults() { + let payment_paths = payment_paths(); + let payment_hash = payment_hash(); + let now = now(); + let invoice = 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() + .respond_with(payment_paths.clone(), payment_hash, now).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + assert_eq!(invoice.bytes, buffer.as_slice()); + assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); + assert_eq!(invoice.created_at(), now); + assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); + #[cfg(feature = "std")] + assert!(!invoice.is_expired()); + assert_eq!(invoice.payment_hash(), payment_hash); + assert_eq!(invoice.amount_msats(), 1000); + assert_eq!(invoice.fallbacks(), vec![]); + assert_eq!(invoice.features(), &Bolt12InvoiceFeatures::empty()); + assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); + assert!( + merkle::verify_signature( + &invoice.signature, SIGNATURE_TAG, &invoice.bytes, recipient_pubkey() + ).is_ok() + ); + + assert_eq!( + invoice.as_tlv_stream(), + ( + PayerTlvStreamRef { metadata: Some(&vec![1; 32]) }, + OfferTlvStreamRef { + chains: None, + metadata: None, + currency: None, + amount: Some(1000), + description: Some(&String::from("foo")), + features: None, + absolute_expiry: None, + paths: None, + issuer: None, + quantity_max: None, + node_id: Some(&recipient_pubkey()), + }, + InvoiceRequestTlvStreamRef { + chain: None, + amount: None, + features: None, + quantity: None, + payer_id: Some(&payer_pubkey()), + payer_note: None, + }, + InvoiceTlvStreamRef { + paths: Some(Iterable(payment_paths.iter().map(|(path, _)| path))), + blindedpay: Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))), + created_at: Some(now.as_secs()), + relative_expiry: None, + payment_hash: Some(&payment_hash), + amount: Some(1000), + fallbacks: None, + features: None, + node_id: Some(&recipient_pubkey()), + }, + SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ), + ); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + } + + #[test] + fn builds_invoice_for_refund_with_defaults() { + let payment_paths = payment_paths(); + let payment_hash = payment_hash(); + let now = now(); + let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .build().unwrap() + .respond_with(payment_paths.clone(), payment_hash, recipient_pubkey(), now).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + assert_eq!(invoice.bytes, buffer.as_slice()); + assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); + assert_eq!(invoice.created_at(), now); + assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); + #[cfg(feature = "std")] + assert!(!invoice.is_expired()); + assert_eq!(invoice.payment_hash(), payment_hash); + assert_eq!(invoice.amount_msats(), 1000); + assert_eq!(invoice.fallbacks(), vec![]); + assert_eq!(invoice.features(), &Bolt12InvoiceFeatures::empty()); + assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); + assert!( + merkle::verify_signature( + &invoice.signature, SIGNATURE_TAG, &invoice.bytes, recipient_pubkey() + ).is_ok() + ); + + assert_eq!( + invoice.as_tlv_stream(), + ( + PayerTlvStreamRef { metadata: Some(&vec![1; 32]) }, + OfferTlvStreamRef { + chains: None, + metadata: None, + currency: None, + amount: None, + description: Some(&String::from("foo")), + features: None, + absolute_expiry: None, + paths: None, + issuer: None, + quantity_max: None, + node_id: None, + }, + InvoiceRequestTlvStreamRef { + chain: None, + amount: Some(1000), + features: None, + quantity: None, + payer_id: Some(&payer_pubkey()), + payer_note: None, + }, + InvoiceTlvStreamRef { + paths: Some(Iterable(payment_paths.iter().map(|(path, _)| path))), + blindedpay: Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))), + created_at: Some(now.as_secs()), + relative_expiry: None, + payment_hash: Some(&payment_hash), + amount: Some(1000), + fallbacks: None, + features: None, + node_id: Some(&recipient_pubkey()), + }, + SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, + ), + ); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + } + + #[cfg(feature = "std")] + #[test] + fn builds_invoice_from_refund_with_expiration() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .absolute_expiry(future_expiry) + .build().unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .build() + { + panic!("error building invoice: {:?}", e); + } + + match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + .absolute_expiry(past_expiry) + .build().unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::AlreadyExpired), + } + } + + #[test] + fn builds_invoice_with_relative_expiry() { + let now = now(); + let one_hour = Duration::from_secs(3600); + + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now).unwrap() + .relative_expiry(one_hour.as_secs() as u32) + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + #[cfg(feature = "std")] + assert!(!invoice.is_expired()); + assert_eq!(invoice.relative_expiry(), one_hour); + assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32)); + + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now - one_hour).unwrap() + .relative_expiry(one_hour.as_secs() as u32 - 1) + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + #[cfg(feature = "std")] + assert!(invoice.is_expired()); + assert_eq!(invoice.relative_expiry(), one_hour - Duration::from_secs(1)); + assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32 - 1)); + } + + #[test] + fn builds_invoice_with_amount_from_request() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.amount_msats(), 1001); + assert_eq!(tlv_stream.amount, Some(1001)); + } + + #[test] + fn builds_invoice_with_fallback_address() { + let script = Script::new(); + let pubkey = bitcoin::util::key::PublicKey::new(recipient_pubkey()); + let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); + + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .fallback_v0_p2wsh(&script.wscript_hash()) + .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) + .fallback_v1_p2tr_tweaked(&tweaked_pubkey) + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!( + invoice.fallbacks(), + vec![ + Address::p2wsh(&script, Network::Bitcoin), + Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(), + Address::p2tr_tweaked(tweaked_pubkey, Network::Bitcoin), + ], + ); + assert_eq!( + tlv_stream.fallbacks, + Some(&vec![ + FallbackAddress { + version: WitnessVersion::V0.to_num(), + program: Vec::from(&script.wscript_hash().into_inner()[..]), + }, + FallbackAddress { + version: WitnessVersion::V0.to_num(), + program: Vec::from(&pubkey.wpubkey_hash().unwrap().into_inner()[..]), + }, + FallbackAddress { + version: WitnessVersion::V1.to_num(), + program: Vec::from(&tweaked_pubkey.serialize()[..]), + }, + ]) + ); + } + + #[test] + fn builds_invoice_with_allow_mpp() { + let mut features = Bolt12InvoiceFeatures::empty(); + features.set_basic_mpp_optional(); + + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .allow_mpp() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.features(), &features); + assert_eq!(tlv_stream.features, Some(&features)); + } + + #[test] + fn fails_signing_invoice() { + match 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() + .respond_with(payment_paths(), payment_hash(), now()).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(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(payer_sign) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SignError::Verification(secp256k1::Error::InvalidSignature)), + } + } +} diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 4185c95ded9..e863ef6cd61 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -342,14 +342,14 @@ impl InvoiceRequest { /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at pub fn respond_with( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, - #[cfg(not(feature = "std"))] + #[cfg(any(test, not(feature = "std")))] created_at: core::time::Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(feature = "std")] + #[cfg(all(not(test), feature = "std"))] let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 48be9774aec..bee6cc7f5f6 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -321,14 +321,14 @@ impl Refund { pub fn respond_with( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, signing_pubkey: PublicKey, - #[cfg(not(feature = "std"))] + #[cfg(any(test, not(feature = "std")))] created_at: Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(feature = "std")] + #[cfg(all(not(test), feature = "std"))] let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); From a452551480410820a25718a79bf399a9311e712a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sun, 15 Jan 2023 23:59:49 -0600 Subject: [PATCH 10/11] Invoice parsing tests Tests for checking invoice semantics when parsing invoice bytes as defined by BOLT 12. --- lightning/src/offers/invoice.rs | 398 +++++++++++++++++++++++++++++++- lightning/src/util/ser.rs | 18 ++ 2 files changed, 411 insertions(+), 5 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 1ca2c06cefd..99a97368d92 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -529,6 +529,12 @@ impl Writeable for Invoice { } } +impl Writeable for InvoiceContents { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.as_tlv_stream().write(writer) + } +} + impl TryFrom> for Invoice { type Error = ParseError; @@ -581,7 +587,7 @@ impl_writeable!(BlindedPayInfo, { }); /// Wire representation for an on-chain fallback address. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct FallbackAddress { version: u8, program: Vec, @@ -727,27 +733,28 @@ impl TryFrom for InvoiceContents { #[cfg(test)] mod tests { - use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; + use super::{DEFAULT_RELATIVE_EXPIRY, BlindedPayInfo, FallbackAddress, FullInvoiceTlvStreamRef, Invoice, InvoiceTlvStreamRef, SIGNATURE_TAG}; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, self}; use bitcoin::secp256k1::schnorr::Signature; - use bitcoin::util::address::{Address, WitnessVersion}; + use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; use core::convert::{Infallible, TryFrom}; use core::time::Duration; use crate::ln::PaymentHash; + use crate::ln::msgs::DecodeError; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef}; - use crate::offers::parse::SemanticError; + use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; use crate::onion_message::{BlindedHop, BlindedPath}; - use crate::util::ser::{Iterable, Writeable}; + use crate::util::ser::{BigSize, Iterable, Writeable}; fn payer_keys() -> KeyPair { let secp_ctx = Secp256k1::new(); @@ -788,6 +795,22 @@ mod tests { SecretKey::from_slice(&[byte; 32]).unwrap() } + trait ToBytes { + fn to_bytes(&self) -> Vec; + } + + impl<'a> ToBytes for FullInvoiceTlvStreamRef<'a> { + fn to_bytes(&self) -> Vec { + let mut buffer = Vec::new(); + self.0.write(&mut buffer).unwrap(); + self.1.write(&mut buffer).unwrap(); + self.2.write(&mut buffer).unwrap(); + self.3.write(&mut buffer).unwrap(); + self.4.write(&mut buffer).unwrap(); + buffer + } + } + fn payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { let paths = vec![ BlindedPath { @@ -1174,4 +1197,369 @@ mod tests { Err(e) => assert_eq!(e, SignError::Verification(secp256k1::Error::InvalidSignature)), } } + + #[test] + fn parses_invoice_with_payment_paths() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.paths = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.blindedpay = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)), + } + + let empty_payment_paths = vec![]; + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.paths = Some(Iterable(empty_payment_paths.iter().map(|(path, _)| path))); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + + let mut payment_paths = payment_paths(); + payment_paths.pop(); + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.blindedpay = Some(Iterable(payment_paths.iter().map(|(_, payinfo)| payinfo))); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidPayInfo)), + } + } + + #[test] + fn parses_invoice_with_created_at() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.created_at = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingCreationTime)); + }, + } + } + + #[test] + fn parses_invoice_with_relative_expiry() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .relative_expiry(3600) + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => assert_eq!(invoice.relative_expiry(), Duration::from_secs(3600)), + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_payment_hash() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.payment_hash = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaymentHash)); + }, + } + } + + #[test] + fn parses_invoice_with_amount() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.amount = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)), + } + } + + #[test] + fn parses_invoice_with_allow_mpp() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .allow_mpp() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => { + let mut features = Bolt12InvoiceFeatures::empty(); + features.set_basic_mpp_optional(); + assert_eq!(invoice.features(), &features); + }, + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_fallback_address() { + let script = Script::new(); + let pubkey = bitcoin::util::key::PublicKey::new(recipient_pubkey()); + let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; + let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); + + 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 mut unsigned_invoice = invoice_request + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .fallback_v0_p2wsh(&script.wscript_hash()) + .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) + .fallback_v1_p2tr_tweaked(&tweaked_pubkey) + .build().unwrap(); + + // Only standard addresses will be included. + let mut fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + // Non-standard addresses + fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] }); + fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] }); + fallbacks.push(FallbackAddress { version: 17, program: vec![0u8; 40] }); + // Standard address + fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 33] }); + fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 40] }); + + let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(invoice) => { + assert_eq!( + invoice.fallbacks(), + vec![ + Address::p2wsh(&script, Network::Bitcoin), + Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(), + Address::p2tr_tweaked(tweaked_pubkey, Network::Bitcoin), + Address { + payload: Payload::WitnessProgram { + version: WitnessVersion::V1, + program: vec![0u8; 33], + }, + network: Network::Bitcoin, + }, + Address { + payload: Payload::WitnessProgram { + version: WitnessVersion::V2, + program: vec![0u8; 40], + }, + network: Network::Bitcoin, + }, + ], + ); + }, + Err(e) => panic!("error parsing invoice: {:?}", e), + } + } + + #[test] + fn parses_invoice_with_node_id() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.node_id = None; + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey)); + }, + } + + let invalid_pubkey = payer_pubkey(); + let mut tlv_stream = invoice.as_tlv_stream(); + tlv_stream.3.node_id = Some(&invalid_pubkey); + + match Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidSigningPubkey)); + }, + } + } + + #[test] + fn fails_parsing_invoice_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() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .invoice + .write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSignature)), + } + } + + #[test] + fn fails_parsing_invoice_with_invalid_signature() { + let mut invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let last_signature_byte = invoice.bytes.last_mut().unwrap(); + *last_signature_byte = last_signature_byte.wrapping_add(1); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSignature(secp256k1::Error::InvalidSignature)); + }, + } + } + + #[test] + fn fails_parsing_invoice_with_extra_tlv_records() { + let invoice = 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() + .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut encoded_invoice = Vec::new(); + invoice.write(&mut encoded_invoice).unwrap(); + BigSize(1002).write(&mut encoded_invoice).unwrap(); + BigSize(32).write(&mut encoded_invoice).unwrap(); + [42u8; 32].write(&mut encoded_invoice).unwrap(); + + match Invoice::try_from(encoded_invoice) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index ebe20677cda..928cc61946e 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1085,6 +1085,24 @@ impl Writeable for (A, B, C) { } } +impl Readable for (A, B, C, D) { + fn read(r: &mut R) -> Result { + let a: A = Readable::read(r)?; + let b: B = Readable::read(r)?; + let c: C = Readable::read(r)?; + let d: D = Readable::read(r)?; + Ok((a, b, c, d)) + } +} +impl Writeable for (A, B, C, D) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w)?; + self.1.write(w)?; + self.2.write(w)?; + self.3.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) From 15f12953b2b0931d8bbb03fd341679b72b8e79dd Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 18 Jan 2023 22:14:35 -0600 Subject: [PATCH 11/11] Fix unused_imports warning in no-std tests --- lightning/src/routing/gossip.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 1a5b978502c..39f07914a61 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -1935,6 +1935,7 @@ mod tests { use crate::chain; use crate::ln::channelmanager; use crate::ln::chan_utils::make_funding_redeemscript; + #[cfg(feature = "std")] use crate::ln::features::InitFeatures; use crate::routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate, NodeAlias, MAX_EXCESS_BYTES_FOR_RELAY, NodeId, RoutingFees, ChannelUpdateInfo, ChannelInfo, NodeAnnouncementInfo, NodeInfo}; use crate::ln::msgs::{RoutingMessageHandler, UnsignedNodeAnnouncement, NodeAnnouncement,