Add support for storing a source HRN in BOLT 12 invoice_requests

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`
This commit is contained in:
Matt Corallo 2024-07-13 14:04:39 +00:00
parent 46df35b0ff
commit e447b49136
4 changed files with 43 additions and 4 deletions

View file

@ -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()))),

View file

@ -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<Keypair>, Option<&'b Secp256k1<$secp_context>>),
Bolt12SemanticError
@ -699,6 +709,7 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
features: InvoiceRequestFeatures,
quantity: Option<u64>,
payer_note: Option<String>,
offer_from_hrn: Option<HumanReadableName>,
#[cfg(test)]
experimental_bar: Option<u64>,
}
@ -745,6 +756,12 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
pub fn payer_note(&$self) -> Option<PrintableString> {
$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<HumanReadableName> {
$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<HumanReadableName> {
&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<BlindedMessagePath>, WithoutLength)),
(91, offer_from_hrn: HumanReadableName),
});
/// Valid type range for experimental invoice_request TLV records.
@ -1266,6 +1287,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> 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<PartialInvoiceRequestTlvStream> 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 {

View file

@ -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<CheckedHrpstringError> for Bolt12ParseError {

View file

@ -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<RefundTlvStream> 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<RefundTlvStream> 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,