From: Matt Corallo Date: Sat, 13 Jul 2024 14:04:39 +0000 (+0000) Subject: Add support for storing a source HRN in BOLT 12 `invoice_request`s X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=e447b4913688bbb49b2dcfe4c9b14b0b7fdc59fc;p=rust-lightning Add support for storing a source HRN in BOLT 12 `invoice_request`s When we resolve a Human Readable Name to a BOLT 12 `offer`, we may end up resolving to a wildcard DNS name covering all possible `user` parts. In that case, if we just blindly pay the `offer`, the recipient would have no way to tell which `user` we paid. Instead, BOLT 12 defines a field to include the HRN resolved in the `invoice_request`, which we implement here. We also take this opportunity to remove constant parameters from the `outbound_payment.rs` interface to `channelmanager.rs` --- diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index b1d7660f2..e7e459690 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1766,6 +1766,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + offer_from_hrn: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|path| path.inner_blinded_path()))), @@ -1868,6 +1869,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + offer_from_hrn: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|path| path.inner_blinded_path()))), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index d1ab6d067..c269eb01b 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -75,6 +75,7 @@ use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError}; use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; use crate::offers::signer::{Metadata, MetadataMaterial}; +use crate::onion_message::dns_resolution::HumanReadableName; use crate::util::ser::{CursorReadable, HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::{PrintableString, UntrustedString}; @@ -241,6 +242,7 @@ macro_rules! invoice_request_builder_methods { ( InvoiceRequestContentsWithoutPayerSigningPubkey { payer: PayerContents(metadata), offer, chain: None, amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None, + offer_from_hrn: None, #[cfg(test)] experimental_bar: None, } @@ -301,6 +303,14 @@ macro_rules! invoice_request_builder_methods { ( $return_value } + /// Sets the [`InvoiceRequest::offer_from_hrn`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn sourced_from_human_readable_name($($self_mut)* $self: $self_type, hrn: HumanReadableName) -> $return_type { + $self.invoice_request.offer_from_hrn = Some(hrn); + $return_value + } + fn build_with_checks($($self_mut)* $self: $self_type) -> Result< (UnsignedInvoiceRequest, Option, Option<&'b Secp256k1<$secp_context>>), Bolt12SemanticError @@ -699,6 +709,7 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey { features: InvoiceRequestFeatures, quantity: Option, payer_note: Option, + offer_from_hrn: Option, #[cfg(test)] experimental_bar: Option, } @@ -745,6 +756,12 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => { pub fn payer_note(&$self) -> Option { $contents.payer_note() } + + /// If the [`Offer`] was sourced from a BIP 353 Human Readable Name, this should be set by the + /// builder to indicate the original [`HumanReadableName`] which was resolved. + pub fn offer_from_hrn(&$self) -> &Option { + $contents.offer_from_hrn() + } } } impl UnsignedInvoiceRequest { @@ -1004,9 +1021,7 @@ impl VerifiedInvoiceRequest { let InvoiceRequestContents { payer_signing_pubkey, inner: InvoiceRequestContentsWithoutPayerSigningPubkey { - payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note, - #[cfg(test)] - experimental_bar: _, + quantity, payer_note, .. }, } = &self.inner.contents; @@ -1049,6 +1064,10 @@ impl InvoiceRequestContents { .map(|payer_note| PrintableString(payer_note.as_str())) } + pub(super) fn offer_from_hrn(&self) -> &Option { + &self.inner.offer_from_hrn + } + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { let (payer, offer, mut invoice_request, experimental_offer, experimental_invoice_request) = self.inner.as_tlv_stream(); @@ -1085,6 +1104,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { quantity: self.quantity, payer_id: None, payer_note: self.payer_note.as_ref(), + offer_from_hrn: self.offer_from_hrn.as_ref(), paths: None, }; @@ -1142,6 +1162,7 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ (89, payer_note: (String, WithoutLength)), // Only used for Refund since the onion message of an InvoiceRequest has a reply path. (90, paths: (Vec, WithoutLength)), + (91, offer_from_hrn: HumanReadableName), }); /// Valid type range for experimental invoice_request TLV records. @@ -1266,6 +1287,7 @@ impl TryFrom for InvoiceRequestContents { offer_tlv_stream, InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths, + offer_from_hrn, }, experimental_offer_tlv_stream, ExperimentalInvoiceRequestTlvStream { @@ -1305,6 +1327,7 @@ impl TryFrom for InvoiceRequestContents { Ok(InvoiceRequestContents { inner: InvoiceRequestContentsWithoutPayerSigningPubkey { payer, offer, chain, amount_msats: amount, features, quantity, payer_note, + offer_from_hrn, #[cfg(test)] experimental_bar, }, @@ -1484,6 +1507,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + offer_from_hrn: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, ExperimentalOfferTlvStreamRef { diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 7c9d80387..3828ecbdf 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -198,6 +198,11 @@ pub enum Bolt12SemanticError { InvalidSigningPubkey, /// A signature was expected but was missing. MissingSignature, + /// A Human Readable Name was provided but was not expected (i.e. was included in a + /// [`Refund`]). + /// + /// [`Refund`]: super::refund::Refund + UnexpectedHumanReadableName, } impl From for Bolt12ParseError { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index b1f5b0520..7a47ad99b 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -792,6 +792,7 @@ impl RefundContents { payer_id: Some(&self.payer_signing_pubkey), payer_note: self.payer_note.as_ref(), paths: self.paths.as_ref(), + offer_from_hrn: None, }; let experimental_offer = ExperimentalOfferTlvStreamRef { @@ -888,7 +889,8 @@ impl TryFrom for RefundContents { issuer_id, }, InvoiceRequestTlvStream { - chain, amount, features, quantity, payer_id, payer_note, paths + chain, amount, features, quantity, payer_id, payer_note, paths, + offer_from_hrn, }, ExperimentalOfferTlvStream { #[cfg(test)] @@ -940,6 +942,11 @@ impl TryFrom for RefundContents { return Err(Bolt12SemanticError::UnexpectedIssuerSigningPubkey); } + if offer_from_hrn.is_some() { + // Only offers can be resolved using Human Readable Names + return Err(Bolt12SemanticError::UnexpectedHumanReadableName); + } + let amount_msats = match amount { None => return Err(Bolt12SemanticError::MissingAmount), Some(amount_msats) if amount_msats > MAX_VALUE_MSAT => { @@ -1066,6 +1073,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + offer_from_hrn: None, }, ExperimentalOfferTlvStreamRef { experimental_foo: None,