Skip to content

Commit e447b49

Browse files
committed
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`
1 parent 46df35b commit e447b49

File tree

4 files changed

+43
-4
lines changed

4 files changed

+43
-4
lines changed

lightning/src/offers/invoice.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,7 @@ mod tests {
17661766
payer_id: Some(&payer_pubkey()),
17671767
payer_note: None,
17681768
paths: None,
1769+
offer_from_hrn: None,
17691770
},
17701771
InvoiceTlvStreamRef {
17711772
paths: Some(Iterable(payment_paths.iter().map(|path| path.inner_blinded_path()))),
@@ -1868,6 +1869,7 @@ mod tests {
18681869
payer_id: Some(&payer_pubkey()),
18691870
payer_note: None,
18701871
paths: None,
1872+
offer_from_hrn: None,
18711873
},
18721874
InvoiceTlvStreamRef {
18731875
paths: Some(Iterable(payment_paths.iter().map(|path| path.inner_blinded_path()))),

lightning/src/offers/invoice_request.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream,
7575
use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError};
7676
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
7777
use crate::offers::signer::{Metadata, MetadataMaterial};
78+
use crate::onion_message::dns_resolution::HumanReadableName;
7879
use crate::util::ser::{CursorReadable, HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
7980
use crate::util::string::{PrintableString, UntrustedString};
8081

@@ -241,6 +242,7 @@ macro_rules! invoice_request_builder_methods { (
241242
InvoiceRequestContentsWithoutPayerSigningPubkey {
242243
payer: PayerContents(metadata), offer, chain: None, amount_msats: None,
243244
features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None,
245+
offer_from_hrn: None,
244246
#[cfg(test)]
245247
experimental_bar: None,
246248
}
@@ -301,6 +303,14 @@ macro_rules! invoice_request_builder_methods { (
301303
$return_value
302304
}
303305

306+
/// Sets the [`InvoiceRequest::offer_from_hrn`].
307+
///
308+
/// Successive calls to this method will override the previous setting.
309+
pub fn sourced_from_human_readable_name($($self_mut)* $self: $self_type, hrn: HumanReadableName) -> $return_type {
310+
$self.invoice_request.offer_from_hrn = Some(hrn);
311+
$return_value
312+
}
313+
304314
fn build_with_checks($($self_mut)* $self: $self_type) -> Result<
305315
(UnsignedInvoiceRequest, Option<Keypair>, Option<&'b Secp256k1<$secp_context>>),
306316
Bolt12SemanticError
@@ -699,6 +709,7 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
699709
features: InvoiceRequestFeatures,
700710
quantity: Option<u64>,
701711
payer_note: Option<String>,
712+
offer_from_hrn: Option<HumanReadableName>,
702713
#[cfg(test)]
703714
experimental_bar: Option<u64>,
704715
}
@@ -745,6 +756,12 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
745756
pub fn payer_note(&$self) -> Option<PrintableString> {
746757
$contents.payer_note()
747758
}
759+
760+
/// If the [`Offer`] was sourced from a BIP 353 Human Readable Name, this should be set by the
761+
/// builder to indicate the original [`HumanReadableName`] which was resolved.
762+
pub fn offer_from_hrn(&$self) -> &Option<HumanReadableName> {
763+
$contents.offer_from_hrn()
764+
}
748765
} }
749766

750767
impl UnsignedInvoiceRequest {
@@ -1004,9 +1021,7 @@ impl VerifiedInvoiceRequest {
10041021
let InvoiceRequestContents {
10051022
payer_signing_pubkey,
10061023
inner: InvoiceRequestContentsWithoutPayerSigningPubkey {
1007-
payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note,
1008-
#[cfg(test)]
1009-
experimental_bar: _,
1024+
quantity, payer_note, ..
10101025
},
10111026
} = &self.inner.contents;
10121027

@@ -1049,6 +1064,10 @@ impl InvoiceRequestContents {
10491064
.map(|payer_note| PrintableString(payer_note.as_str()))
10501065
}
10511066

1067+
pub(super) fn offer_from_hrn(&self) -> &Option<HumanReadableName> {
1068+
&self.inner.offer_from_hrn
1069+
}
1070+
10521071
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
10531072
let (payer, offer, mut invoice_request, experimental_offer, experimental_invoice_request) =
10541073
self.inner.as_tlv_stream();
@@ -1085,6 +1104,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
10851104
quantity: self.quantity,
10861105
payer_id: None,
10871106
payer_note: self.payer_note.as_ref(),
1107+
offer_from_hrn: self.offer_from_hrn.as_ref(),
10881108
paths: None,
10891109
};
10901110

@@ -1142,6 +1162,7 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQ
11421162
(89, payer_note: (String, WithoutLength)),
11431163
// Only used for Refund since the onion message of an InvoiceRequest has a reply path.
11441164
(90, paths: (Vec<BlindedMessagePath>, WithoutLength)),
1165+
(91, offer_from_hrn: HumanReadableName),
11451166
});
11461167

11471168
/// Valid type range for experimental invoice_request TLV records.
@@ -1266,6 +1287,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
12661287
offer_tlv_stream,
12671288
InvoiceRequestTlvStream {
12681289
chain, amount, features, quantity, payer_id, payer_note, paths,
1290+
offer_from_hrn,
12691291
},
12701292
experimental_offer_tlv_stream,
12711293
ExperimentalInvoiceRequestTlvStream {
@@ -1305,6 +1327,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
13051327
Ok(InvoiceRequestContents {
13061328
inner: InvoiceRequestContentsWithoutPayerSigningPubkey {
13071329
payer, offer, chain, amount_msats: amount, features, quantity, payer_note,
1330+
offer_from_hrn,
13081331
#[cfg(test)]
13091332
experimental_bar,
13101333
},
@@ -1484,6 +1507,7 @@ mod tests {
14841507
payer_id: Some(&payer_pubkey()),
14851508
payer_note: None,
14861509
paths: None,
1510+
offer_from_hrn: None,
14871511
},
14881512
SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) },
14891513
ExperimentalOfferTlvStreamRef {

lightning/src/offers/parse.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ pub enum Bolt12SemanticError {
198198
InvalidSigningPubkey,
199199
/// A signature was expected but was missing.
200200
MissingSignature,
201+
/// A Human Readable Name was provided but was not expected (i.e. was included in a
202+
/// [`Refund`]).
203+
///
204+
/// [`Refund`]: super::refund::Refund
205+
UnexpectedHumanReadableName,
201206
}
202207

203208
impl From<CheckedHrpstringError> for Bolt12ParseError {

lightning/src/offers/refund.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ impl RefundContents {
792792
payer_id: Some(&self.payer_signing_pubkey),
793793
payer_note: self.payer_note.as_ref(),
794794
paths: self.paths.as_ref(),
795+
offer_from_hrn: None,
795796
};
796797

797798
let experimental_offer = ExperimentalOfferTlvStreamRef {
@@ -888,7 +889,8 @@ impl TryFrom<RefundTlvStream> for RefundContents {
888889
issuer_id,
889890
},
890891
InvoiceRequestTlvStream {
891-
chain, amount, features, quantity, payer_id, payer_note, paths
892+
chain, amount, features, quantity, payer_id, payer_note, paths,
893+
offer_from_hrn,
892894
},
893895
ExperimentalOfferTlvStream {
894896
#[cfg(test)]
@@ -940,6 +942,11 @@ impl TryFrom<RefundTlvStream> for RefundContents {
940942
return Err(Bolt12SemanticError::UnexpectedIssuerSigningPubkey);
941943
}
942944

945+
if offer_from_hrn.is_some() {
946+
// Only offers can be resolved using Human Readable Names
947+
return Err(Bolt12SemanticError::UnexpectedHumanReadableName);
948+
}
949+
943950
let amount_msats = match amount {
944951
None => return Err(Bolt12SemanticError::MissingAmount),
945952
Some(amount_msats) if amount_msats > MAX_VALUE_MSAT => {
@@ -1066,6 +1073,7 @@ mod tests {
10661073
payer_id: Some(&payer_pubkey()),
10671074
payer_note: None,
10681075
paths: None,
1076+
offer_from_hrn: None,
10691077
},
10701078
ExperimentalOfferTlvStreamRef {
10711079
experimental_foo: None,

0 commit comments

Comments
 (0)