From 9c708df2ff52335eb10f01e40477ecd5434d4319 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sat, 25 Feb 2023 22:36:57 -0600 Subject: [PATCH 01/16] Tidy up bitcoin::secp256k1 imports --- lightning/src/sign/mod.rs | 5 ++--- lightning/src/util/test_utils.rs | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index a71bdae8876..eb0b9cb6c6b 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -26,10 +26,9 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::WPubkeyHash; -use bitcoin::secp256k1::{SecretKey, PublicKey, Scalar}; -use bitcoin::secp256k1::{Secp256k1, ecdsa::Signature, Signing}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::secp256k1::ecdh::SharedSecret; -use bitcoin::secp256k1::ecdsa::RecoverableSignature; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::{PackedLockTime, secp256k1, Sequence, Witness}; use crate::util::transaction_utils; diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 65c0483a59c..187abe19ffc 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -44,9 +44,9 @@ use bitcoin::network::constants::Network; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::util::sighash::SighashCache; -use bitcoin::secp256k1::{SecretKey, PublicKey, Secp256k1, ecdsa::Signature, Scalar}; +use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::secp256k1::ecdh::SharedSecret; -use bitcoin::secp256k1::ecdsa::RecoverableSignature; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; #[cfg(any(test, feature = "_test_utils"))] use regex; From 4bb4a970e8ca01b0228939206f5df38e7538c813 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 15 Jun 2023 17:14:10 -0500 Subject: [PATCH 02/16] Make offers::merkle::SignError visible --- lightning/src/offers/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 31d8bf9cbdf..d97632a78cc 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -15,7 +15,7 @@ pub mod invoice; pub mod invoice_error; pub mod invoice_request; -mod merkle; +pub mod merkle; pub mod offer; pub mod parse; mod payer; From 1811ebff32e5f32aaf80e9f14bfeef12a47a35c6 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 27 Feb 2023 14:23:05 -0600 Subject: [PATCH 03/16] TaggedHash for BOLT 12 signing function The function used to sign BOLT 12 messages only takes a message digest. This doesn't allow signers to independently verify the message before signing nor does it allow them to derive the necessary signing keys, if needed. Introduce a TaggedHash wrapper for a message digest, which each unsigned BOLT 12 message type constructs upon initialization. Change the signing function to take AsRef, which each unsigned type implements. This allows the signing function to take any unsigned message and obtain its tagged hash. --- fuzz/src/invoice_request_deser.rs | 10 +-- fuzz/src/offer_deser.rs | 8 +- fuzz/src/refund_deser.rs | 8 +- lightning/src/offers/invoice.rs | 85 +++++++++++-------- lightning/src/offers/invoice_request.rs | 108 +++++++++++++----------- lightning/src/offers/merkle.rs | 68 ++++++++++++--- lightning/src/offers/test_utils.rs | 13 +-- 7 files changed, 187 insertions(+), 113 deletions(-) diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index ca9d06ab1f8..22a2258f4e2 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -38,7 +38,7 @@ pub fn do_test(data: &[u8], _out: Out) { if signing_pubkey == odd_pubkey || signing_pubkey == even_pubkey { unsigned_invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -46,7 +46,7 @@ pub fn do_test(data: &[u8], _out: Out) { } else { unsigned_invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap_err(); } @@ -69,9 +69,9 @@ fn privkey(byte: u8) -> SecretKey { SecretKey::from_slice(&[byte; 32]).unwrap() } -fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( - invoice_request: &'a InvoiceRequest, secp_ctx: &Secp256k1 -) -> Result, Bolt12SemanticError> { +fn build_response( + invoice_request: &InvoiceRequest, secp_ctx: &Secp256k1 +) -> Result { let entropy_source = Randomness {}; let paths = vec![ BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs index 53f67a3380d..e16c3b4103b 100644 --- a/fuzz/src/offer_deser.rs +++ b/fuzz/src/offer_deser.rs @@ -30,7 +30,7 @@ pub fn do_test(data: &[u8], _out: Out) { if let Ok(invoice_request) = build_response(&offer, pubkey) { invoice_request .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -39,9 +39,9 @@ pub fn do_test(data: &[u8], _out: Out) { } } -fn build_response<'a>( - offer: &'a Offer, pubkey: PublicKey -) -> Result, Bolt12SemanticError> { +fn build_response( + offer: &Offer, pubkey: PublicKey +) -> Result { let mut builder = offer.request_invoice(vec![42; 64], pubkey)?; builder = match offer.amount() { diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs index 81b614d602b..fd273d7e028 100644 --- a/fuzz/src/refund_deser.rs +++ b/fuzz/src/refund_deser.rs @@ -34,7 +34,7 @@ pub fn do_test(data: &[u8], _out: Out) { if let Ok(invoice) = build_response(&refund, pubkey, &secp_ctx) { invoice .sign::<_, Infallible>( - |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) ) .unwrap() .write(&mut buffer) @@ -58,9 +58,9 @@ fn privkey(byte: u8) -> SecretKey { SecretKey::from_slice(&[byte; 32]).unwrap() } -fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( - refund: &'a Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 -) -> Result, Bolt12SemanticError> { +fn build_response( + refund: &Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 +) -> Result { let entropy_source = Randomness {}; let paths = vec![ BlindedPath::new_for_message(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index c3d4500aaeb..773e1ead26c 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -55,7 +55,9 @@ //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -84,7 +86,9 @@ //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -97,11 +101,11 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; use bitcoin::util::address::{Address, Payload, WitnessVersion}; use bitcoin::util::schnorr::TweakedPublicKey; -use core::convert::{Infallible, TryFrom}; +use core::convert::{AsRef, Infallible, TryFrom}; use core::time::Duration; use crate::io; use crate::blinded_path::BlindedPath; @@ -110,7 +114,7 @@ use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; @@ -126,7 +130,8 @@ use std::time::SystemTime; const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); -pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); +/// Tag for the hash function used when signing a [`Bolt12Invoice`]'s merkle root. +pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); /// Builds a [`Bolt12Invoice`] from either: /// - an [`InvoiceRequest`] for the "offer to be paid" flow or @@ -331,7 +336,7 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { /// Builds an unsigned [`Bolt12Invoice`] after checking for valid semantics. It can be signed by /// [`UnsignedBolt12Invoice::sign`]. - pub fn build(self) -> Result, Bolt12SemanticError> { + pub fn build(self) -> Result { #[cfg(feature = "std")] { if self.invoice.is_offer_or_refund_expired() { return Err(Bolt12SemanticError::AlreadyExpired); @@ -339,7 +344,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, .. } = self; - Ok(UnsignedBolt12Invoice { invreq_bytes, invoice }) + Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice)) } } @@ -355,23 +360,42 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; - let unsigned_invoice = UnsignedBolt12Invoice { invreq_bytes, invoice }; + let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); let keys = keys.unwrap(); let invoice = unsigned_invoice - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); Ok(invoice) } } /// A semantically valid [`Bolt12Invoice`] that hasn't been signed. -pub struct UnsignedBolt12Invoice<'a> { - invreq_bytes: &'a Vec, +pub struct UnsignedBolt12Invoice { + bytes: Vec, invoice: InvoiceContents, + tagged_hash: TaggedHash, } -impl<'a> UnsignedBolt12Invoice<'a> { +impl UnsignedBolt12Invoice { + fn new(invreq_bytes: &[u8], invoice: InvoiceContents) -> Self { + // 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) = invoice.as_tlv_stream(); + let invoice_request_bytes = WithoutSignatures(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 tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Self { bytes, invoice, tagged_hash } + } + /// The public key corresponding to the key needed to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { self.invoice.fields().signing_pubkey @@ -380,37 +404,33 @@ impl<'a> UnsignedBolt12Invoice<'a> { /// Signs the invoice using the given function. /// /// This is not exported to bindings users as functions aren't currently mapped. - pub fn sign(self, sign: F) -> Result> + pub fn sign(mut self, sign: F) -> Result> where - F: FnOnce(&Message) -> Result + F: FnOnce(&Self) -> 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)?; + let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature), }; - signature_tlv_stream.write(&mut bytes).unwrap(); + signature_tlv_stream.write(&mut self.bytes).unwrap(); Ok(Bolt12Invoice { - bytes, + bytes: self.bytes, contents: self.invoice, signature, }) } } +impl AsRef for UnsignedBolt12Invoice { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + /// A `Bolt12Invoice` 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 @@ -1686,15 +1706,14 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - let mut unsigned_invoice = invoice_request + let mut invoice_builder = invoice_request .respond_with_no_std(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(); + .fallback_v1_p2tr_tweaked(&tweaked_pubkey); // Only standard addresses will be included. - let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + let fallbacks = invoice_builder.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] }); @@ -1703,7 +1722,7 @@ mod tests { 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 invoice = invoice_builder.build().unwrap().sign(recipient_sign).unwrap(); let mut buffer = Vec::new(); invoice.write(&mut buffer).unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index f014bf12002..1dea6503f58 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -44,7 +44,9 @@ //! .quantity(5)? //! .payer_note("foo".to_string()) //! .build()? -//! .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) +//! .sign::<_, Infallible>( +//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! ) //! .expect("failed verifying signature") //! .write(&mut buffer) //! .unwrap(); @@ -54,9 +56,9 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; -use core::convert::{Infallible, TryFrom}; +use core::convert::{AsRef, Infallible, TryFrom}; use core::ops::Deref; use crate::sign::EntropySource; use crate::io; @@ -66,7 +68,7 @@ use crate::ln::features::InvoiceRequestFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef}; use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; @@ -76,7 +78,8 @@ use crate::util::string::PrintableString; use crate::prelude::*; -const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); +/// Tag for the hash function used when signing an [`InvoiceRequest`]'s merkle root. +pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~"; @@ -214,7 +217,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a } fn build_with_checks(mut self) -> Result< - (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>), + (UnsignedInvoiceRequest, Option, Option<&'b Secp256k1>), Bolt12SemanticError > { #[cfg(feature = "std")] { @@ -245,7 +248,7 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a } fn build_without_checks(mut self) -> - (UnsignedInvoiceRequest<'a>, Option, Option<&'b Secp256k1>) + (UnsignedInvoiceRequest, Option, Option<&'b Secp256k1>) { // Create the metadata for stateless verification of a Bolt12Invoice. let mut keys = None; @@ -275,22 +278,20 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a debug_assert!(self.payer_id.is_some()); let payer_id = self.payer_id.unwrap(); - let unsigned_invoice = UnsignedInvoiceRequest { - offer: self.offer, - invoice_request: InvoiceRequestContents { - inner: self.invoice_request, - payer_id, - }, + let invoice_request = InvoiceRequestContents { + inner: self.invoice_request, + payer_id, }; + let unsigned_invoice_request = UnsignedInvoiceRequest::new(self.offer, invoice_request); - (unsigned_invoice, keys, secp_ctx) + (unsigned_invoice_request, keys, secp_ctx) } } impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> { /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed /// by [`UnsignedInvoiceRequest::sign`]. - pub fn build(self) -> Result, Bolt12SemanticError> { + pub fn build(self) -> Result { let (unsigned_invoice_request, keys, _) = self.build_with_checks()?; debug_assert!(keys.is_none()); Ok(unsigned_invoice_request) @@ -306,7 +307,9 @@ impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId let secp_ctx = secp_ctx.unwrap(); let keys = keys.unwrap(); let invoice_request = unsigned_invoice_request - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); Ok(invoice_request) } @@ -335,52 +338,65 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a self } - pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> { + pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest { self.build_without_checks().0 } } /// A semantically valid [`InvoiceRequest`] that hasn't been signed. -pub struct UnsignedInvoiceRequest<'a> { - offer: &'a Offer, +pub struct UnsignedInvoiceRequest { + bytes: Vec, invoice_request: InvoiceRequestContents, + tagged_hash: TaggedHash, } -impl<'a> UnsignedInvoiceRequest<'a> { - /// Signs the invoice request using the given function. - /// - /// This is not exported to bindings users as functions are not yet mapped. - pub fn sign(self, sign: F) -> Result> - where - F: FnOnce(&Message) -> Result - { +impl UnsignedInvoiceRequest { + fn new(offer: &Offer, invoice_request: InvoiceRequestContents) -> Self { // Use the offer bytes instead of the offer TLV stream as the offer may have contained // unknown TLV records, which are not stored in `OfferContents`. let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = - self.invoice_request.as_tlv_stream(); - let offer_bytes = WithoutLength(&self.offer.bytes); + invoice_request.as_tlv_stream(); + let offer_bytes = WithoutLength(&offer.bytes); let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); let mut bytes = Vec::new(); unsigned_tlv_stream.write(&mut bytes).unwrap(); + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Self { bytes, invoice_request, tagged_hash } + } + + /// Signs the invoice request using the given function. + /// + /// This is not exported to bindings users as functions are not yet mapped. + pub fn sign(mut self, sign: F) -> Result> + where + F: FnOnce(&Self) -> Result + { let pubkey = self.invoice_request.payer_id; - let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?; + let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature), }; - signature_tlv_stream.write(&mut bytes).unwrap(); + signature_tlv_stream.write(&mut self.bytes).unwrap(); Ok(InvoiceRequest { - bytes, + bytes: self.bytes, contents: self.invoice_request, signature, }) } } +impl AsRef for UnsignedInvoiceRequest { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + /// An `InvoiceRequest` is a request for a [`Bolt12Invoice`] formulated from an [`Offer`]. /// /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request @@ -591,7 +607,7 @@ impl InvoiceRequest { } impl InvoiceRequestContents { - pub fn metadata(&self) -> &[u8] { + pub(super) fn metadata(&self) -> &[u8] { self.inner.metadata() } @@ -790,7 +806,7 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; - use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; @@ -922,9 +938,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -946,9 +961,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -992,9 +1006,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -1016,9 +1029,8 @@ mod tests { let mut bytes = Vec::new(); tlv_stream.write(&mut bytes).unwrap(); - let signature = merkle::sign_message( - recipient_sign, INVOICE_SIGNATURE_TAG, &bytes, recipient_pubkey() - ).unwrap(); + let message = TaggedHash::new(INVOICE_SIGNATURE_TAG, &bytes); + let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap(); signature_tlv_stream.signature = Some(&signature); let mut encoded_invoice = bytes; @@ -1771,7 +1783,9 @@ mod tests { .build().unwrap() .request_invoice(vec![1; 32], keys.public_key()).unwrap() .build().unwrap() - .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) .unwrap(); let mut encoded_invoice_request = Vec::new(); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index f7c33902c51..d15039cd317 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -12,6 +12,7 @@ use bitcoin::hashes::{Hash, HashEngine, sha256}; use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; +use core::convert::AsRef; use crate::io; use crate::util::ser::{BigSize, Readable, Writeable, Writer}; @@ -24,6 +25,33 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { (240, signature: Signature), }); +/// A hash for use in a specific context by tweaking with a context-dependent tag as per [BIP 340] +/// and computed over the merkle root of a TLV stream to sign as defined in [BOLT 12]. +/// +/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki +/// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation +pub struct TaggedHash(Message); + +impl TaggedHash { + /// Creates a tagged hash with the given parameters. + /// + /// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record. + pub(super) fn new(tag: &str, tlv_stream: &[u8]) -> Self { + Self(message_digest(tag, tlv_stream)) + } + + /// Returns the digest to sign. + pub fn as_digest(&self) -> &Message { + &self.0 + } +} + +impl AsRef for TaggedHash { + fn as_ref(&self) -> &TaggedHash { + self + } +} + /// Error when signing messages. #[derive(Debug, PartialEq)] pub enum SignError { @@ -33,22 +61,28 @@ pub enum SignError { Verification(secp256k1::Error), } -/// Signs a message digest consisting of a tagged hash of the given bytes, checking if it can be -/// verified with the supplied pubkey. +/// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream, checking if it +/// can be verified with the supplied `pubkey`. /// -/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. -pub(super) fn sign_message( - sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +/// Since `message` is any type that implements [`AsRef`], `sign` may be a closure that +/// takes a message such as [`Bolt12Invoice`] or [`InvoiceRequest`]. This allows further message +/// verification before signing its [`TaggedHash`]. +/// +/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub(super) fn sign_message( + sign: F, message: &T, pubkey: PublicKey, ) -> Result> where - F: FnOnce(&Message) -> Result + F: FnOnce(&T) -> Result, + T: AsRef, { - let digest = message_digest(tag, bytes); - let signature = sign(&digest).map_err(|e| SignError::Signing(e))?; + let signature = sign(message).map_err(|e| SignError::Signing(e))?; + let digest = message.as_ref().as_digest(); let pubkey = pubkey.into(); let secp_ctx = Secp256k1::verification_only(); - secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?; + secp_ctx.verify_schnorr(&signature, digest, &pubkey).map_err(|e| SignError::Verification(e))?; Ok(signature) } @@ -207,12 +241,12 @@ 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); +pub(super) struct WithoutSignatures<'a>(pub &'a [u8]); impl<'a> Writeable for WithoutSignatures<'a> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { - let tlv_stream = TlvStream::new(&self.0[..]); + let tlv_stream = TlvStream::new(self.0); for record in tlv_stream.skip_signatures() { writer.write_all(record.record_bytes)?; } @@ -271,7 +305,9 @@ mod tests { .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))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); assert_eq!( invoice_request.to_string(), @@ -304,7 +340,9 @@ mod tests { .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))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); let mut bytes_without_signature = Vec::new(); @@ -334,7 +372,9 @@ mod tests { .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))) + .sign::<_, Infallible>( + |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &payer_keys)) + ) .unwrap(); let tlv_stream = TlvStream::new(&invoice_request.bytes).range(0..1) diff --git a/lightning/src/offers/test_utils.rs b/lightning/src/offers/test_utils.rs index 230c6aa1628..f1b3c79edc0 100644 --- a/lightning/src/offers/test_utils.rs +++ b/lightning/src/offers/test_utils.rs @@ -9,25 +9,26 @@ //! Utilities for testing BOLT 12 Offers interfaces -use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; use bitcoin::secp256k1::schnorr::Signature; -use core::convert::Infallible; +use core::convert::{AsRef, Infallible}; use core::time::Duration; use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::sign::EntropySource; use crate::ln::PaymentHash; use crate::ln::features::BlindedHopFeatures; use crate::offers::invoice::BlindedPayInfo; +use crate::offers::merkle::TaggedHash; pub(super) fn payer_keys() -> KeyPair { let secp_ctx = Secp256k1::new(); KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()) } -pub(super) fn payer_sign(digest: &Message) -> Result { +pub(super) fn payer_sign>(message: &T) -> 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)) + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) } pub(super) fn payer_pubkey() -> PublicKey { @@ -39,10 +40,10 @@ pub(super) fn recipient_keys() -> KeyPair { KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) } -pub(super) fn recipient_sign(digest: &Message) -> Result { +pub(super) fn recipient_sign>(message: &T) -> 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)) + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) } pub(super) fn recipient_pubkey() -> PublicKey { From 239f22c0c9ea216919ef1ed34d00438fe9ca619d Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 11 Aug 2023 13:11:14 -0500 Subject: [PATCH 04/16] Wrap KeyPair by DerivedSigningPubkey InvoiceBuilder is parameterized by a SigningPubkeyStrategy, either ExplicitSigningPubkey and DerivedSigningPubkey. It also holds an Option, which may be None and Some for those strategies, respectively. This leads to methods for InvoiceBuilder parameterized by DerivedSigningPubkey needing to blindly unwrap the Option. Instead, have DerivedSigningPubkey wrap KeyPair. --- lightning/src/offers/invoice.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 773e1ead26c..5442a072f37 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -147,8 +147,7 @@ pub const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signatu pub struct InvoiceBuilder<'a, S: SigningPubkeyStrategy> { invreq_bytes: &'a Vec, invoice: InvoiceContents, - keys: Option, - signing_pubkey_strategy: core::marker::PhantomData, + signing_pubkey_strategy: S, } /// Indicates how [`Bolt12Invoice::signing_pubkey`] was set. @@ -164,7 +163,7 @@ pub struct ExplicitSigningPubkey {} /// [`Bolt12Invoice::signing_pubkey`] was derived. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. -pub struct DerivedSigningPubkey {} +pub struct DerivedSigningPubkey(KeyPair); impl SigningPubkeyStrategy for ExplicitSigningPubkey {} impl SigningPubkeyStrategy for DerivedSigningPubkey {} @@ -183,7 +182,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { ), }; - Self::new(&invoice_request.bytes, contents, None) + Self::new(&invoice_request.bytes, contents, ExplicitSigningPubkey {}) } pub(super) fn for_refund( @@ -198,7 +197,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { ), }; - Self::new(&refund.bytes, contents, None) + Self::new(&refund.bytes, contents, ExplicitSigningPubkey {}) } } @@ -216,7 +215,7 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { ), }; - Self::new(&invoice_request.bytes, contents, Some(keys)) + Self::new(&invoice_request.bytes, contents, DerivedSigningPubkey(keys)) } pub(super) fn for_refund_using_keys( @@ -232,7 +231,7 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { ), }; - Self::new(&refund.bytes, contents, Some(keys)) + Self::new(&refund.bytes, contents, DerivedSigningPubkey(keys)) } } @@ -262,18 +261,13 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { } fn new( - invreq_bytes: &'a Vec, contents: InvoiceContents, keys: Option + invreq_bytes: &'a Vec, contents: InvoiceContents, signing_pubkey_strategy: S ) -> Result { if contents.fields().payment_paths.is_empty() { return Err(Bolt12SemanticError::MissingPaths); } - Ok(Self { - invreq_bytes, - invoice: contents, - keys, - signing_pubkey_strategy: core::marker::PhantomData, - }) + Ok(Self { invreq_bytes, invoice: contents, signing_pubkey_strategy }) } /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. @@ -359,10 +353,11 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } } - let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; + let InvoiceBuilder { + invreq_bytes, invoice, signing_pubkey_strategy: DerivedSigningPubkey(keys) + } = self; let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); - let keys = keys.unwrap(); let invoice = unsigned_invoice .sign::<_, Infallible>( |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) From 889848d5edb6dc53aa2aac999e54f1a80b7c618f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 11 Aug 2023 21:56:21 -0500 Subject: [PATCH 05/16] Rename field of unsigned BOLT message contents Using `contents` for the field name is more consistent with the signed messages. --- lightning/src/offers/invoice.rs | 16 ++++++++-------- lightning/src/offers/invoice_request.rs | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 5442a072f37..bb62e71c5aa 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -370,16 +370,16 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { /// A semantically valid [`Bolt12Invoice`] that hasn't been signed. pub struct UnsignedBolt12Invoice { bytes: Vec, - invoice: InvoiceContents, + contents: InvoiceContents, tagged_hash: TaggedHash, } impl UnsignedBolt12Invoice { - fn new(invreq_bytes: &[u8], invoice: InvoiceContents) -> Self { + fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self { // 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) = invoice.as_tlv_stream(); + let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream(); let invoice_request_bytes = WithoutSignatures(invreq_bytes); let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream); @@ -388,12 +388,12 @@ impl UnsignedBolt12Invoice { let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); - Self { bytes, invoice, tagged_hash } + Self { bytes, contents, tagged_hash } } /// The public key corresponding to the key needed to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { - self.invoice.fields().signing_pubkey + self.contents.fields().signing_pubkey } /// Signs the invoice using the given function. @@ -403,7 +403,7 @@ impl UnsignedBolt12Invoice { where F: FnOnce(&Self) -> Result { - let pubkey = self.invoice.fields().signing_pubkey; + let pubkey = self.contents.fields().signing_pubkey; let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. @@ -414,7 +414,7 @@ impl UnsignedBolt12Invoice { Ok(Bolt12Invoice { bytes: self.bytes, - contents: self.invoice, + contents: self.contents, signature, }) } @@ -1802,7 +1802,7 @@ mod tests { .sign(payer_sign).unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() - .invoice + .contents .write(&mut buffer).unwrap(); match Bolt12Invoice::try_from(buffer) { diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 1dea6503f58..978ae77bfb7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -346,16 +346,16 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a /// A semantically valid [`InvoiceRequest`] that hasn't been signed. pub struct UnsignedInvoiceRequest { bytes: Vec, - invoice_request: InvoiceRequestContents, + contents: InvoiceRequestContents, tagged_hash: TaggedHash, } impl UnsignedInvoiceRequest { - fn new(offer: &Offer, invoice_request: InvoiceRequestContents) -> Self { + fn new(offer: &Offer, contents: InvoiceRequestContents) -> Self { // Use the offer bytes instead of the offer TLV stream as the offer may have contained // unknown TLV records, which are not stored in `OfferContents`. let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = - invoice_request.as_tlv_stream(); + contents.as_tlv_stream(); let offer_bytes = WithoutLength(&offer.bytes); let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); @@ -364,7 +364,7 @@ impl UnsignedInvoiceRequest { let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); - Self { bytes, invoice_request, tagged_hash } + Self { bytes, contents, tagged_hash } } /// Signs the invoice request using the given function. @@ -374,7 +374,7 @@ impl UnsignedInvoiceRequest { where F: FnOnce(&Self) -> Result { - let pubkey = self.invoice_request.payer_id; + let pubkey = self.contents.payer_id; let signature = merkle::sign_message(sign, &self, pubkey)?; // Append the signature TLV record to the bytes. @@ -385,7 +385,7 @@ impl UnsignedInvoiceRequest { Ok(InvoiceRequest { bytes: self.bytes, - contents: self.invoice_request, + contents: self.contents, signature, }) } @@ -1681,7 +1681,7 @@ mod tests { .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap(); - let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); tlv_stream.0.metadata = None; let mut buffer = Vec::new(); @@ -1702,7 +1702,7 @@ mod tests { .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap(); - let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); tlv_stream.2.payer_id = None; let mut buffer = Vec::new(); @@ -1721,7 +1721,7 @@ mod tests { .build().unwrap(); let unsigned_invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap(); - let mut tlv_stream = unsigned_invoice_request.invoice_request.as_tlv_stream(); + let mut tlv_stream = unsigned_invoice_request.contents.as_tlv_stream(); tlv_stream.1.node_id = None; let mut buffer = Vec::new(); @@ -1743,7 +1743,7 @@ mod tests { .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() - .invoice_request + .contents .write(&mut buffer).unwrap(); match InvoiceRequest::try_from(buffer) { From 230f081e5a63161c3396adfb509ccfe045e921f8 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 11 Aug 2023 22:13:36 -0500 Subject: [PATCH 06/16] Unsigned BOLT 12 message parsing and serialization --- lightning/src/offers/invoice.rs | 65 +++++++++++++++++++++++-- lightning/src/offers/invoice_request.rs | 54 ++++++++++++++++++-- lightning/src/offers/merkle.rs | 1 + 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index bb62e71c5aa..4bc2ac6e707 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -368,6 +368,11 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } /// A semantically valid [`Bolt12Invoice`] that hasn't been signed. +/// +/// # Serialization +/// +/// This is serialized as a TLV stream, which includes TLV records from the originating message. As +/// such, it may include unknown, odd TLV records. pub struct UnsignedBolt12Invoice { bytes: Vec, contents: InvoiceContents, @@ -396,7 +401,9 @@ impl UnsignedBolt12Invoice { self.contents.fields().signing_pubkey } - /// Signs the invoice using the given function. + /// Signs the [`TaggedHash`] of the invoice using the given function. + /// + /// Note: The hash computation may have included unknown, odd TLV records. /// /// This is not exported to bindings users as functions aren't currently mapped. pub fn sign(mut self, sign: F) -> Result> @@ -733,6 +740,12 @@ impl InvoiceFields { } } +impl Writeable for UnsignedBolt12Invoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + impl Writeable for Bolt12Invoice { fn write(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) @@ -745,6 +758,25 @@ impl Writeable for InvoiceContents { } } +impl TryFrom> for UnsignedBolt12Invoice { + type Error = Bolt12ParseError; + + fn try_from(bytes: Vec) -> Result { + let invoice = ParsedMessage::::try_from(bytes)?; + let ParsedMessage { bytes, tlv_stream } = invoice; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + ) = tlv_stream; + let contents = InvoiceContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) + )?; + + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash }) + } +} + impl TryFrom> for Bolt12Invoice { type Error = Bolt12ParseError; @@ -857,6 +889,17 @@ type PartialInvoiceTlvStreamRef<'a> = ( InvoiceTlvStreamRef<'a>, ); +impl SeekReadable for PartialInvoiceTlvStream { + 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)?; + + Ok((payer, offer, invoice_request, invoice)) + } +} + impl TryFrom> for Bolt12Invoice { type Error = Bolt12ParseError; @@ -961,7 +1004,7 @@ impl TryFrom for InvoiceContents { #[cfg(test)] mod tests { - use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG}; + use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; @@ -1007,15 +1050,27 @@ mod tests { let payment_paths = payment_paths(); let payment_hash = payment_hash(); let now = now(); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_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_no_std(payment_paths.clone(), payment_hash, now).unwrap() - .build().unwrap() - .sign(recipient_sign).unwrap(); + .build().unwrap(); + + let mut buffer = Vec::new(); + unsigned_invoice.write(&mut buffer).unwrap(); + + match UnsignedBolt12Invoice::try_from(buffer) { + Err(e) => panic!("error parsing unsigned invoice: {:?}", e), + Ok(parsed) => { + assert_eq!(parsed.bytes, unsigned_invoice.bytes); + assert_eq!(parsed.tagged_hash, unsigned_invoice.tagged_hash); + }, + } + + let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); let mut buffer = Vec::new(); invoice.write(&mut buffer).unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 978ae77bfb7..b05f9af8481 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -344,6 +344,11 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a } /// A semantically valid [`InvoiceRequest`] that hasn't been signed. +/// +/// # Serialization +/// +/// This is serialized as a TLV stream, which includes TLV records from the originating message. As +/// such, it may include unknown, odd TLV records. pub struct UnsignedInvoiceRequest { bytes: Vec, contents: InvoiceRequestContents, @@ -367,7 +372,9 @@ impl UnsignedInvoiceRequest { Self { bytes, contents, tagged_hash } } - /// Signs the invoice request using the given function. + /// Signs the [`TaggedHash`] of the invoice request using the given function. + /// + /// Note: The hash computation may have included unknown, odd TLV records. /// /// This is not exported to bindings users as functions are not yet mapped. pub fn sign(mut self, sign: F) -> Result> @@ -664,6 +671,12 @@ impl InvoiceRequestContentsWithoutPayerId { } } +impl Writeable for UnsignedInvoiceRequest { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + impl Writeable for InvoiceRequest { fn write(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) @@ -723,6 +736,25 @@ type PartialInvoiceRequestTlvStreamRef<'a> = ( InvoiceRequestTlvStreamRef<'a>, ); +impl TryFrom> for UnsignedInvoiceRequest { + type Error = Bolt12ParseError; + + fn try_from(bytes: Vec) -> Result { + let invoice_request = ParsedMessage::::try_from(bytes)?; + let ParsedMessage { bytes, tlv_stream } = invoice_request; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + ) = tlv_stream; + let contents = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash }) + } +} + impl TryFrom> for InvoiceRequest { type Error = Bolt12ParseError; @@ -792,7 +824,7 @@ impl TryFrom for InvoiceRequestContents { #[cfg(test)] mod tests { - use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG}; + use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG, UnsignedInvoiceRequest}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; @@ -816,12 +848,24 @@ mod tests { #[test] fn builds_invoice_request_with_defaults() { - let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() - .build().unwrap() - .sign(payer_sign).unwrap(); + .build().unwrap(); + + let mut buffer = Vec::new(); + unsigned_invoice_request.write(&mut buffer).unwrap(); + + match UnsignedInvoiceRequest::try_from(buffer) { + Err(e) => panic!("error parsing unsigned invoice request: {:?}", e), + Ok(parsed) => { + assert_eq!(parsed.bytes, unsigned_invoice_request.bytes); + assert_eq!(parsed.tagged_hash, unsigned_invoice_request.tagged_hash); + }, + } + + let invoice_request = unsigned_invoice_request.sign(payer_sign).unwrap(); let mut buffer = Vec::new(); invoice_request.write(&mut buffer).unwrap(); diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index d15039cd317..b3867bf6f65 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -30,6 +30,7 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { /// /// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki /// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation +#[derive(Debug, PartialEq)] pub struct TaggedHash(Message); impl TaggedHash { From 270bc2e4c07f2891c8fbbc2339297565bf529050 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 14 Aug 2023 21:09:57 -0500 Subject: [PATCH 07/16] Move BOLT 12 offer method implementations --- lightning/src/offers/offer.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d801be9d26f..a84c2ff43f1 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -398,14 +398,14 @@ impl Offer { /// Features pertaining to the offer. pub fn features(&self) -> &OfferFeatures { - &self.contents.features + &self.contents.features() } /// Duration since the Unix epoch when an invoice should no longer be requested. /// /// If `None`, the offer does not expire. pub fn absolute_expiry(&self) -> Option { - self.contents.absolute_expiry + self.contents.absolute_expiry() } /// Whether the offer has expired. @@ -417,13 +417,13 @@ impl Offer { /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be /// displayed to the user but with the caveat that it has not been verified in any way. pub fn issuer(&self) -> Option { - self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + self.contents.issuer() } /// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide /// recipient privacy by obfuscating its node id. pub fn paths(&self) -> &[BlindedPath] { - self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + self.contents.paths() } /// The quantity of items supported. @@ -551,10 +551,22 @@ impl OfferContents { self.metadata.as_ref().and_then(|metadata| metadata.as_bytes()) } + pub fn amount(&self) -> Option<&Amount> { + self.amount.as_ref() + } + pub fn description(&self) -> PrintableString { PrintableString(&self.description) } + pub fn features(&self) -> &OfferFeatures { + &self.features + } + + pub fn absolute_expiry(&self) -> Option { + self.absolute_expiry + } + #[cfg(feature = "std")] pub(super) fn is_expired(&self) -> bool { match self.absolute_expiry { @@ -566,8 +578,12 @@ impl OfferContents { } } - pub fn amount(&self) -> Option<&Amount> { - self.amount.as_ref() + pub fn issuer(&self) -> Option { + self.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + } + + pub fn paths(&self) -> &[BlindedPath] { + self.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) } pub(super) fn check_amount_msats_for_quantity( From 85c471aac254cb4b7a93f0fa38a26ad0b637cbe3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 14 Aug 2023 12:55:34 -0500 Subject: [PATCH 08/16] Move BOLT 12 InvoiceRequest method implementations --- lightning/src/offers/invoice_request.rs | 28 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index b05f9af8481..c3b9f5bdd20 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -460,29 +460,28 @@ impl InvoiceRequest { /// /// [`chain`]: Self::chain pub fn amount_msats(&self) -> Option { - self.contents.inner.amount_msats + self.contents.amount_msats() } /// Features pertaining to requesting an invoice. pub fn features(&self) -> &InvoiceRequestFeatures { - &self.contents.inner.features + &self.contents.features() } /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. pub fn quantity(&self) -> Option { - self.contents.inner.quantity + self.contents.quantity() } /// A possibly transient pubkey used to sign the invoice request. pub fn payer_id(&self) -> PublicKey { - self.contents.payer_id + self.contents.payer_id() } /// A payer-provided note which will be seen by the recipient and reflected back in the invoice /// response. pub fn payer_note(&self) -> Option { - self.contents.inner.payer_note.as_ref() - .map(|payer_note| PrintableString(payer_note.as_str())) + self.contents.payer_note() } /// Signature of the invoice request using [`payer_id`]. @@ -626,10 +625,27 @@ impl InvoiceRequestContents { self.inner.chain() } + fn amount_msats(&self) -> Option { + self.inner.amount_msats + } + + fn features(&self) -> &InvoiceRequestFeatures { + &self.inner.features + } + + fn quantity(&self) -> Option { + self.inner.quantity + } + pub(super) fn payer_id(&self) -> PublicKey { self.payer_id } + fn payer_note(&self) -> Option { + self.inner.payer_note.as_ref() + .map(|payer_note| PrintableString(payer_note.as_str())) + } + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream(); invoice_request.payer_id = Some(&self.payer_id); From 68d471751b90c210d1664a11b07278fbc1c052d7 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Sun, 13 Aug 2023 13:29:45 -0500 Subject: [PATCH 09/16] Move BOLT 12 invoice method implementations --- lightning/src/offers/invoice.rs | 160 +++++++++++++++++++------------- 1 file changed, 95 insertions(+), 65 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 4bc2ac6e707..cf5d4367eca 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -501,106 +501,50 @@ impl Bolt12Invoice { /// This is not exported to bindings users as slices with non-reference types cannot be ABI /// matched in another language. pub fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] { - &self.contents.fields().payment_paths[..] + self.contents.payment_paths() } /// Duration since the Unix epoch when the invoice was created. pub fn created_at(&self) -> Duration { - self.contents.fields().created_at + self.contents.created_at() } /// Duration since [`Bolt12Invoice::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) + self.contents.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, - } + self.contents.is_expired() } /// 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 + self.contents.payment_hash() } /// The minimum amount required for a successful payment of the invoice. pub fn amount_msats(&self) -> u64 { - self.contents.fields().amount_msats + self.contents.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 - } + self.contents.fallbacks() } /// Features pertaining to paying an invoice. pub fn features(&self) -> &Bolt12InvoiceFeatures { - &self.contents.fields().features + self.contents.features() } /// The public key corresponding to the key used to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { - self.contents.fields().signing_pubkey + self.contents.signing_pubkey() } /// Signature of the invoice verified using [`Bolt12Invoice::signing_pubkey`]. @@ -659,6 +603,92 @@ impl InvoiceContents { } } + fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] { + &self.fields().payment_paths[..] + } + + fn created_at(&self) -> Duration { + self.fields().created_at + } + + fn relative_expiry(&self) -> Duration { + self.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY) + } + + #[cfg(feature = "std")] + 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, + } + } + + fn payment_hash(&self) -> PaymentHash { + self.fields().payment_hash + } + + fn amount_msats(&self) -> u64 { + self.fields().amount_msats + } + + fn fallbacks(&self) -> Vec
{ + let chain = self.chain(); + let network = if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Network::Bitcoin + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Network::Testnet + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Network::Signet + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Network::Regtest + } else { + return Vec::new() + }; + + 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: program.clone(), + }, + network, + }; + + if !address.is_standard() && version == WitnessVersion::V0 { + return None; + } + + Some(address) + }; + + self.fields().fallbacks + .as_ref() + .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect()) + .unwrap_or_else(Vec::new) + } + + fn features(&self) -> &Bolt12InvoiceFeatures { + &self.fields().features + } + + fn signing_pubkey(&self) -> PublicKey { + self.fields().signing_pubkey + } + fn fields(&self) -> &InvoiceFields { match self { InvoiceContents::ForOffer { fields, .. } => fields, From 66060cae8af6969373bb16b73064b8ea3a7b689a Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Aug 2023 07:45:06 -0500 Subject: [PATCH 10/16] Macro-ize Offer accessors for reuse InvoiceRequest wraps OfferContents, which shouldn't be exposed as it is an implementation detail. Define a macro for Offer accessor methods so that InvoiceRequest and UnsignedInvoiceRequest can also define them. --- lightning/src/offers/mod.rs | 4 +- lightning/src/offers/offer.rs | 94 ++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index d97632a78cc..c62702711c6 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,11 +12,13 @@ //! //! Offers are a flexible protocol for Lightning payments. +#[macro_use] +pub mod offer; + pub mod invoice; pub mod invoice_error; pub mod invoice_request; pub mod merkle; -pub mod offer; pub mod parse; mod payer; pub mod refund; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a84c2ff43f1..1044ac9606d 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -358,77 +358,86 @@ pub(super) struct OfferContents { signing_pubkey: PublicKey, } -impl Offer { +macro_rules! offer_accessors { ($self: ident, $contents: expr) => { // TODO: Return a slice once ChainHash has constants. // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1283 // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1286 /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet). /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) /// for the selected chain. - pub fn chains(&self) -> Vec { - self.contents.chains() - } - - pub(super) fn implied_chain(&self) -> ChainHash { - self.contents.implied_chain() - } - - /// Returns whether the given chain is supported by the offer. - pub fn supports_chain(&self, chain: ChainHash) -> bool { - self.contents.supports_chain(chain) + pub fn chains(&$self) -> Vec<$crate::bitcoin::blockdata::constants::ChainHash> { + $contents.chains() } // TODO: Link to corresponding method in `InvoiceRequest`. /// Opaque bytes set by the originator. Useful for authentication and validating fields since it /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. - pub fn metadata(&self) -> Option<&Vec> { - self.contents.metadata() + pub fn metadata(&$self) -> Option<&Vec> { + $contents.metadata() } /// The minimum amount required for a successful payment of a single item. - pub fn amount(&self) -> Option<&Amount> { - self.contents.amount() + pub fn amount(&$self) -> Option<&$crate::offers::offer::Amount> { + $contents.amount() } /// A complete description of the purpose of the payment. Intended to be displayed to the user /// but with the caveat that it has not been verified in any way. - pub fn description(&self) -> PrintableString { - self.contents.description() + pub fn description(&$self) -> $crate::util::string::PrintableString { + $contents.description() } /// Features pertaining to the offer. - pub fn features(&self) -> &OfferFeatures { - &self.contents.features() + pub fn offer_features(&$self) -> &$crate::ln::features::OfferFeatures { + &$contents.features() } /// Duration since the Unix epoch when an invoice should no longer be requested. /// /// If `None`, the offer does not expire. - pub fn absolute_expiry(&self) -> Option { - self.contents.absolute_expiry() - } - - /// Whether the offer has expired. - #[cfg(feature = "std")] - pub fn is_expired(&self) -> bool { - self.contents.is_expired() + pub fn absolute_expiry(&$self) -> Option { + $contents.absolute_expiry() } /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be /// displayed to the user but with the caveat that it has not been verified in any way. - pub fn issuer(&self) -> Option { - self.contents.issuer() + pub fn issuer(&$self) -> Option<$crate::util::string::PrintableString> { + $contents.issuer() } /// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide /// recipient privacy by obfuscating its node id. - pub fn paths(&self) -> &[BlindedPath] { - self.contents.paths() + pub fn paths(&$self) -> &[$crate::blinded_path::BlindedPath] { + $contents.paths() } /// The quantity of items supported. - pub fn supported_quantity(&self) -> Quantity { - self.contents.supported_quantity() + pub fn supported_quantity(&$self) -> $crate::offers::offer::Quantity { + $contents.supported_quantity() + } + + /// The public key used by the recipient to sign invoices. + pub fn signing_pubkey(&$self) -> $crate::bitcoin::secp256k1::PublicKey { + $contents.signing_pubkey() + } +} } + +impl Offer { + offer_accessors!(self, self.contents); + + pub(super) fn implied_chain(&self) -> ChainHash { + self.contents.implied_chain() + } + + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.contents.supports_chain(chain) + } + + /// Whether the offer has expired. + #[cfg(feature = "std")] + pub fn is_expired(&self) -> bool { + self.contents.is_expired() } /// Returns whether the given quantity is valid for the offer. @@ -443,11 +452,6 @@ impl Offer { self.contents.expects_quantity() } - /// The public key used by the recipient to sign invoices. - pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey() - } - /// Similar to [`Offer::request_invoice`] except it: /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each /// request, and @@ -469,7 +473,7 @@ impl Offer { where ES::Target: EntropySource, { - if self.features().requires_unknown_bits() { + if self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -490,7 +494,7 @@ impl Offer { where ES::Target: EntropySource, { - if self.features().requires_unknown_bits() { + if self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -515,7 +519,7 @@ impl Offer { pub fn request_invoice( &self, metadata: Vec, payer_id: PublicKey ) -> Result, Bolt12SemanticError> { - if self.features().requires_unknown_bits() { + if self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -890,7 +894,7 @@ mod tests { assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); assert_eq!(offer.description(), PrintableString("foo")); - assert_eq!(offer.features(), &OfferFeatures::empty()); + assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.absolute_expiry(), None); #[cfg(feature = "std")] assert!(!offer.is_expired()); @@ -1131,7 +1135,7 @@ mod tests { .features_unchecked(OfferFeatures::unknown()) .build() .unwrap(); - assert_eq!(offer.features(), &OfferFeatures::unknown()); + assert_eq!(offer.offer_features(), &OfferFeatures::unknown()); assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); let offer = OfferBuilder::new("foo".into(), pubkey(42)) @@ -1139,7 +1143,7 @@ mod tests { .features_unchecked(OfferFeatures::empty()) .build() .unwrap(); - assert_eq!(offer.features(), &OfferFeatures::empty()); + assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.as_tlv_stream().features, None); } From 9d02d06e06b112d76bdd0380ede58bf7e6242769 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Aug 2023 08:24:40 -0500 Subject: [PATCH 11/16] Macro-ize InvoiceRequest accessors for reuse Various messages wrap InvoiceRequestContents, which shouldn't be exposed as it is an implementation detail. Define a macro for InvoiceRequest accessor methods so that these messages can also define them. --- lightning/src/offers/invoice_request.rs | 46 ++++++++++++++----------- lightning/src/offers/offer.rs | 8 ++--- lightning/src/offers/payer.rs | 4 +-- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index c3b9f5bdd20..e41a699f2e8 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -441,48 +441,52 @@ pub(super) struct InvoiceRequestContentsWithoutPayerId { payer_note: Option, } -impl InvoiceRequest { +macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { /// An unpredictable series of bytes, typically containing information about the derivation of /// [`payer_id`]. /// /// [`payer_id`]: Self::payer_id - pub fn metadata(&self) -> &[u8] { - self.contents.metadata() + pub fn payer_metadata(&$self) -> &[u8] { + $contents.metadata() } /// A chain from [`Offer::chains`] that the offer is valid for. - pub fn chain(&self) -> ChainHash { - self.contents.chain() + pub fn chain(&$self) -> ChainHash { + $contents.chain() } /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which /// must be greater than or equal to [`Offer::amount`], converted if necessary. /// /// [`chain`]: Self::chain - pub fn amount_msats(&self) -> Option { - self.contents.amount_msats() + pub fn amount_msats(&$self) -> Option { + $contents.amount_msats() } /// Features pertaining to requesting an invoice. - pub fn features(&self) -> &InvoiceRequestFeatures { - &self.contents.features() + pub fn invoice_request_features(&$self) -> &InvoiceRequestFeatures { + &$contents.features() } /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. - pub fn quantity(&self) -> Option { - self.contents.quantity() + pub fn quantity(&$self) -> Option { + $contents.quantity() } /// A possibly transient pubkey used to sign the invoice request. - pub fn payer_id(&self) -> PublicKey { - self.contents.payer_id() + pub fn payer_id(&$self) -> PublicKey { + $contents.payer_id() } /// A payer-provided note which will be seen by the recipient and reflected back in the invoice /// response. - pub fn payer_note(&self) -> Option { - self.contents.payer_note() + pub fn payer_note(&$self) -> Option { + $contents.payer_note() } +} } + +impl InvoiceRequest { + invoice_request_accessors!(self, self.contents); /// Signature of the invoice request using [`payer_id`]. /// @@ -534,7 +538,7 @@ impl InvoiceRequest { &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash, created_at: core::time::Duration ) -> Result, Bolt12SemanticError> { - if self.features().requires_unknown_bits() { + if self.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -577,7 +581,7 @@ impl InvoiceRequest { &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash, created_at: core::time::Duration, expanded_key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> Result, Bolt12SemanticError> { - if self.features().requires_unknown_bits() { + if self.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } @@ -887,10 +891,10 @@ mod tests { invoice_request.write(&mut buffer).unwrap(); assert_eq!(invoice_request.bytes, buffer.as_slice()); - assert_eq!(invoice_request.metadata(), &[1; 32]); + assert_eq!(invoice_request.payer_metadata(), &[1; 32]); assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); assert_eq!(invoice_request.amount_msats(), None); - assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::empty()); + assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(invoice_request.quantity(), None); assert_eq!(invoice_request.payer_id(), payer_pubkey()); assert_eq!(invoice_request.payer_note(), None); @@ -1291,7 +1295,7 @@ mod tests { .build().unwrap() .sign(payer_sign).unwrap(); let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); - assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::unknown()); + assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::unknown()); assert_eq!(tlv_stream.features, Some(&InvoiceRequestFeatures::unknown())); let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) @@ -1303,7 +1307,7 @@ mod tests { .build().unwrap() .sign(payer_sign).unwrap(); let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream(); - assert_eq!(invoice_request.features(), &InvoiceRequestFeatures::empty()); + assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(tlv_stream.features, None); } diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 1044ac9606d..f6aa354b9e4 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -455,16 +455,16 @@ impl Offer { /// Similar to [`Offer::request_invoice`] except it: /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each /// request, and - /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such - /// that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was requested - /// using a base [`ExpandedKey`] from which the payer id was derived. + /// - sets the [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is + /// called such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice + /// was requested using a base [`ExpandedKey`] from which the payer id was derived. /// /// Useful to protect the sender's privacy. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id - /// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata + /// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata /// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>( diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index bfc02b5dbcb..b3b2f7a88ae 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -22,9 +22,9 @@ use crate::prelude::*; #[cfg_attr(test, derive(PartialEq))] pub(super) struct PayerContents(pub Metadata); -/// TLV record type for [`InvoiceRequest::metadata`] and [`Refund::metadata`]. +/// TLV record type for [`InvoiceRequest::payer_metadata`] and [`Refund::metadata`]. /// -/// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata +/// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata /// [`Refund::metadata`]: crate::offers::refund::Refund::metadata pub(super) const PAYER_METADATA_TYPE: u64 = 0; From bde982344a685a04bcd219c1c040b8fabe8f08af Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Aug 2023 13:02:02 -0500 Subject: [PATCH 12/16] Expose Offer accessor functions in InvoiceRequest Also, expose both Offer and InvoiceRequest functions in UnsignedInvoiceRequest. --- lightning/src/offers/invoice_request.rs | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index e41a699f2e8..5704dcbd3e2 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -485,7 +485,13 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { } } } +impl UnsignedInvoiceRequest { + offer_accessors!(self, self.contents.inner.offer); + invoice_request_accessors!(self, self.contents); +} + impl InvoiceRequest { + offer_accessors!(self, self.contents.inner.offer); invoice_request_accessors!(self, self.contents); /// Signature of the invoice request using [`payer_id`]. @@ -854,7 +860,7 @@ mod tests { #[cfg(feature = "std")] use core::time::Duration; use crate::sign::KeyMaterial; - use crate::ln::features::InvoiceRequestFeatures; + use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG}; @@ -877,6 +883,25 @@ mod tests { let mut buffer = Vec::new(); unsigned_invoice_request.write(&mut buffer).unwrap(); + assert_eq!(unsigned_invoice_request.bytes, buffer.as_slice()); + assert_eq!(unsigned_invoice_request.payer_metadata(), &[1; 32]); + assert_eq!(unsigned_invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert_eq!(unsigned_invoice_request.metadata(), None); + assert_eq!(unsigned_invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); + assert_eq!(unsigned_invoice_request.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice_request.offer_features(), &OfferFeatures::empty()); + assert_eq!(unsigned_invoice_request.absolute_expiry(), None); + assert_eq!(unsigned_invoice_request.paths(), &[]); + assert_eq!(unsigned_invoice_request.issuer(), None); + assert_eq!(unsigned_invoice_request.supported_quantity(), Quantity::One); + assert_eq!(unsigned_invoice_request.signing_pubkey(), recipient_pubkey()); + assert_eq!(unsigned_invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(unsigned_invoice_request.amount_msats(), None); + assert_eq!(unsigned_invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); + assert_eq!(unsigned_invoice_request.quantity(), None); + assert_eq!(unsigned_invoice_request.payer_id(), payer_pubkey()); + assert_eq!(unsigned_invoice_request.payer_note(), None); + match UnsignedInvoiceRequest::try_from(buffer) { Err(e) => panic!("error parsing unsigned invoice request: {:?}", e), Ok(parsed) => { @@ -892,6 +917,16 @@ mod tests { assert_eq!(invoice_request.bytes, buffer.as_slice()); assert_eq!(invoice_request.payer_metadata(), &[1; 32]); + assert_eq!(invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert_eq!(invoice_request.metadata(), None); + assert_eq!(invoice_request.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); + assert_eq!(invoice_request.description(), PrintableString("foo")); + assert_eq!(invoice_request.offer_features(), &OfferFeatures::empty()); + assert_eq!(invoice_request.absolute_expiry(), None); + assert_eq!(invoice_request.paths(), &[]); + assert_eq!(invoice_request.issuer(), None); + assert_eq!(invoice_request.supported_quantity(), Quantity::One); + assert_eq!(invoice_request.signing_pubkey(), recipient_pubkey()); assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); assert_eq!(invoice_request.amount_msats(), None); assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty()); From 57e62da9f44a4da9044a6ef8993bb280ef3cbfd8 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 15 Aug 2023 13:09:06 -0500 Subject: [PATCH 13/16] Expose invoice accessors in UnsignedBolt12Invoice --- lightning/src/offers/invoice.rs | 79 ++++++++++++++++++++------------- lightning/src/routing/router.rs | 2 +- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index cf5d4367eca..068cc958325 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -320,7 +320,8 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { self } - /// Sets [`Bolt12Invoice::features`] to indicate MPP may be used. Otherwise, MPP is disallowed. + /// Sets [`Bolt12Invoice::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 @@ -396,11 +397,6 @@ impl UnsignedBolt12Invoice { Self { bytes, contents, tagged_hash } } - /// The public key corresponding to the key needed to sign the invoice. - pub fn signing_pubkey(&self) -> PublicKey { - self.contents.fields().signing_pubkey - } - /// Signs the [`TaggedHash`] of the invoice using the given function. /// /// Note: The hash computation may have included unknown, odd TLV records. @@ -485,11 +481,11 @@ struct InvoiceFields { signing_pubkey: PublicKey, } -impl Bolt12Invoice { +macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// A complete description of the purpose of the originating offer or refund. Intended to be /// displayed to the user but with the caveat that it has not been verified in any way. - pub fn description(&self) -> PrintableString { - self.contents.description() + pub fn description(&$self) -> PrintableString { + $contents.description() } /// Paths to the recipient originating from publicly reachable nodes, including information @@ -500,52 +496,60 @@ impl Bolt12Invoice { /// /// This is not exported to bindings users as slices with non-reference types cannot be ABI /// matched in another language. - pub fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] { - self.contents.payment_paths() + pub fn payment_paths(&$self) -> &[(BlindedPayInfo, BlindedPath)] { + $contents.payment_paths() } /// Duration since the Unix epoch when the invoice was created. - pub fn created_at(&self) -> Duration { - self.contents.created_at() + pub fn created_at(&$self) -> Duration { + $contents.created_at() } /// Duration since [`Bolt12Invoice::created_at`] when the invoice has expired and therefore /// should no longer be paid. - pub fn relative_expiry(&self) -> Duration { - self.contents.relative_expiry() + pub fn relative_expiry(&$self) -> Duration { + $contents.relative_expiry() } /// Whether the invoice has expired. #[cfg(feature = "std")] - pub fn is_expired(&self) -> bool { - self.contents.is_expired() + pub fn is_expired(&$self) -> bool { + $contents.is_expired() } /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. - pub fn payment_hash(&self) -> PaymentHash { - self.contents.payment_hash() + pub fn payment_hash(&$self) -> PaymentHash { + $contents.payment_hash() } /// The minimum amount required for a successful payment of the invoice. - pub fn amount_msats(&self) -> u64 { - self.contents.amount_msats() + pub fn amount_msats(&$self) -> u64 { + $contents.amount_msats() } /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to /// least-preferred. - pub fn fallbacks(&self) -> Vec
{ - self.contents.fallbacks() + pub fn fallbacks(&$self) -> Vec
{ + $contents.fallbacks() } /// Features pertaining to paying an invoice. - pub fn features(&self) -> &Bolt12InvoiceFeatures { - self.contents.features() + pub fn invoice_features(&$self) -> &Bolt12InvoiceFeatures { + $contents.features() } /// The public key corresponding to the key used to sign the invoice. - pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey() + pub fn signing_pubkey(&$self) -> PublicKey { + $contents.signing_pubkey() } +} } + +impl UnsignedBolt12Invoice { + invoice_accessors!(self, self.contents); +} + +impl Bolt12Invoice { + invoice_accessors!(self, self.contents); /// Signature of the invoice verified using [`Bolt12Invoice::signing_pubkey`]. pub fn signature(&self) -> Signature { @@ -1092,6 +1096,19 @@ mod tests { let mut buffer = Vec::new(); unsigned_invoice.write(&mut buffer).unwrap(); + assert_eq!(unsigned_invoice.bytes, buffer.as_slice()); + assert_eq!(unsigned_invoice.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice.payment_paths(), payment_paths.as_slice()); + assert_eq!(unsigned_invoice.created_at(), now); + assert_eq!(unsigned_invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); + #[cfg(feature = "std")] + assert!(!unsigned_invoice.is_expired()); + assert_eq!(unsigned_invoice.payment_hash(), payment_hash); + assert_eq!(unsigned_invoice.amount_msats(), 1000); + assert_eq!(unsigned_invoice.fallbacks(), vec![]); + assert_eq!(unsigned_invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); + assert_eq!(unsigned_invoice.signing_pubkey(), recipient_pubkey()); + match UnsignedBolt12Invoice::try_from(buffer) { Err(e) => panic!("error parsing unsigned invoice: {:?}", e), Ok(parsed) => { @@ -1115,7 +1132,7 @@ mod tests { 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.invoice_features(), &Bolt12InvoiceFeatures::empty()); assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); assert!( merkle::verify_signature( @@ -1198,7 +1215,7 @@ mod tests { 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.invoice_features(), &Bolt12InvoiceFeatures::empty()); assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); assert!( merkle::verify_signature( @@ -1546,7 +1563,7 @@ mod tests { .build().unwrap() .sign(recipient_sign).unwrap(); let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); - assert_eq!(invoice.features(), &features); + assert_eq!(invoice.invoice_features(), &features); assert_eq!(tlv_stream.features, Some(&features)); } @@ -1766,7 +1783,7 @@ mod tests { Ok(invoice) => { let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); - assert_eq!(invoice.features(), &features); + assert_eq!(invoice.invoice_features(), &features); }, Err(e) => panic!("error parsing invoice: {:?}", e), } diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 3419f122e7d..a7a0f1017f9 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -652,7 +652,7 @@ impl PaymentParameters { /// [`PaymentParameters::expiry_time`]. pub fn from_bolt12_invoice(invoice: &Bolt12Invoice) -> Self { Self::blinded(invoice.payment_paths().to_vec()) - .with_bolt12_features(invoice.features().clone()).unwrap() + .with_bolt12_features(invoice.invoice_features().clone()).unwrap() .with_expiry_time(invoice.created_at().as_secs().saturating_add(invoice.relative_expiry().as_secs())) } From 7f641da655810ef78cd61b796b79cbc4707d28bf Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Wed, 16 Aug 2023 16:35:16 -0500 Subject: [PATCH 14/16] Expose Offer/InvoiceRequest methods in Invoice Bolt12Invoice can either be for an Offer (via an InvoiceRequest) or a Refund. It wraps those types, so expose their methods on both Bolt12Invoice and UnsignedBolt12Invoice. Since Refund does not have all the Offer/InvoiceRequest methods, use an Option return type such that None can returned for refund-based invoices. For methods that are duplicated between Offer/InvoiceRequest and Bolt12Invoice, prefer the (non-Option, if applicable) method from Bolt12Invoice (e.g., amount_msats, signing_pubkey). --- lightning/src/offers/invoice.rs | 294 +++++++++++++++++++++++- lightning/src/offers/invoice_request.rs | 8 +- lightning/src/offers/payer.rs | 4 +- lightning/src/offers/refund.rs | 65 ++++-- 4 files changed, 344 insertions(+), 27 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 068cc958325..05960642efd 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -110,12 +110,12 @@ use core::time::Duration; use crate::io; use crate::blinded_path::BlindedPath; use crate::ln::PaymentHash; -use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; +use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; -use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents}; @@ -482,12 +482,141 @@ struct InvoiceFields { } macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { - /// A complete description of the purpose of the originating offer or refund. Intended to be - /// displayed to the user but with the caveat that it has not been verified in any way. + /// The chains that may be used when paying a requested invoice. + /// + /// From [`Offer::chains`]; `None` if the invoice was created in response to a [`Refund`]. + /// + /// [`Offer::chains`]: crate::offers::offer::Offer::chains + pub fn offer_chains(&$self) -> Option> { + $contents.offer_chains() + } + + /// The chain that must be used when paying the invoice; selected from [`offer_chains`] if the + /// invoice originated from an offer. + /// + /// From [`InvoiceRequest::chain`] or [`Refund::chain`]. + /// + /// [`offer_chains`]: Self::offer_chains + /// [`InvoiceRequest::chain`]: crate::offers::invoice_request::InvoiceRequest::chain + pub fn chain(&$self) -> ChainHash { + $contents.chain() + } + + /// Opaque bytes set by the originating [`Offer`]. + /// + /// From [`Offer::metadata`]; `None` if the invoice was created in response to a [`Refund`] or + /// if the [`Offer`] did not set it. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata + pub fn metadata(&$self) -> Option<&Vec> { + $contents.metadata() + } + + /// The minimum amount required for a successful payment of a single item. + /// + /// From [`Offer::amount`]; `None` if the invoice was created in response to a [`Refund`] or if + /// the [`Offer`] did not set it. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`Offer::amount`]: crate::offers::offer::Offer::amount + pub fn amount(&$self) -> Option<&Amount> { + $contents.amount() + } + + /// Features pertaining to the originating [`Offer`]. + /// + /// From [`Offer::offer_features`]; `None` if the invoice was created in response to a + /// [`Refund`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`Offer::offer_features`]: crate::offers::offer::Offer::offer_features + pub fn offer_features(&$self) -> Option<&OfferFeatures> { + $contents.offer_features() + } + + /// A complete description of the purpose of the originating offer or refund. + /// + /// From [`Offer::description`] or [`Refund::description`]. + /// + /// [`Offer::description`]: crate::offers::offer::Offer::description pub fn description(&$self) -> PrintableString { $contents.description() } + /// Duration since the Unix epoch when an invoice should no longer be requested. + /// + /// From [`Offer::absolute_expiry`] or [`Refund::absolute_expiry`]. + /// + /// [`Offer::absolute_expiry`]: crate::offers::offer::Offer::absolute_expiry + pub fn absolute_expiry(&$self) -> Option { + $contents.absolute_expiry() + } + + /// The issuer of the offer or refund. + /// + /// From [`Offer::issuer`] or [`Refund::issuer`]. + /// + /// [`Offer::issuer`]: crate::offers::offer::Offer::issuer + pub fn issuer(&$self) -> Option { + $contents.issuer() + } + + /// Paths to the recipient originating from publicly reachable nodes. + /// + /// From [`Offer::paths`] or [`Refund::paths`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + pub fn message_paths(&$self) -> &[BlindedPath] { + $contents.message_paths() + } + + /// The quantity of items supported. + /// + /// From [`Offer::supported_quantity`]; `None` if the invoice was created in response to a + /// [`Refund`]. + /// + /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity + pub fn supported_quantity(&$self) -> Option { + $contents.supported_quantity() + } + + /// An unpredictable series of bytes from the payer. + /// + /// From [`InvoiceRequest::payer_metadata`] or [`Refund::payer_metadata`]. + pub fn payer_metadata(&$self) -> &[u8] { + $contents.payer_metadata() + } + + /// Features pertaining to requesting an invoice. + /// + /// From [`InvoiceRequest::invoice_request_features`] or [`Refund::features`]. + pub fn invoice_request_features(&$self) -> &InvoiceRequestFeatures { + &$contents.invoice_request_features() + } + + /// The quantity of items requested or refunded for. + /// + /// From [`InvoiceRequest::quantity`] or [`Refund::quantity`]. + pub fn quantity(&$self) -> Option { + $contents.quantity() + } + + /// A possibly transient pubkey used to sign the invoice request or to send an invoice for a + /// refund in case there are no [`message_paths`]. + /// + /// [`message_paths`]: Self::message_paths + pub fn payer_id(&$self) -> PublicKey { + $contents.payer_id() + } + + /// A payer-provided note reflected back in the invoice. + /// + /// From [`InvoiceRequest::payer_note`] or [`Refund::payer_note`]. + pub fn payer_note(&$self) -> Option { + $contents.payer_note() + } + /// Paths to the recipient originating from publicly reachable nodes, including information /// needed for routing payments across them. /// @@ -591,6 +720,14 @@ impl InvoiceContents { } } + fn offer_chains(&self) -> Option> { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => + Some(invoice_request.inner.offer.chains()), + InvoiceContents::ForRefund { .. } => None, + } + } + fn chain(&self) -> ChainHash { match self { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(), @@ -598,6 +735,22 @@ impl InvoiceContents { } } + fn metadata(&self) -> Option<&Vec> { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => + invoice_request.inner.offer.metadata(), + InvoiceContents::ForRefund { .. } => None, + } + } + + fn amount(&self) -> Option<&Amount> { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => + invoice_request.inner.offer.amount(), + InvoiceContents::ForRefund { .. } => None, + } + } + fn description(&self) -> PrintableString { match self { InvoiceContents::ForOffer { invoice_request, .. } => { @@ -607,6 +760,86 @@ impl InvoiceContents { } } + fn offer_features(&self) -> Option<&OfferFeatures> { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + Some(invoice_request.inner.offer.features()) + }, + InvoiceContents::ForRefund { .. } => None, + } + } + + fn absolute_expiry(&self) -> Option { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + invoice_request.inner.offer.absolute_expiry() + }, + InvoiceContents::ForRefund { refund, .. } => refund.absolute_expiry(), + } + } + + fn issuer(&self) -> Option { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + invoice_request.inner.offer.issuer() + }, + InvoiceContents::ForRefund { refund, .. } => refund.issuer(), + } + } + + fn message_paths(&self) -> &[BlindedPath] { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + invoice_request.inner.offer.paths() + }, + InvoiceContents::ForRefund { refund, .. } => refund.paths(), + } + } + + fn supported_quantity(&self) -> Option { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => { + Some(invoice_request.inner.offer.supported_quantity()) + }, + InvoiceContents::ForRefund { .. } => None, + } + } + + fn payer_metadata(&self) -> &[u8] { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.metadata(), + InvoiceContents::ForRefund { refund, .. } => refund.metadata(), + } + } + + fn invoice_request_features(&self) -> &InvoiceRequestFeatures { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.features(), + InvoiceContents::ForRefund { refund, .. } => refund.features(), + } + } + + fn quantity(&self) -> Option { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.quantity(), + InvoiceContents::ForRefund { refund, .. } => refund.quantity(), + } + } + + fn payer_id(&self) -> PublicKey { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.payer_id(), + InvoiceContents::ForRefund { refund, .. } => refund.payer_id(), + } + } + + fn payer_note(&self) -> Option { + match self { + InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.payer_note(), + InvoiceContents::ForRefund { refund, .. } => refund.payer_note(), + } + } + fn payment_paths(&self) -> &[(BlindedPayInfo, BlindedPath)] { &self.fields().payment_paths[..] } @@ -1040,6 +1273,7 @@ impl TryFrom for InvoiceContents { mod tests { use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; + use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; @@ -1050,12 +1284,12 @@ mod tests { use core::time::Duration; use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::sign::KeyMaterial; - use crate::ln::features::Bolt12InvoiceFeatures; + use crate::ln::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; - use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; + use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; @@ -1097,7 +1331,23 @@ mod tests { unsigned_invoice.write(&mut buffer).unwrap(); assert_eq!(unsigned_invoice.bytes, buffer.as_slice()); + assert_eq!(unsigned_invoice.payer_metadata(), &[1; 32]); + assert_eq!(unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); + assert_eq!(unsigned_invoice.metadata(), None); + assert_eq!(unsigned_invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); assert_eq!(unsigned_invoice.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice.offer_features(), Some(&OfferFeatures::empty())); + assert_eq!(unsigned_invoice.absolute_expiry(), None); + assert_eq!(unsigned_invoice.message_paths(), &[]); + assert_eq!(unsigned_invoice.issuer(), None); + assert_eq!(unsigned_invoice.supported_quantity(), Some(Quantity::One)); + assert_eq!(unsigned_invoice.signing_pubkey(), recipient_pubkey()); + assert_eq!(unsigned_invoice.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(unsigned_invoice.amount_msats(), 1000); + assert_eq!(unsigned_invoice.invoice_request_features(), &InvoiceRequestFeatures::empty()); + assert_eq!(unsigned_invoice.quantity(), None); + assert_eq!(unsigned_invoice.payer_id(), payer_pubkey()); + assert_eq!(unsigned_invoice.payer_note(), None); assert_eq!(unsigned_invoice.payment_paths(), payment_paths.as_slice()); assert_eq!(unsigned_invoice.created_at(), now); assert_eq!(unsigned_invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); @@ -1123,7 +1373,23 @@ mod tests { invoice.write(&mut buffer).unwrap(); assert_eq!(invoice.bytes, buffer.as_slice()); + assert_eq!(invoice.payer_metadata(), &[1; 32]); + assert_eq!(invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); + assert_eq!(invoice.metadata(), None); + assert_eq!(invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.offer_features(), Some(&OfferFeatures::empty())); + assert_eq!(invoice.absolute_expiry(), None); + assert_eq!(invoice.message_paths(), &[]); + assert_eq!(invoice.issuer(), None); + assert_eq!(invoice.supported_quantity(), Some(Quantity::One)); + assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); + assert_eq!(invoice.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(invoice.amount_msats(), 1000); + assert_eq!(invoice.invoice_request_features(), &InvoiceRequestFeatures::empty()); + assert_eq!(invoice.quantity(), None); + assert_eq!(invoice.payer_id(), payer_pubkey()); + assert_eq!(invoice.payer_note(), None); assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); assert_eq!(invoice.created_at(), now); assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); @@ -1206,7 +1472,23 @@ mod tests { invoice.write(&mut buffer).unwrap(); assert_eq!(invoice.bytes, buffer.as_slice()); + assert_eq!(invoice.payer_metadata(), &[1; 32]); + assert_eq!(invoice.offer_chains(), None); + assert_eq!(invoice.metadata(), None); + assert_eq!(invoice.amount(), None); assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.offer_features(), None); + assert_eq!(invoice.absolute_expiry(), None); + assert_eq!(invoice.message_paths(), &[]); + assert_eq!(invoice.issuer(), None); + assert_eq!(invoice.supported_quantity(), None); + assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); + assert_eq!(invoice.chain(), ChainHash::using_genesis_block(Network::Bitcoin)); + assert_eq!(invoice.amount_msats(), 1000); + assert_eq!(invoice.invoice_request_features(), &InvoiceRequestFeatures::empty()); + assert_eq!(invoice.quantity(), None); + assert_eq!(invoice.payer_id(), payer_pubkey()); + assert_eq!(invoice.payer_note(), None); assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); assert_eq!(invoice.created_at(), now); assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 5704dcbd3e2..198e15ec527 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -635,15 +635,15 @@ impl InvoiceRequestContents { self.inner.chain() } - fn amount_msats(&self) -> Option { + pub(super) fn amount_msats(&self) -> Option { self.inner.amount_msats } - fn features(&self) -> &InvoiceRequestFeatures { + pub(super) fn features(&self) -> &InvoiceRequestFeatures { &self.inner.features } - fn quantity(&self) -> Option { + pub(super) fn quantity(&self) -> Option { self.inner.quantity } @@ -651,7 +651,7 @@ impl InvoiceRequestContents { self.payer_id } - fn payer_note(&self) -> Option { + pub(super) fn payer_note(&self) -> Option { self.inner.payer_note.as_ref() .map(|payer_note| PrintableString(payer_note.as_str())) } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index b3b2f7a88ae..19aef23363d 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -22,10 +22,10 @@ use crate::prelude::*; #[cfg_attr(test, derive(PartialEq))] pub(super) struct PayerContents(pub Metadata); -/// TLV record type for [`InvoiceRequest::payer_metadata`] and [`Refund::metadata`]. +/// TLV record type for [`InvoiceRequest::payer_metadata`] and [`Refund::payer_metadata`]. /// /// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata -/// [`Refund::metadata`]: crate::offers::refund::Refund::metadata +/// [`Refund::payer_metadata`]: crate::offers::refund::Refund::payer_metadata pub(super) const PAYER_METADATA_TYPE: u64 = 0; tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index 2c8dffeb151..d419e8fe0d2 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -117,7 +117,7 @@ impl<'a> RefundBuilder<'a, secp256k1::SignOnly> { /// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to /// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey. /// - /// Additionally, sets the required [`Refund::description`], [`Refund::metadata`], and + /// Additionally, sets the required [`Refund::description`], [`Refund::payer_metadata`], and /// [`Refund::amount_msats`]. pub fn new( description: String, metadata: Vec, payer_id: PublicKey, amount_msats: u64 @@ -319,7 +319,7 @@ impl Refund { /// /// If `None`, the refund does not expire. pub fn absolute_expiry(&self) -> Option { - self.contents.absolute_expiry + self.contents.absolute_expiry() } /// Whether the refund has expired. @@ -331,43 +331,43 @@ impl Refund { /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. Intended to be /// displayed to the user but with the caveat that it has not been verified in any way. pub fn issuer(&self) -> Option { - self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + self.contents.issuer() } /// Paths to the sender originating from publicly reachable nodes. Blinded paths provide sender /// privacy by obfuscating its node id. pub fn paths(&self) -> &[BlindedPath] { - self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + self.contents.paths() } /// An unpredictable series of bytes, typically containing information about the derivation of /// [`payer_id`]. /// /// [`payer_id`]: Self::payer_id - pub fn metadata(&self) -> &[u8] { + pub fn payer_metadata(&self) -> &[u8] { self.contents.metadata() } /// A chain that the refund is valid for. pub fn chain(&self) -> ChainHash { - self.contents.chain.unwrap_or_else(|| self.contents.implied_chain()) + self.contents.chain() } /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]). /// /// [`chain`]: Self::chain pub fn amount_msats(&self) -> u64 { - self.contents.amount_msats + self.contents.amount_msats() } /// Features pertaining to requesting an invoice. pub fn features(&self) -> &InvoiceRequestFeatures { - &self.contents.features + &self.contents.features() } /// The quantity of an item that refund is for. pub fn quantity(&self) -> Option { - self.contents.quantity + self.contents.quantity() } /// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly @@ -375,12 +375,12 @@ impl Refund { /// /// [`paths`]: Self::paths pub fn payer_id(&self) -> PublicKey { - self.contents.payer_id + self.contents.payer_id() } /// Payer provided note to include in the invoice. pub fn payer_note(&self) -> Option { - self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) + self.contents.payer_note() } /// Creates an [`InvoiceBuilder`] for the refund with the given required fields and using the @@ -503,6 +503,10 @@ impl RefundContents { PrintableString(&self.description) } + pub fn absolute_expiry(&self) -> Option { + self.absolute_expiry + } + #[cfg(feature = "std")] pub(super) fn is_expired(&self) -> bool { match self.absolute_expiry { @@ -514,6 +518,14 @@ impl RefundContents { } } + pub fn issuer(&self) -> Option { + self.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + } + + pub fn paths(&self) -> &[BlindedPath] { + self.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + } + pub(super) fn metadata(&self) -> &[u8] { self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[]) } @@ -526,14 +538,37 @@ impl RefundContents { ChainHash::using_genesis_block(Network::Bitcoin) } - pub(super) fn derives_keys(&self) -> bool { - self.payer.0.derives_keys() + pub fn amount_msats(&self) -> u64 { + self.amount_msats + } + + /// Features pertaining to requesting an invoice. + pub fn features(&self) -> &InvoiceRequestFeatures { + &self.features } - pub(super) fn payer_id(&self) -> PublicKey { + /// The quantity of an item that refund is for. + pub fn quantity(&self) -> Option { + self.quantity + } + + /// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly + /// transient pubkey. + /// + /// [`paths`]: Self::paths + pub fn payer_id(&self) -> PublicKey { self.payer_id } + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option { + self.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) + } + + pub(super) fn derives_keys(&self) -> bool { + self.payer.0.derives_keys() + } + pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef { let payer = PayerTlvStreamRef { metadata: self.payer.0.as_bytes(), @@ -745,7 +780,7 @@ mod tests { refund.write(&mut buffer).unwrap(); assert_eq!(refund.bytes, buffer.as_slice()); - assert_eq!(refund.metadata(), &[1; 32]); + assert_eq!(refund.payer_metadata(), &[1; 32]); assert_eq!(refund.description(), PrintableString("foo")); assert_eq!(refund.absolute_expiry(), None); #[cfg(feature = "std")] From 63d0d5583d2b18021434092face066ae92d5ae7b Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 11 Jul 2023 15:08:23 -0500 Subject: [PATCH 15/16] Use TaggedHash in merkle::verify_signature An earlier commit introduced TaggedHash for use in sign_message. For consistency, use it in verify_signature, too. --- lightning/src/offers/invoice.rs | 21 +++++++++------------ lightning/src/offers/invoice_request.rs | 11 +++++------ lightning/src/offers/merkle.rs | 10 ++++------ 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 05960642efd..745b389fa46 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1184,8 +1184,9 @@ impl TryFrom> for Bolt12Invoice { None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), Some(signature) => signature, }; + let message = TaggedHash::new(SIGNATURE_TAG, &bytes); let pubkey = contents.fields().signing_pubkey; - merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?; + merkle::verify_signature(&signature, message, pubkey)?; Ok(Bolt12Invoice { bytes, contents, signature }) } @@ -1288,7 +1289,7 @@ mod tests { use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; - use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; + use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; @@ -1400,11 +1401,9 @@ mod tests { assert_eq!(invoice.fallbacks(), vec![]); assert_eq!(invoice.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() - ); + + let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + assert!(merkle::verify_signature(&invoice.signature, message, recipient_pubkey()).is_ok()); let digest = Message::from_slice(&invoice.signable_hash()).unwrap(); let pubkey = recipient_pubkey().into(); @@ -1499,11 +1498,9 @@ mod tests { assert_eq!(invoice.fallbacks(), vec![]); assert_eq!(invoice.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() - ); + + let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + assert!(merkle::verify_signature(&invoice.signature, message, recipient_pubkey()).is_ok()); assert_eq!( invoice.as_tlv_stream(), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 198e15ec527..41c86171b1b 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -799,7 +799,8 @@ impl TryFrom> for InvoiceRequest { None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), Some(signature) => signature, }; - merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, contents.payer_id)?; + let message = TaggedHash::new(SIGNATURE_TAG, &bytes); + merkle::verify_signature(&signature, message, contents.payer_id)?; Ok(InvoiceRequest { bytes, contents, signature }) } @@ -933,11 +934,9 @@ mod tests { assert_eq!(invoice_request.quantity(), None); assert_eq!(invoice_request.payer_id(), payer_pubkey()); assert_eq!(invoice_request.payer_note(), None); - assert!( - merkle::verify_signature( - &invoice_request.signature, SIGNATURE_TAG, &invoice_request.bytes, payer_pubkey() - ).is_ok() - ); + + let message = TaggedHash::new(SIGNATURE_TAG, &invoice_request.bytes); + assert!(merkle::verify_signature(&invoice_request.signature, message, payer_pubkey()).is_ok()); assert_eq!( invoice_request.as_tlv_stream(), diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index b3867bf6f65..7390b58fef8 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -88,17 +88,15 @@ where Ok(signature) } -/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message +/// Verifies the signature with a pubkey over the given message using a tagged hash as the message /// digest. -/// -/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record. pub(super) fn verify_signature( - signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey, + signature: &Signature, message: TaggedHash, pubkey: PublicKey, ) -> Result<(), secp256k1::Error> { - let digest = message_digest(tag, bytes); + let digest = message.as_digest(); let pubkey = pubkey.into(); let secp_ctx = Secp256k1::verification_only(); - secp_ctx.verify_schnorr(signature, &digest, &pubkey) + secp_ctx.verify_schnorr(signature, digest, &pubkey) } pub(super) fn message_digest(tag: &str, bytes: &[u8]) -> Message { From 39012e35957922eea239c6ed33a6aaf16e7dee9c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 27 Feb 2023 12:10:32 -0600 Subject: [PATCH 16/16] Support signing BOLT 12 invoices in NodeSigner BOLT 12 messages need to be signed in the following scenarios: - constructing an InvoiceRequest after scanning an Offer, - constructing an Invoice after scanning a Refund, and - constructing an Invoice when handling an InvoiceRequest. Extend the NodeSigner trait to support signing BOLT 12 invoices such that it can be used in the latter contexts. The method could be used in an OffersMessageHandler. --- fuzz/src/chanmon_consistency.rs | 15 ++++++ fuzz/src/full_stack.rs | 15 ++++++ fuzz/src/onion_message.rs | 15 ++++++ lightning/src/offers/invoice.rs | 5 ++ lightning/src/offers/invoice_request.rs | 5 ++ lightning/src/sign/mod.rs | 65 ++++++++++++++++++++++++- lightning/src/util/test_utils.rs | 27 ++++++++++ 7 files changed, 146 insertions(+), 1 deletion(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index e923ef882f2..296b3a03e9c 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -44,6 +44,8 @@ use lightning::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; use lightning::ln::msgs::{self, CommitmentUpdate, ChannelMessageHandler, DecodeError, UpdateAddHTLC, Init}; use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; +use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::util::enforcing_trait_impls::{EnforcingSigner, EnforcementState}; use lightning::util::errors::APIError; use lightning::util::logger::Logger; @@ -57,6 +59,7 @@ use crate::utils::test_persister::TestPersister; use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::schnorr; use std::mem; use std::cmp::{self, Ordering}; @@ -211,6 +214,18 @@ impl NodeSigner for KeyProvider { unreachable!() } + fn sign_bolt12_invoice_request( + &self, _invoice_request: &UnsignedInvoiceRequest + ) -> Result { + unreachable!() + } + + fn sign_bolt12_invoice( + &self, _invoice: &UnsignedBolt12Invoice, + ) -> Result { + unreachable!() + } + fn sign_gossip_message(&self, msg: lightning::ln::msgs::UnsignedGossipMessage) -> Result { let msg_hash = Message::from_slice(&Sha256dHash::hash(&msg.encode()[..])[..]).map_err(|_| ())?; let secp_ctx = Secp256k1::signing_only(); diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 1fbd7dbec88..cf8060ab6b6 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -40,6 +40,8 @@ use lightning::ln::peer_handler::{MessageHandler,PeerManager,SocketDescriptor,Ig use lightning::ln::msgs::{self, DecodeError}; use lightning::ln::script::ShutdownScript; use lightning::ln::functional_test_utils::*; +use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::routing::gossip::{P2PGossipSync, NetworkGraph}; use lightning::routing::utxo::UtxoLookup; use lightning::routing::router::{InFlightHtlcs, PaymentParameters, Route, RouteParameters, Router}; @@ -55,6 +57,7 @@ use crate::utils::test_persister::TestPersister; use bitcoin::secp256k1::{Message, PublicKey, SecretKey, Scalar, Secp256k1}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::schnorr; use std::cell::RefCell; use hashbrown::{HashMap, hash_map}; @@ -316,6 +319,18 @@ impl NodeSigner for KeyProvider { unreachable!() } + fn sign_bolt12_invoice_request( + &self, _invoice_request: &UnsignedInvoiceRequest + ) -> Result { + unreachable!() + } + + fn sign_bolt12_invoice( + &self, _invoice: &UnsignedBolt12Invoice, + ) -> Result { + unreachable!() + } + fn sign_gossip_message(&self, msg: lightning::ln::msgs::UnsignedGossipMessage) -> Result { let msg_hash = Message::from_slice(&Sha256dHash::hash(&msg.encode()[..])[..]).map_err(|_| ())?; let secp_ctx = Secp256k1::signing_only(); diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index d323ecb21fb..0ffc090ea19 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -4,10 +4,13 @@ use bitcoin::blockdata::script::Script; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::RecoverableSignature; +use bitcoin::secp256k1::schnorr; use lightning::sign::{Recipient, KeyMaterial, EntropySource, NodeSigner, SignerProvider}; use lightning::ln::msgs::{self, DecodeError, OnionMessageHandler}; use lightning::ln::script::ShutdownScript; +use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::util::enforcing_trait_impls::EnforcingSigner; use lightning::util::logger::Logger; use lightning::util::ser::{Readable, Writeable, Writer}; @@ -153,6 +156,18 @@ impl NodeSigner for KeyProvider { unreachable!() } + fn sign_bolt12_invoice_request( + &self, _invoice_request: &UnsignedInvoiceRequest + ) -> Result { + unreachable!() + } + + fn sign_bolt12_invoice( + &self, _invoice: &UnsignedBolt12Invoice, + ) -> Result { + unreachable!() + } + fn sign_gossip_message(&self, _msg: lightning::ln::msgs::UnsignedGossipMessage) -> Result { unreachable!() } diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 745b389fa46..75a844cd117 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -397,6 +397,11 @@ impl UnsignedBolt12Invoice { Self { bytes, contents, tagged_hash } } + /// Returns the [`TaggedHash`] of the invoice to sign. + pub fn tagged_hash(&self) -> &TaggedHash { + &self.tagged_hash + } + /// Signs the [`TaggedHash`] of the invoice using the given function. /// /// Note: The hash computation may have included unknown, odd TLV records. diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 41c86171b1b..03af068d1d6 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -372,6 +372,11 @@ impl UnsignedInvoiceRequest { Self { bytes, contents, tagged_hash } } + /// Returns the [`TaggedHash`] of the invoice to sign. + pub fn tagged_hash(&self) -> &TaggedHash { + &self.tagged_hash + } + /// Signs the [`TaggedHash`] of the invoice request using the given function. /// /// Note: The hash computation may have included unknown, odd TLV records. diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index eb0b9cb6c6b..65df556174b 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -26,9 +26,10 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::WPubkeyHash; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; +use bitcoin::secp256k1::{KeyPair, PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::schnorr; use bitcoin::{PackedLockTime, secp256k1, Sequence, Witness}; use crate::util::transaction_utils; @@ -41,6 +42,8 @@ use crate::ln::{chan_utils, PaymentPreimage}; use crate::ln::chan_utils::{HTLCOutputInCommitment, make_funding_redeemscript, ChannelPublicKeys, HolderCommitmentTransaction, ChannelTransactionParameters, CommitmentTransaction, ClosingTransaction}; use crate::ln::msgs::{UnsignedChannelAnnouncement, UnsignedGossipMessage}; use crate::ln::script::ShutdownScript; +use crate::offers::invoice::UnsignedBolt12Invoice; +use crate::offers::invoice_request::UnsignedInvoiceRequest; use crate::prelude::*; use core::convert::TryInto; @@ -619,6 +622,36 @@ pub trait NodeSigner { /// Errors if the [`Recipient`] variant is not supported by the implementation. fn sign_invoice(&self, hrp_bytes: &[u8], invoice_data: &[u5], recipient: Recipient) -> Result; + /// Signs the [`TaggedHash`] of a BOLT 12 invoice request. + /// + /// May be called by a function passed to [`UnsignedInvoiceRequest::sign`] where + /// `invoice_request` is the callee. + /// + /// Implementors may check that the `invoice_request` is expected rather than blindly signing + /// the tagged hash. An `Ok` result should sign `invoice_request.tagged_hash().as_digest()` with + /// the node's signing key or an ephemeral key to preserve privacy, whichever is associated with + /// [`UnsignedInvoiceRequest::payer_id`]. + /// + /// [`TaggedHash`]: crate::offers::merkle::TaggedHash + fn sign_bolt12_invoice_request( + &self, invoice_request: &UnsignedInvoiceRequest + ) -> Result; + + /// Signs the [`TaggedHash`] of a BOLT 12 invoice. + /// + /// May be called by a function passed to [`UnsignedBolt12Invoice::sign`] where `invoice` is the + /// callee. + /// + /// Implementors may check that the `invoice` is expected rather than blindly signing the tagged + /// hash. An `Ok` result should sign `invoice.tagged_hash().as_digest()` with the node's signing + /// key or an ephemeral key to preserve privacy, whichever is associated with + /// [`UnsignedBolt12Invoice::signing_pubkey`]. + /// + /// [`TaggedHash`]: crate::offers::merkle::TaggedHash + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice + ) -> Result; + /// Sign a gossip message. /// /// Note that if this fails, LDK may panic and the message will not be broadcast to the network @@ -1449,6 +1482,24 @@ impl NodeSigner for KeysManager { Ok(self.secp_ctx.sign_ecdsa_recoverable(&hash_to_message!(&Sha256::hash(&preimage)), secret)) } + fn sign_bolt12_invoice_request( + &self, invoice_request: &UnsignedInvoiceRequest + ) -> Result { + let message = invoice_request.tagged_hash().as_digest(); + let keys = KeyPair::from_secret_key(&self.secp_ctx, &self.node_secret); + let aux_rand = self.get_secure_random_bytes(); + Ok(self.secp_ctx.sign_schnorr_with_aux_rand(message, &keys, &aux_rand)) + } + + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice + ) -> Result { + let message = invoice.tagged_hash().as_digest(); + let keys = KeyPair::from_secret_key(&self.secp_ctx, &self.node_secret); + let aux_rand = self.get_secure_random_bytes(); + Ok(self.secp_ctx.sign_schnorr_with_aux_rand(message, &keys, &aux_rand)) + } + fn sign_gossip_message(&self, msg: UnsignedGossipMessage) -> Result { let msg_hash = hash_to_message!(&Sha256dHash::hash(&msg.encode()[..])[..]); Ok(self.secp_ctx.sign_ecdsa(&msg_hash, &self.node_secret)) @@ -1557,6 +1608,18 @@ impl NodeSigner for PhantomKeysManager { Ok(self.inner.secp_ctx.sign_ecdsa_recoverable(&hash_to_message!(&Sha256::hash(&preimage)), secret)) } + fn sign_bolt12_invoice_request( + &self, invoice_request: &UnsignedInvoiceRequest + ) -> Result { + self.inner.sign_bolt12_invoice_request(invoice_request) + } + + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice + ) -> Result { + self.inner.sign_bolt12_invoice(invoice) + } + fn sign_gossip_message(&self, msg: UnsignedGossipMessage) -> Result { self.inner.sign_gossip_message(msg) } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 187abe19ffc..e7e29600dab 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -24,6 +24,8 @@ use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures}; use crate::ln::{msgs, wire}; use crate::ln::msgs::LightningError; use crate::ln::script::ShutdownScript; +use crate::offers::invoice::UnsignedBolt12Invoice; +use crate::offers::invoice_request::UnsignedInvoiceRequest; use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId}; use crate::routing::utxo::{UtxoLookup, UtxoLookupError, UtxoResult}; use crate::routing::router::{find_route, InFlightHtlcs, Path, Route, RouteParameters, Router, ScorerAccountingForInFlightHtlcs}; @@ -47,6 +49,7 @@ use bitcoin::util::sighash::SighashCache; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::schnorr; #[cfg(any(test, feature = "_test_utils"))] use regex; @@ -800,6 +803,18 @@ impl NodeSigner for TestNodeSigner { unreachable!() } + fn sign_bolt12_invoice_request( + &self, _invoice_request: &UnsignedInvoiceRequest + ) -> Result { + unreachable!() + } + + fn sign_bolt12_invoice( + &self, _invoice: &UnsignedBolt12Invoice, + ) -> Result { + unreachable!() + } + fn sign_gossip_message(&self, _msg: msgs::UnsignedGossipMessage) -> Result { unreachable!() } @@ -840,6 +855,18 @@ impl NodeSigner for TestKeysInterface { self.backing.sign_invoice(hrp_bytes, invoice_data, recipient) } + fn sign_bolt12_invoice_request( + &self, invoice_request: &UnsignedInvoiceRequest + ) -> Result { + self.backing.sign_bolt12_invoice_request(invoice_request) + } + + fn sign_bolt12_invoice( + &self, invoice: &UnsignedBolt12Invoice, + ) -> Result { + self.backing.sign_bolt12_invoice(invoice) + } + fn sign_gossip_message(&self, msg: msgs::UnsignedGossipMessage) -> Result { self.backing.sign_gossip_message(msg) }