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/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/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/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..75a844cd117 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,21 +101,21 @@ 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; 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, TlvStream, WithoutSignatures, self}; -use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef}; +use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; +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}; @@ -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 @@ -142,8 +147,7 @@ pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", " 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. @@ -159,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 {} @@ -178,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( @@ -193,7 +197,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { ), }; - Self::new(&refund.bytes, contents, None) + Self::new(&refund.bytes, contents, ExplicitSigningPubkey {}) } } @@ -211,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( @@ -227,7 +231,7 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { ), }; - Self::new(&refund.bytes, contents, Some(keys)) + Self::new(&refund.bytes, contents, DerivedSigningPubkey(keys)) } } @@ -257,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`]. @@ -321,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 @@ -331,7 +331,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 +339,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { } let InvoiceBuilder { invreq_bytes, invoice, .. } = self; - Ok(UnsignedBolt12Invoice { invreq_bytes, invoice }) + Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice)) } } @@ -354,63 +354,86 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { } } - let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; - let unsigned_invoice = UnsignedBolt12Invoice { invreq_bytes, invoice }; + 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>(|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, - invoice: InvoiceContents, +/// +/// # 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, + tagged_hash: TaggedHash, } -impl<'a> UnsignedBolt12Invoice<'a> { - /// The public key corresponding to the key needed to sign the invoice. - pub fn signing_pubkey(&self) -> PublicKey { - self.invoice.fields().signing_pubkey - } - - /// 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> - where - F: FnOnce(&Message) -> Result - { +impl UnsignedBolt12Invoice { + 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) = self.invoice.as_tlv_stream(); - let invoice_request_bytes = WithoutSignatures(self.invreq_bytes); + 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); 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 tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + 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. + /// + /// This is not exported to bindings users as functions aren't currently mapped. + pub fn sign(mut self, sign: F) -> Result> + where + F: FnOnce(&Self) -> Result + { + let pubkey = self.contents.fields().signing_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, - contents: self.invoice, + bytes: self.bytes, + contents: self.contents, 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 @@ -463,11 +486,140 @@ struct InvoiceFields { signing_pubkey: PublicKey, } -impl Bolt12Invoice { - /// 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() +macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { + /// 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 @@ -478,108 +630,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.fields().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.fields().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.fields().relative_expiry.unwrap_or(DEFAULT_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 { - 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, - } + 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.fields().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.fields().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
{ - 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 - } + pub fn fallbacks(&$self) -> Vec
{ + $contents.fallbacks() } /// Features pertaining to paying an invoice. - pub fn features(&self) -> &Bolt12InvoiceFeatures { - &self.contents.fields().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.fields().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 { @@ -621,6 +725,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(), @@ -628,6 +740,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, .. } => { @@ -637,6 +765,172 @@ 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[..] + } + + 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, @@ -718,6 +1012,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) @@ -730,6 +1030,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; @@ -842,6 +1161,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; @@ -859,8 +1189,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 }) } @@ -946,8 +1277,9 @@ 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::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; @@ -958,12 +1290,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::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; + use crate::offers::offer::{Amount, OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; @@ -992,21 +1324,78 @@ 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(); + + 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); + #[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) => { + 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(); 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); @@ -1015,13 +1404,11 @@ 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( - &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(); @@ -1089,7 +1476,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); @@ -1098,13 +1501,11 @@ 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( - &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(), @@ -1446,7 +1847,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)); } @@ -1666,7 +2067,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), } @@ -1686,15 +2087,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 +2103,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(); @@ -1788,7 +2188,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 f014bf12002..03af068d1d6 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,77 @@ 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, - invoice_request: InvoiceRequestContents, +/// +/// # 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, + 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, 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) = - self.invoice_request.as_tlv_stream(); - let offer_bytes = WithoutLength(&self.offer.bytes); + contents.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 pubkey = self.invoice_request.payer_id; - let signature = merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?; + let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + + 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. + /// + /// 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.contents.payer_id; + 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, - contents: self.invoice_request, + bytes: self.bytes, + contents: self.contents, 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 @@ -418,49 +446,58 @@ 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.inner.amount_msats + pub fn amount_msats(&$self) -> Option { + $contents.amount_msats() } /// Features pertaining to requesting an invoice. - pub fn features(&self) -> &InvoiceRequestFeatures { - &self.contents.inner.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.inner.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.inner.payer_note.as_ref() - .map(|payer_note| PrintableString(payer_note.as_str())) + pub fn payer_note(&$self) -> Option { + $contents.payer_note() } +} } + +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`]. /// @@ -512,7 +549,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); } @@ -555,7 +592,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); } @@ -591,7 +628,7 @@ impl InvoiceRequest { } impl InvoiceRequestContents { - pub fn metadata(&self) -> &[u8] { + pub(super) fn metadata(&self) -> &[u8] { self.inner.metadata() } @@ -603,10 +640,27 @@ impl InvoiceRequestContents { self.inner.chain() } + pub(super) fn amount_msats(&self) -> Option { + self.inner.amount_msats + } + + pub(super) fn features(&self) -> &InvoiceRequestFeatures { + &self.inner.features + } + + pub(super) fn quantity(&self) -> Option { + self.inner.quantity + } + pub(super) fn payer_id(&self) -> PublicKey { self.payer_id } + pub(super) 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); @@ -648,6 +702,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) @@ -707,6 +767,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; @@ -725,7 +804,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 }) } @@ -776,7 +856,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; @@ -786,11 +866,11 @@ 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}; - 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; @@ -800,29 +880,68 @@ 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(); + + 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) => { + 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(); 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.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.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); - 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(), @@ -922,9 +1041,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 +1064,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 +1109,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 +1132,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; @@ -1219,7 +1334,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()) @@ -1231,7 +1346,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); } @@ -1669,7 +1784,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(); @@ -1690,7 +1805,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(); @@ -1709,7 +1824,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(); @@ -1731,7 +1846,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) { @@ -1771,7 +1886,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..7390b58fef8 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,34 @@ 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 +#[derive(Debug, PartialEq)] +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,37 +62,41 @@ 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`. +/// +/// 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`]. /// -/// 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, +/// [`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) } -/// 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 { @@ -207,12 +240,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 +304,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 +339,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 +371,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/mod.rs b/lightning/src/offers/mod.rs index 31d8bf9cbdf..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; -mod merkle; -pub mod offer; +pub mod merkle; pub mod parse; mod payer; pub mod refund; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d801be9d26f..f6aa354b9e4 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.as_ref().map(|issuer| PrintableString(issuer.as_str())) + 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.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + 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,24 +452,19 @@ 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 - /// - 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>( @@ -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); } @@ -551,10 +555,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 +582,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( @@ -874,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()); @@ -1115,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)) @@ -1123,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); } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index bfc02b5dbcb..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::metadata`] and [`Refund::metadata`]. +/// TLV record type for [`InvoiceRequest::payer_metadata`] and [`Refund::payer_metadata`]. /// -/// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata -/// [`Refund::metadata`]: crate::offers::refund::Refund::metadata +/// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_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")] 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 { 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())) } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index a71bdae8876..65df556174b 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -26,10 +26,10 @@ 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::{KeyPair, PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::secp256k1::ecdh::SharedSecret; -use bitcoin::secp256k1::ecdsa::RecoverableSignature; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::schnorr; use bitcoin::{PackedLockTime, secp256k1, Sequence, Witness}; use crate::util::transaction_utils; @@ -42,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; @@ -620,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 @@ -1450,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)) @@ -1558,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 65c0483a59c..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}; @@ -44,9 +46,10 @@ 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}; +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) }