From 547d166aaae7a957a42f1f5f8741cd7dec5cb5bd Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Thu, 22 Jan 2026 11:01:06 +0100 Subject: [PATCH 1/2] `DefaultMessageRouter`: Make compact blinded paths configurable BOLT12 offers are designed for compactness to fit in QR codes, and can be "much longer-lived than a particular invoice". These goals are in tension when constructing blinded paths: BOLT04 allows using either `short_channel_id` or `next_node_id` in the encrypted_data_tlv for routing, but each has trade-offs. Using SCIDs produces smaller encoded offers suitable for QR codes, but if the referenced channel closes or is modified (e.g., via splicing), the blinded path breaks and the offer becomes unusable. Using full node IDs produces larger offers but paths remain valid as long as the node connection persists, regardless of specific channel lifecycle. For long-lived offers where stability matters more than size, users may prefer full node ID paths. This adds a `compact_paths` field to `DefaultMessageRouter` and a `with_compact_paths` constructor to allow downstream applications to choose their preferred trade-off. The default behavior (compact paths enabled) is preserved. --- lightning/src/onion_message/messenger.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f94eb7877f5..a54d6a33eb6 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -526,6 +526,7 @@ impl> MessageRouter for R { pub struct DefaultMessageRouter>, L: Logger, ES: EntropySource> { network_graph: G, entropy_source: ES, + compact_paths: bool, } // Target total length (in hops) for blinded paths used outside of QR codes. @@ -543,8 +544,24 @@ impl>, L: Logger, ES: EntropySource> DefaultMessageRouter { /// Creates a [`DefaultMessageRouter`] using the given [`NetworkGraph`]. + /// + /// Compact blinded paths are enabled by default. Use [`Self::with_compact_paths`] to + /// configure this behavior. pub fn new(network_graph: G, entropy_source: ES) -> Self { - Self { network_graph, entropy_source } + Self { network_graph, entropy_source, compact_paths: true } + } + + /// Creates a [`DefaultMessageRouter`] with configurable compact blinded paths behavior. + /// + /// When `compact_paths` is `true`, blinded paths will use short channel IDs (SCIDs) instead + /// of full node pubkeys when possible, resulting in smaller serialization suitable for + /// space-constrained formats like QR codes. However, compact paths may fail to route if + /// the corresponding channel is closed or modified. + /// + /// When `compact_paths` is `false`, blinded paths will always use full node IDs, which + /// are more stable for long-lived offers but result in larger encoded data. + pub fn with_compact_paths(network_graph: G, entropy_source: ES, compact_paths: bool) -> Self { + Self { network_graph, entropy_source, compact_paths } } pub(crate) fn create_blinded_paths_from_iter< @@ -727,7 +744,7 @@ impl>, L: Logger, ES: EntropySource> MessageRo peers.into_iter(), &self.entropy_source, secp_ctx, - false, + !self.compact_paths, ) } } From 9fd93e8abad20a53fc8fc2f0ce6caf0841a83c33 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Wed, 25 Mar 2026 13:44:44 +0100 Subject: [PATCH 2/2] Include DNSSECProof in PaymentSent for BIP 353 proof of payment Store the DNSSEC proof received during BIP 353 Human Readable Name resolution and carry it through the payment lifecycle alongside the Bolt12Invoice. The proof is stored in PendingOutboundPayment::Retryable and HTLCSource::OutboundRoute (for restart persistence), and emitted in the PaymentSent event. This allows users who pay via HRN to have a complete chain of proof: DNS name -> DNSSEC proof -> Offer -> Invoice -> Payment preimage. Addresses the DNSSECProof part of #3344. Co-Authored-By: Claude Opus 4.6 (1M context) --- lightning/src/events/mod.rs | 12 +++ lightning/src/ln/channel.rs | 2 + lightning/src/ln/channelmanager.rs | 41 +++++++-- lightning/src/ln/functional_test_utils.rs | 1 + lightning/src/ln/onion_utils.rs | 4 + lightning/src/ln/outbound_payment.rs | 88 ++++++++++++++----- lightning/src/onion_message/dns_resolution.rs | 17 ++++ 7 files changed, 140 insertions(+), 25 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 011b7f595bc..78898eaf966 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -32,6 +32,7 @@ use crate::ln::types::ChannelId; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::static_invoice::StaticInvoice; +use crate::onion_message::dns_resolution::DNSSECProof; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters}; @@ -1101,6 +1102,12 @@ pub enum Event { /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice bolt12_invoice: Option, + /// The DNSSEC proof for BIP 353 proof of payment, if this payment originated from + /// a Human Readable Name resolution. This proof, combined with the [`Bolt12Invoice`], + /// provides a complete chain of proof from the DNS name to the payment. + /// + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + dnssec_proof: Option, }, /// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events /// provide failure information for each path attempt in the payment, including retries. @@ -1973,6 +1980,7 @@ impl Writeable for Event { ref amount_msat, ref fee_paid_msat, ref bolt12_invoice, + ref dnssec_proof, } => { 2u8.write(writer)?; write_tlv_fields!(writer, { @@ -1982,6 +1990,7 @@ impl Writeable for Event { (5, fee_paid_msat, option), (7, amount_msat, option), (9, bolt12_invoice, option), + (11, dnssec_proof, option), }); }, &Event::PaymentPathFailed { @@ -2474,6 +2483,7 @@ impl MaybeReadable for Event { let mut amount_msat = None; let mut fee_paid_msat = None; let mut bolt12_invoice = None; + let mut dnssec_proof: Option = None; read_tlv_fields!(reader, { (0, payment_preimage, required), (1, payment_hash, option), @@ -2481,6 +2491,7 @@ impl MaybeReadable for Event { (5, fee_paid_msat, option), (7, amount_msat, option), (9, bolt12_invoice, option), + (11, dnssec_proof, option), }); if payment_hash.is_none() { payment_hash = Some(PaymentHash( @@ -2494,6 +2505,7 @@ impl MaybeReadable for Event { amount_msat, fee_paid_msat, bolt12_invoice, + dnssec_proof, })) }; f() diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 8b05d984e30..b15b7d45613 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -16535,6 +16535,7 @@ mod tests { first_hop_htlc_msat: 548, payment_id: PaymentId([42; 32]), bolt12_invoice: None, + dnssec_proof: None, }, skimmed_fee_msat: None, blinding_point: None, @@ -16986,6 +16987,7 @@ mod tests { first_hop_htlc_msat: 0, payment_id: PaymentId([42; 32]), bolt12_invoice: None, + dnssec_proof: None, }; let dummy_outbound_output = OutboundHTLCOutput { htlc_id: 0, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f9772bb120b..1bba6ff3e61 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -108,7 +108,7 @@ use crate::onion_message::async_payments::{ AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted, }; -use crate::onion_message::dns_resolution::HumanReadableName; +use crate::onion_message::dns_resolution::{DNSSECProof, HumanReadableName}; use crate::onion_message::messenger::{ MessageRouter, MessageSendInstructions, Responder, ResponseInstruction, }; @@ -836,6 +836,10 @@ mod fuzzy_channelmanager { /// we can provide proof-of-payment details in payment claim events even after a restart /// with a stale ChannelManager state. bolt12_invoice: Option, + /// The DNSSEC proof for BIP 353 proof of payment, if this payment originated from + /// a Human Readable Name resolution. Stored here to ensure we can provide it in + /// payment claim events even after a restart with a stale ChannelManager state. + dnssec_proof: Option, }, } @@ -909,6 +913,7 @@ impl core::hash::Hash for HTLCSource { payment_id, first_hop_htlc_msat, bolt12_invoice, + dnssec_proof, } => { 1u8.hash(hasher); path.hash(hasher); @@ -916,6 +921,7 @@ impl core::hash::Hash for HTLCSource { payment_id.hash(hasher); first_hop_htlc_msat.hash(hasher); bolt12_invoice.hash(hasher); + dnssec_proof.hash(hasher); }, HTLCSource::TrampolineForward { previous_hop_data, @@ -943,6 +949,7 @@ impl HTLCSource { first_hop_htlc_msat: 0, payment_id: PaymentId([2; 32]), bolt12_invoice: None, + dnssec_proof: None, } } @@ -5394,6 +5401,7 @@ impl< keysend_preimage, invoice_request: None, bolt12_invoice: None, + dnssec_proof: None, session_priv_bytes, hold_htlc_at_next_hop: false, }) @@ -5409,6 +5417,7 @@ impl< keysend_preimage, invoice_request, bolt12_invoice, + dnssec_proof, session_priv_bytes, hold_htlc_at_next_hop, } = args; @@ -5485,6 +5494,7 @@ impl< first_hop_htlc_msat: htlc_msat, payment_id, bolt12_invoice: bolt12_invoice.cloned(), + dnssec_proof: dnssec_proof.cloned(), }; let send_res = chan.send_htlc_and_commit( htlc_msat, @@ -9614,7 +9624,8 @@ impl< ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -9652,7 +9663,8 @@ impl< ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCClaimSource, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -9952,7 +9964,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let htlc_id = SentHTLCId::from_source(&source); match source { HTLCSource::OutboundRoute { - session_priv, payment_id, path, bolt12_invoice, .. + session_priv, + payment_id, + path, + bolt12_invoice, + dnssec_proof, + .. } => { debug_assert!(!startup_replay, "We don't support claim_htlc claims during startup - monitors may not be available yet"); @@ -9984,6 +10001,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ payment_id, payment_preimage, bolt12_invoice, + dnssec_proof, session_priv, path, from_onchain, @@ -17386,6 +17404,7 @@ impl< #[rustfmt::skip] fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { + let proof = message.clone(); let offer_opt = self.flow.hrn_resolver.handle_dnssec_proof_for_offer(message, context); #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] if let Some((completed_requests, mut offer)) = offer_opt { @@ -17404,7 +17423,12 @@ impl< .received_offer(payment_id, Some(retryable_invoice_request)) .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) }); - if offer_pay_res.is_err() { + if offer_pay_res.is_ok() { + // Store the DNSSEC proof for BIP 353 proof of payment. The proof is + // now attached to the AwaitingInvoice state and will be carried through + // to the PaymentSent event. + self.pending_outbound_payments.set_dnssec_proof(payment_id, proof.clone()); + } else { // The offer we tried to pay is the canonical current offer for the name we // wanted to pay. If we can't pay it, there's no way to recover so fail the // payment. @@ -17767,6 +17791,7 @@ impl Readable for HTLCSource { let mut payment_params: Option = None; let mut blinded_tail: Option = None; let mut bolt12_invoice: Option = None; + let mut dnssec_proof: Option = None; read_tlv_fields!(reader, { (0, session_priv, required), (1, payment_id, option), @@ -17775,6 +17800,7 @@ impl Readable for HTLCSource { (5, payment_params, (option: ReadableArgs, 0)), (6, blinded_tail, option), (7, bolt12_invoice, option), + (9, dnssec_proof, option), }); if payment_id.is_none() { // For backwards compat, if there was no payment_id written, use the session_priv bytes @@ -17798,6 +17824,7 @@ impl Readable for HTLCSource { path, payment_id: payment_id.unwrap(), bolt12_invoice, + dnssec_proof, }) } 1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), @@ -17817,6 +17844,7 @@ impl Writeable for HTLCSource { ref path, payment_id, bolt12_invoice, + dnssec_proof, } => { 0u8.write(writer)?; let payment_id_opt = Some(payment_id); @@ -17829,6 +17857,7 @@ impl Writeable for HTLCSource { (5, None::, option), // payment_params in LDK versions prior to 0.0.115 (6, path.blinded_tail, option), (7, bolt12_invoice, option), + (9, dnssec_proof, option), }); }, HTLCSource::PreviousHopData(ref field) => { @@ -19687,6 +19716,7 @@ impl< session_priv, path, bolt12_invoice, + dnssec_proof, .. } => { if let Some(preimage) = preimage_opt { @@ -19704,6 +19734,7 @@ impl< payment_id, preimage, bolt12_invoice, + dnssec_proof, session_priv, path, true, diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e8859494071..fc59ef044a7 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3025,6 +3025,7 @@ pub fn expect_payment_sent>( ref amount_msat, ref fee_paid_msat, ref bolt12_invoice, + .. } => { assert_eq!(expected_payment_preimage, *payment_preimage); assert_eq!(expected_payment_hash, *payment_hash); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 9b1b009e93a..68f404e549b 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -3547,6 +3547,7 @@ mod tests { first_hop_htlc_msat: 0, payment_id: PaymentId([1; 32]), bolt12_invoice: None, + dnssec_proof: None, }; process_onion_failure(&ctx_full, &logger, &htlc_source, onion_error) @@ -3733,6 +3734,7 @@ mod tests { first_hop_htlc_msat: dummy_amt_msat, payment_id: PaymentId([1; 32]), bolt12_invoice: None, + dnssec_proof: None, }; { @@ -3921,6 +3923,7 @@ mod tests { first_hop_htlc_msat: 0, payment_id: PaymentId([1; 32]), bolt12_invoice: None, + dnssec_proof: None, }; // Iterate over all possible failure positions and check that the cases that can be attributed are. @@ -4030,6 +4033,7 @@ mod tests { first_hop_htlc_msat: 0, payment_id: PaymentId([1; 32]), bolt12_invoice: None, + dnssec_proof: None, }; let decrypted_failure = process_onion_failure(&ctx_full, &logger, &htlc_source, packet); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index b08b0f5a886..00e5bd3c841 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -28,6 +28,7 @@ use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::static_invoice::StaticInvoice; +use crate::onion_message::dns_resolution::DNSSECProof; use crate::routing::router::{ BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, RouteParametersConfig, Router, @@ -85,6 +86,9 @@ pub(crate) enum PendingOutboundPayment { retry_strategy: Retry, route_params_config: RouteParametersConfig, retryable_invoice_request: Option, + /// The DNSSEC proof for BIP 353 proof of payment, if this payment originated from + /// a Human Readable Name resolution. + dnssec_proof: Option, }, // Represents the state after the invoice has been received, transitioning from the corresponding // `AwaitingInvoice` state. @@ -127,6 +131,9 @@ pub(crate) enum PendingOutboundPayment { // Storing the BOLT 12 invoice here to allow Proof of Payment after // the payment is made. bolt12_invoice: Option, + /// The DNSSEC proof for BIP 353 proof of payment, if this payment originated from + /// a Human Readable Name resolution. + dnssec_proof: Option, custom_tlvs: Vec<(u64, Vec)>, pending_amt_msat: u64, /// Used to track the fee paid. Present iff the payment was serialized on 0.0.103+. @@ -188,6 +195,13 @@ impl PendingOutboundPayment { } } + fn dnssec_proof(&self) -> Option<&DNSSECProof> { + match self { + PendingOutboundPayment::Retryable { dnssec_proof, .. } => dnssec_proof.as_ref(), + _ => None, + } + } + fn increment_attempts(&mut self) { if let PendingOutboundPayment::Retryable { attempts, .. } = self { attempts.count += 1; @@ -928,6 +942,7 @@ pub(super) struct SendAlongPathArgs<'a> { pub keysend_preimage: &'a Option, pub invoice_request: Option<&'a InvoiceRequest>, pub bolt12_invoice: Option<&'a PaidBolt12Invoice>, + pub dnssec_proof: Option<&'a DNSSECProof>, pub session_priv_bytes: [u8; 32], pub hold_htlc_at_next_hop: bool, } @@ -1098,7 +1113,7 @@ impl OutboundPayments { SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, { - let (payment_hash, retry_strategy, params_config, _) = self + let (payment_hash, retry_strategy, params_config, dnssec_proof, _) = self .mark_invoice_received_and_get_details(invoice, payment_id)?; if invoice.invoice_features().requires_unknown_bits_from(&features) { @@ -1117,7 +1132,7 @@ impl OutboundPayments { } let invoice = PaidBolt12Invoice::Bolt12Invoice(invoice.clone()); self.send_payment_for_bolt12_invoice_internal( - payment_id, payment_hash, None, None, invoice, route_params, retry_strategy, false, router, + payment_id, payment_hash, None, None, invoice, dnssec_proof, route_params, retry_strategy, false, router, first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, best_block_height, pending_events, send_payment_along_path, logger, ) @@ -1129,7 +1144,7 @@ impl OutboundPayments { >( &self, payment_id: PaymentId, payment_hash: PaymentHash, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, - bolt12_invoice: PaidBolt12Invoice, + bolt12_invoice: PaidBolt12Invoice, dnssec_proof: Option, mut route_params: RouteParameters, retry_strategy: Retry, hold_htlcs_at_next_hop: bool, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, @@ -1188,7 +1203,8 @@ impl OutboundPayments { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), &route, + payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), + dnssec_proof.clone(), &route, Some(retry_strategy), payment_params, entropy_source, best_block_height, ); *entry.into_mut() = retryable_payment; @@ -1199,7 +1215,8 @@ impl OutboundPayments { invoice_request } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), &route, + payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), + dnssec_proof.clone(), &route, Some(retry_strategy), payment_params, entropy_source, best_block_height ); outbounds.insert(payment_id, retryable_payment); @@ -1212,7 +1229,8 @@ impl OutboundPayments { core::mem::drop(outbounds); let result = self.pay_route_internal( - &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), payment_id, + &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), + dnssec_proof.as_ref(), payment_id, &onion_session_privs, hold_htlcs_at_next_hop, node_signer, best_block_height, &send_payment_along_path ); @@ -1395,6 +1413,7 @@ impl OutboundPayments { Some(keysend_preimage), Some(&invoice_request), invoice, + None, // Static invoices don't originate from HRN resolution route_params, retry_strategy, hold_htlcs_at_next_hop, @@ -1604,7 +1623,7 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1671,7 +1690,7 @@ impl OutboundPayments { } } } - let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { + let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice, dnssec_proof) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { @@ -1714,8 +1733,9 @@ impl OutboundPayments { payment.get_mut().increment_attempts(); let bolt12_invoice = payment.get().bolt12_invoice(); + let dnssec_proof = payment.get().dnssec_proof(); - (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) + (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned(), dnssec_proof.cloned()) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -1755,7 +1775,7 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, + invoice_request.as_ref(), bolt12_invoice.as_ref(), dnssec_proof.as_ref(), payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { @@ -1914,7 +1934,7 @@ impl OutboundPayments { })?; match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, None, payment_id, &onion_session_privs, false, node_signer, + None, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -1977,7 +1997,7 @@ impl OutboundPayments { hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment), hash_map::Entry::Vacant(entry) => { let (payment, onion_session_privs) = Self::create_pending_payment( - payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, route, retry_strategy, + payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, None, route, retry_strategy, payment_params, entropy_source, best_block_height ); entry.insert(payment); @@ -1990,7 +2010,8 @@ impl OutboundPayments { fn create_pending_payment( payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, invoice_request: Option, - bolt12_invoice: Option, route: &Route, retry_strategy: Option, + bolt12_invoice: Option, dnssec_proof: Option, + route: &Route, retry_strategy: Option, payment_params: Option, entropy_source: &ES, best_block_height: u32 ) -> (PendingOutboundPayment, Vec<[u8; 32]>) { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); @@ -2011,6 +2032,7 @@ impl OutboundPayments { keysend_preimage, invoice_request, bolt12_invoice, + dnssec_proof, custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), @@ -2075,6 +2097,7 @@ impl OutboundPayments { retry_strategy: *retry_strategy, route_params_config: *route_params_config, retryable_invoice_request, + dnssec_proof: None, }; core::mem::swap(&mut new_val, entry.into_mut()); Ok(()) @@ -2085,6 +2108,19 @@ impl OutboundPayments { } } + /// Sets the DNSSEC proof for a payment that originated from a BIP 353 Human Readable Name + /// resolution. This should be called after `received_offer` to attach the proof to the + /// `AwaitingInvoice` state. + #[cfg(feature = "dnssec")] + pub(super) fn set_dnssec_proof(&self, payment_id: PaymentId, proof: DNSSECProof) { + let mut outbounds = self.pending_outbound_payments.lock().unwrap(); + if let Some(payment) = outbounds.get_mut(&payment_id) { + if let PendingOutboundPayment::AwaitingInvoice { dnssec_proof, .. } = payment { + *dnssec_proof = Some(proof); + } + } + } + pub(super) fn add_new_awaiting_invoice( &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, route_params_config: RouteParametersConfig, @@ -2102,6 +2138,7 @@ impl OutboundPayments { retry_strategy, route_params_config, retryable_invoice_request, + dnssec_proof: None, }); Ok(()) @@ -2114,7 +2151,7 @@ impl OutboundPayments { &self, invoice: &Bolt12Invoice, payment_id: PaymentId ) -> Result<(), Bolt12PaymentError> { self.mark_invoice_received_and_get_details(invoice, payment_id) - .and_then(|(_, _, _, is_newly_marked)| { + .and_then(|(_, _, _, _, is_newly_marked)| { is_newly_marked .then_some(()) .ok_or(Bolt12PaymentError::DuplicateInvoice) @@ -2124,7 +2161,7 @@ impl OutboundPayments { #[rustfmt::skip] fn mark_invoice_received_and_get_details( &self, invoice: &Bolt12Invoice, payment_id: PaymentId - ) -> Result<(PaymentHash, Retry, RouteParametersConfig, bool), Bolt12PaymentError> { + ) -> Result<(PaymentHash, Retry, RouteParametersConfig, Option, bool), Bolt12PaymentError> { match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::AwaitingInvoice { @@ -2133,13 +2170,18 @@ impl OutboundPayments { let payment_hash = invoice.payment_hash(); let retry = *retry; let config = *route_params_config; + let dnssec_proof = if let PendingOutboundPayment::AwaitingInvoice { dnssec_proof, .. } = entry.get() { + dnssec_proof.clone() + } else { + None + }; *entry.into_mut() = PendingOutboundPayment::InvoiceReceived { payment_hash, retry_strategy: retry, route_params_config: config, }; - Ok((payment_hash, retry, config, true)) + Ok((payment_hash, retry, config, dnssec_proof, true)) }, // When manual invoice handling is enabled, the corresponding `PendingOutboundPayment` entry // is already updated at the time the invoice is received. This ensures that `InvoiceReceived` @@ -2148,7 +2190,7 @@ impl OutboundPayments { PendingOutboundPayment::InvoiceReceived { retry_strategy, route_params_config, .. } => { - Ok((invoice.payment_hash(), *retry_strategy, *route_params_config, false)) + Ok((invoice.payment_hash(), *retry_strategy, *route_params_config, None, false)) }, _ => Err(Bolt12PaymentError::DuplicateInvoice), }, @@ -2160,6 +2202,7 @@ impl OutboundPayments { fn pay_route_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, bolt12_invoice: Option<&PaidBolt12Invoice>, + dnssec_proof: Option<&DNSSECProof>, payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, hold_htlcs_at_next_hop: bool, node_signer: &NS, best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> @@ -2209,7 +2252,7 @@ impl OutboundPayments { let path_res = send_payment_along_path(SendAlongPathArgs { path: &path, payment_hash: &payment_hash, recipient_onion, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, - bolt12_invoice, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, + bolt12_invoice, dnssec_proof, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, session_priv_bytes: *session_priv_bytes }); results.push(path_res); @@ -2276,7 +2319,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, &onion_session_privs, + keysend_preimage, None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } @@ -2300,6 +2343,7 @@ impl OutboundPayments { #[rustfmt::skip] pub(super) fn claim_htlc( &self, payment_id: PaymentId, payment_preimage: PaymentPreimage, bolt12_invoice: Option, + dnssec_proof: Option, session_priv: SecretKey, path: Path, from_onchain: bool, ev_completion_action: &mut Option, pending_events: &Mutex)>>, logger: &WithContext, @@ -2322,7 +2366,8 @@ impl OutboundPayments { payment_hash, amount_msat, fee_paid_msat, - bolt12_invoice: bolt12_invoice, + bolt12_invoice, + dnssec_proof, }, ev_completion_action.take())); payment.get_mut().mark_fulfilled(); } @@ -2718,6 +2763,7 @@ impl OutboundPayments { keysend_preimage: None, // only used for retries, and we'll never retry on startup invoice_request: None, // only used for retries, and we'll never retry on startup bolt12_invoice: None, // only used for retries, and we'll never retry on startup! + dnssec_proof: None, // only used for retries, and we'll never retry on startup custom_tlvs: Vec::new(), // only used for retries, and we'll never retry on startup pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), @@ -2822,6 +2868,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, })), (13, invoice_request, option), (15, bolt12_invoice, option), + (17, dnssec_proof, option), (not_written, retry_strategy, (static_value, None)), (not_written, attempts, (static_value, PaymentAttempts::new())), }, @@ -2847,6 +2894,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, |fee_msat| RouteParametersConfig::default().with_max_total_routing_fee_msat(fee_msat) ) ))), + (9, dnssec_proof, option), }, (7, InvoiceReceived) => { (0, payment_hash, required), diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index e857a359c78..29119e0f6ef 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -181,6 +181,23 @@ impl ReadableArgs for DNSResolverMessage { } } +impl Writeable for DNSSECProof { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + (self.name.as_str().len() as u8).write(w)?; + w.write_all(self.name.as_str().as_bytes())?; + self.proof.write(w) + } +} + +impl Readable for DNSSECProof { + fn read(r: &mut R) -> Result { + let s = Hostname::read(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + let proof = Readable::read(r)?; + Ok(DNSSECProof { name, proof }) + } +} + impl OnionMessageContents for DNSResolverMessage { #[cfg(c_bindings)] fn msg_type(&self) -> String {