From 3fdc51c8b09b6f51e10f3b0f12bc0fdd3c4f754c Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 1 Oct 2024 18:06:19 +0000 Subject: [PATCH 1/8] Skip the implicit trailing `.` in `HumanReadableName`'s domain Domain names implicitly have a trailing `.`, which we require in bLIP 32 but generally shouldn't be exposing to the user in `HumanReadableName`s (after all, they're human-readable). Here we make sure the trailing `.` is dropped in `HumanReadableName`s before we re-add them when building the bLIP 32 messages. --- lightning/src/onion_message/dns_resolution.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index bbf8caa4f71..0f6071e73a3 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -198,7 +198,12 @@ pub struct HumanReadableName { impl HumanReadableName { /// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the /// struct-level documentation for more on the requirements on each. - pub fn new(user: String, domain: String) -> Result { + pub fn new(user: String, mut domain: String) -> Result { + // First normalize domain and remove the optional trailing `.` + if domain.ends_with(".") { + domain.pop(); + } + // Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.` const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1; if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 { return Err(()); From 126f7f455592048311c5431712f4713c087876cb Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 12 Jul 2024 23:37:54 +0000 Subject: [PATCH 2/8] Add a new `AwaitingOffer` outbound payment state for BIP 353 --- lightning/src/ln/channelmanager.rs | 11 +++-- lightning/src/ln/outbound_payment.rs | 74 ++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d575ae4a9b6..ea36c0d4335 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3605,11 +3605,11 @@ where pub fn list_recent_payments(&self) -> Vec { self.pending_outbound_payments.pending_outbound_payments.lock().unwrap().iter() .filter_map(|(payment_id, pending_outbound_payment)| match pending_outbound_payment { - PendingOutboundPayment::AwaitingInvoice { .. } => { - Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id }) - }, - // InvoiceReceived is an intermediate state and doesn't need to be exposed - PendingOutboundPayment::InvoiceReceived { .. } => { + PendingOutboundPayment::AwaitingInvoice { .. } + | PendingOutboundPayment::AwaitingOffer { .. } + // InvoiceReceived is an intermediate state and doesn't need to be exposed + | PendingOutboundPayment::InvoiceReceived { .. } => + { Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id }) }, PendingOutboundPayment::StaticInvoiceReceived { .. } => { @@ -12140,6 +12140,7 @@ where } } PendingOutboundPayment::AwaitingInvoice { .. } => {}, + PendingOutboundPayment::AwaitingOffer { .. } => {}, PendingOutboundPayment::InvoiceReceived { .. } => {}, PendingOutboundPayment::StaticInvoiceReceived { .. } => {}, PendingOutboundPayment::Fulfilled { .. } => {}, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index c3fde629a83..8cdeadeaa63 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -58,6 +58,15 @@ pub(crate) enum PendingOutboundPayment { Legacy { session_privs: HashSet<[u8; 32]>, }, + /// Used when we are waiting for an Offer to come back from a BIP 353 resolution + AwaitingOffer { + expiration: StaleExpiration, + retry_strategy: Retry, + max_total_routing_fee_msat: Option, + /// Human Readable Names-originated payments should always specify an explicit amount to + /// send up-front, which we track here and enforce once we receive the offer. + amount_msats: u64, + }, AwaitingInvoice { expiration: StaleExpiration, retry_strategy: Retry, @@ -201,6 +210,7 @@ impl PendingOutboundPayment { fn payment_hash(&self) -> Option { match self { PendingOutboundPayment::Legacy { .. } => None, + PendingOutboundPayment::AwaitingOffer { .. } => None, PendingOutboundPayment::AwaitingInvoice { .. } => None, PendingOutboundPayment::InvoiceReceived { payment_hash, .. } => Some(*payment_hash), PendingOutboundPayment::StaticInvoiceReceived { payment_hash, .. } => Some(*payment_hash), @@ -217,7 +227,8 @@ impl PendingOutboundPayment { PendingOutboundPayment::Retryable { session_privs, .. } | PendingOutboundPayment::Fulfilled { session_privs, .. } | PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs, - PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::AwaitingOffer { .. } | + PendingOutboundPayment::AwaitingInvoice { .. } | PendingOutboundPayment::InvoiceReceived { .. } | PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); return; }, }); @@ -258,7 +269,8 @@ impl PendingOutboundPayment { PendingOutboundPayment::Abandoned { session_privs, .. } => { session_privs.remove(session_priv) }, - PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::AwaitingOffer { .. } | + PendingOutboundPayment::AwaitingInvoice { .. } | PendingOutboundPayment::InvoiceReceived { .. } | PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); false }, }; @@ -288,7 +300,8 @@ impl PendingOutboundPayment { PendingOutboundPayment::Retryable { session_privs, .. } => { session_privs.insert(session_priv) }, - PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::AwaitingOffer { .. } | + PendingOutboundPayment::AwaitingInvoice { .. } | PendingOutboundPayment::InvoiceReceived { .. } | PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); false }, PendingOutboundPayment::Fulfilled { .. } => false, @@ -322,6 +335,7 @@ impl PendingOutboundPayment { session_privs.len() }, PendingOutboundPayment::AwaitingInvoice { .. } => 0, + PendingOutboundPayment::AwaitingOffer { .. } => 0, PendingOutboundPayment::InvoiceReceived { .. } => 0, PendingOutboundPayment::StaticInvoiceReceived { .. } => 0, } @@ -416,8 +430,9 @@ impl Display for PaymentAttempts { } } -/// How long before a [`PendingOutboundPayment::AwaitingInvoice`] should be considered stale and -/// candidate for removal in [`OutboundPayments::remove_stale_payments`]. +/// How long before a [`PendingOutboundPayment::AwaitingInvoice`] or +/// [`PendingOutboundPayment::AwaitingOffer`] should be considered stale and candidate for removal +/// in [`OutboundPayments::remove_stale_payments`]. #[derive(Clone, Copy)] pub(crate) enum StaleExpiration { /// Number of times [`OutboundPayments::remove_stale_payments`] is called. @@ -1388,7 +1403,9 @@ impl OutboundPayments { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); return }, - PendingOutboundPayment::AwaitingInvoice { .. } => { + PendingOutboundPayment::AwaitingInvoice { .. } + | PendingOutboundPayment::AwaitingOffer { .. } => + { log_error!(logger, "Payment not yet sent"); debug_assert!(false); return @@ -1910,7 +1927,9 @@ impl OutboundPayments { true } }, - PendingOutboundPayment::AwaitingInvoice { expiration, .. } => { + PendingOutboundPayment::AwaitingInvoice { expiration, .. } + | PendingOutboundPayment::AwaitingOffer { expiration, .. } => + { let is_stale = match expiration { StaleExpiration::AbsoluteTimeout(absolute_expiry) => { *absolute_expiry <= duration_since_epoch @@ -2096,22 +2115,28 @@ impl OutboundPayments { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) { payment.get_mut().mark_abandoned(reason); - if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = payment.get() { - if payment.get().remaining_parts() == 0 { + match payment.get() { + PendingOutboundPayment::Abandoned { payment_hash, reason, .. } => { + if payment.get().remaining_parts() == 0 { + pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { + payment_id, + payment_hash: Some(*payment_hash), + reason: *reason, + }, None)); + payment.remove(); + } + }, + PendingOutboundPayment::AwaitingInvoice { .. } + | PendingOutboundPayment::AwaitingOffer { .. } => + { pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { payment_id, - payment_hash: Some(*payment_hash), - reason: *reason, + payment_hash: None, + reason: Some(reason), }, None)); payment.remove(); - } - } else if let PendingOutboundPayment::AwaitingInvoice { .. } = payment.get() { - pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { - payment_id, - payment_hash: None, - reason: Some(reason), - }, None)); - payment.remove(); + }, + _ => {}, } } } @@ -2183,7 +2208,8 @@ impl OutboundPayments { match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { hash_map::Entry::Occupied(mut entry) => { let newly_added = match entry.get() { - PendingOutboundPayment::AwaitingInvoice { .. } | + PendingOutboundPayment::AwaitingOffer { .. } | + PendingOutboundPayment::AwaitingInvoice { .. } | PendingOutboundPayment::InvoiceReceived { .. } | PendingOutboundPayment::StaticInvoiceReceived { .. } => { @@ -2285,6 +2311,14 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (6, route_params, required), (8, invoice_request, required), }, + // Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because + // no HTLCs are in-flight. + (11, AwaitingOffer) => { + (0, expiration, required), + (2, retry_strategy, required), + (4, max_total_routing_fee_msat, option), + (6, amount_msats, required), + }, ); #[cfg(test)] From a492e009124c2bae0f7888a5292f00962f824e0f Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 13 Jul 2024 14:04:39 +0000 Subject: [PATCH 3/8] 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` --- lightning/src/offers/invoice.rs | 2 ++ lightning/src/offers/invoice_request.rs | 30 ++++++++++++++++++++++--- lightning/src/offers/parse.rs | 5 +++++ lightning/src/offers/refund.rs | 10 ++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index b1d7660f224..7ef4e97a562 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, + source_human_readable_name: 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, + source_human_readable_name: 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 d1ab6d067d9..ccad8bfc4eb 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, + source_human_readable_name: None, #[cfg(test)] experimental_bar: None, } @@ -301,6 +303,14 @@ macro_rules! invoice_request_builder_methods { ( $return_value } + /// Sets the [`InvoiceRequest::source_human_readable_name`]. + /// + /// 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.source_human_readable_name = 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, + source_human_readable_name: 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 source_human_readable_name(&$self) -> &Option { + $contents.source_human_readable_name() + } } } 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 source_human_readable_name(&self) -> &Option { + &self.inner.source_human_readable_name + } + 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(), + source_human_readable_name: self.source_human_readable_name.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, source_human_readable_name: 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, + source_human_readable_name, }, 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, + source_human_readable_name, #[cfg(test)] experimental_bar, }, @@ -1484,6 +1507,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + source_human_readable_name: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, ExperimentalOfferTlvStreamRef { diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 7c9d80387de..3828ecbdffc 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 b1f5b0520ca..c94b6e40bb0 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(), + source_human_readable_name: 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, + source_human_readable_name, }, ExperimentalOfferTlvStream { #[cfg(test)] @@ -940,6 +942,11 @@ impl TryFrom for RefundContents { return Err(Bolt12SemanticError::UnexpectedIssuerSigningPubkey); } + if source_human_readable_name.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, + source_human_readable_name: None, }, ExperimentalOfferTlvStreamRef { experimental_foo: None, From e9c340101030bc5cad4929f69af63b3efa65e42e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 30 Sep 2024 18:18:38 +0000 Subject: [PATCH 4/8] Store the source `HumanReadableName` in `InvoiceRequestFields` When we receive a payment to an offer we issued resolved with a human readable name, it may have been resolved using a wildcard DNS entry which we want to map to a specific recipient account locally. To do this, we need the human readable name from the `InvoiceRequest` in the `PaymentClaim{able,ed}`, which we pipe through here using `InvoiceRequestFields`. --- fuzz/src/invoice_request_deser.rs | 1 + lightning/src/events/mod.rs | 4 ++++ lightning/src/ln/offers_tests.rs | 6 ++++++ lightning/src/offers/invoice_request.rs | 8 ++++++++ 4 files changed, 19 insertions(+) diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index aa3c006abdd..d5a43ae46ec 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -89,6 +89,7 @@ fn build_response( payer_note_truncated: invoice_request .payer_note() .map(|s| UntrustedString(s.to_string())), + human_readable_name: None, }, }); let payee_tlvs = ReceiveTlvs { diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 3538aa36780..033086981cb 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -125,6 +125,10 @@ pub enum PaymentPurpose { /// The context of the payment such as information about the corresponding [`Offer`] and /// [`InvoiceRequest`]. /// + /// This includes the Human Readable Name which the sender indicated they were paying to, + /// for possible recipient disambiguation if you're using a single wildcard DNS entry to + /// resolve to many recipients. + /// /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest payment_context: Bolt12OfferContext, diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index b667ce2c05d..a7eeebf9848 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -564,6 +564,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); assert_eq!(invoice_request.amount_msats(), None); @@ -724,6 +725,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); assert_eq!(invoice_request.amount_msats(), None); @@ -844,6 +846,7 @@ fn pays_for_offer_without_blinded_paths() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); @@ -1111,6 +1114,7 @@ fn creates_and_pays_for_offer_with_retry() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); assert_eq!(invoice_request.amount_msats(), None); @@ -1175,6 +1179,7 @@ fn pays_bolt12_invoice_asynchronously() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); @@ -1264,6 +1269,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { payer_signing_pubkey: invoice_request.payer_signing_pubkey(), quantity: None, payer_note_truncated: None, + human_readable_name: None, }, }); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index ccad8bfc4eb..18dadcca3d3 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1030,6 +1030,7 @@ impl VerifiedInvoiceRequest { quantity: *quantity, payer_note_truncated: payer_note.clone() .map(|mut s| { s.truncate(PAYER_NOTE_LIMIT); UntrustedString(s) }), + human_readable_name: self.source_human_readable_name().clone(), } } } @@ -1350,6 +1351,9 @@ pub struct InvoiceRequestFields { /// A payer-provided note which will be seen by the recipient and reflected back in the invoice /// response. Truncated to [`PAYER_NOTE_LIMIT`] characters. pub payer_note_truncated: Option, + + /// The Human Readable Name which the sender indicated they were paying to. + pub human_readable_name: Option, } /// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`]. @@ -1359,6 +1363,7 @@ impl Writeable for InvoiceRequestFields { fn write(&self, writer: &mut W) -> Result<(), io::Error> { write_tlv_fields!(writer, { (0, self.payer_signing_pubkey, required), + (1, self.human_readable_name, option), (2, self.quantity.map(|v| HighZeroBytesDroppedBigSize(v)), option), (4, self.payer_note_truncated.as_ref().map(|s| WithoutLength(&s.0)), option), }); @@ -1370,6 +1375,7 @@ impl Readable for InvoiceRequestFields { fn read(reader: &mut R) -> Result { _init_and_read_len_prefixed_tlv_fields!(reader, { (0, payer_signing_pubkey, required), + (1, human_readable_name, option), (2, quantity, (option, encoding: (u64, HighZeroBytesDroppedBigSize))), (4, payer_note_truncated, (option, encoding: (String, WithoutLength))), }); @@ -1378,6 +1384,7 @@ impl Readable for InvoiceRequestFields { payer_signing_pubkey: payer_signing_pubkey.0.unwrap(), quantity, payer_note_truncated: payer_note_truncated.map(|s| UntrustedString(s)), + human_readable_name, }) } } @@ -2733,6 +2740,7 @@ mod tests { payer_signing_pubkey: payer_pubkey(), quantity: Some(1), payer_note_truncated: Some(UntrustedString("0".repeat(PAYER_NOTE_LIMIT))), + human_readable_name: None, } ); From 6a01594f372f38025195363f65c17bf006158f5e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 7 Nov 2024 15:05:26 +0000 Subject: [PATCH 5/8] Support paying Human Readable Names directly from `ChannelManager` Now that we have the ability to resolve BIP 353 Human Readable Names directly and have tracking for outbound payments waiting on an offer resolution, we can implement full BIP 353 support in `ChannelManager`. Users will need one or more known nodes which offer DNS resolution service over onion messages using bLIP 32, which they pass to `ChannelManager::pay_for_offer_from_human_readable_name`, as well as the `HumanReadableName` itself. From there, `ChannelManager` asks the DNS resolver to provide a DNSSEC proof, which it verifies, parses into an `Offer`, and then pays. For those who wish to support on-chain fallbacks, sadly, this will not work, and they'll still have to use `OMNameResolver` directly in order to use their existing `bitcoin:` URI parsing. --- lightning/src/ln/channelmanager.rs | 184 +++++++++++++++++++++++++-- lightning/src/ln/outbound_payment.rs | 56 ++++++++ 2 files changed, 229 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ea36c0d4335..ee1cbfa758b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -75,6 +75,7 @@ use crate::offers::signer; #[cfg(async_payments)] use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; +use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; @@ -87,6 +88,11 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; +#[cfg(feature = "dnssec")] +use crate::blinded_path::message::DNSResolverContext; +#[cfg(feature = "dnssec")] +use crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECQuery, DNSSECProof, OMNameResolver}; + #[cfg(not(c_bindings))] use { crate::offers::offer::DerivedMetadata, @@ -2563,6 +2569,11 @@ where /// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate. last_days_feerates: Mutex>, + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver, + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex>, + entropy_source: ES, node_signer: NS, signer_provider: SP, @@ -3386,6 +3397,11 @@ where signer_provider, logger, + + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver::new(current_timestamp, params.best_block.height), + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex::new(Vec::new()), } } @@ -9460,6 +9476,26 @@ where &self, offer: &Offer, quantity: Option, amount_msats: Option, payer_note: Option, payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option + ) -> Result<(), Bolt12SemanticError> { + self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, |invoice_request, nonce| { + let expiration = StaleExpiration::TimerTicks(1); + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + }; + self.pending_outbound_payments + .add_new_awaiting_invoice( + payment_id, expiration, retry_strategy, max_total_routing_fee_msat, + Some(retryable_invoice_request) + ) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }) + } + + fn pay_for_offer_intern Result<(), Bolt12SemanticError>>( + &self, offer: &Offer, quantity: Option, amount_msats: Option, + payer_note: Option, payment_id: PaymentId, + human_readable_name: Option, create_pending_payment: CPP, ) -> Result<(), Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -9483,6 +9519,10 @@ where None => builder, Some(payer_note) => builder.payer_note(payer_note), }; + let builder = match human_readable_name { + None => builder, + Some(hrn) => builder.sourced_from_human_readable_name(hrn), + }; let invoice_request = builder.build_and_sign()?; let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); @@ -9494,17 +9534,7 @@ where let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - let expiration = StaleExpiration::TimerTicks(1); - let retryable_invoice_request = RetryableInvoiceRequest { - invoice_request: invoice_request.clone(), - nonce, - }; - self.pending_outbound_payments - .add_new_awaiting_invoice( - payment_id, expiration, retry_strategy, max_total_routing_fee_msat, - Some(retryable_invoice_request) - ) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?; + create_pending_payment(&invoice_request, nonce)?; self.enqueue_invoice_request(invoice_request, reply_paths) } @@ -9645,6 +9675,73 @@ where } } + /// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS + /// resolver(s) at `dns_resolvers` which resolve names according to bLIP 32. + /// + /// If the wallet supports paying on-chain schemes, you should instead use + /// [`OMNameResolver::resolve_name`] and [`OMNameResolver::handle_dnssec_proof_for_uri`] (by + /// implementing [`DNSResolverMessageHandler`]) directly to look up a URI and then delegate to + /// your normal URI handling. + /// + /// If `max_total_routing_fee_msat` is not specified, the default from + /// [`RouteParameters::from_payment_params_and_value`] is applied. + /// + /// # Payment + /// + /// The provided `payment_id` is used to ensure that only one invoice is paid for the request + /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has + /// been sent. + /// + /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the + /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the + /// payment will fail with an [`Event::InvoiceRequestFailed`]. + /// + /// # Privacy + /// + /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] + /// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the + /// docs of the parameterized [`Router`], which implements [`MessageRouter`]. + /// + /// # Limitations + /// + /// Requires a direct connection to the given [`Destination`] as well as an introduction node in + /// [`Offer::paths`] or to [`Offer::signing_pubkey`], if empty. A similar restriction applies to + /// the responding [`Bolt12Invoice::payment_paths`]. + /// + /// # Errors + /// + /// Errors if: + /// - a duplicate `payment_id` is provided given the caveats in the aforementioned link, + /// + /// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths + /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments + #[cfg(feature = "dnssec")] + pub fn pay_for_offer_from_human_readable_name( + &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, + retry_strategy: Retry, max_total_routing_fee_msat: Option, + dns_resolvers: Vec, + ) -> Result<(), ()> { + let (onion_message, context) = + self.hrn_resolver.resolve_name(payment_id, name, &*self.entropy_source)?; + let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?; + let expiration = StaleExpiration::TimerTicks(1); + self.pending_outbound_payments.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats)?; + let message_params = dns_resolvers + .iter() + .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) + .take(OFFERS_MESSAGE_REQUEST_LIMIT); + for (reply_path, destination) in message_params { + self.pending_dns_onion_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(onion_message.clone()), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: destination.clone(), + reply_path: reply_path.clone(), + }, + )); + } + Ok(()) + } + /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing /// to pay us. /// @@ -10272,6 +10369,10 @@ where payment_secrets.retain(|_, inbound_payment| { inbound_payment.expiry_time > header.time as u64 }); + #[cfg(feature = "dnssec")] { + let timestamp = self.highest_seen_timestamp.load(Ordering::Relaxed) as u32; + self.hrn_resolver.new_best_block(height, timestamp); + } } fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { @@ -11522,6 +11623,62 @@ where } } +#[cfg(feature = "dnssec")] +impl +DNSResolverMessageHandler for ChannelManager +where + M::Target: chain::Watch<::EcdsaSigner>, + T::Target: BroadcasterInterface, + ES::Target: EntropySource, + NS::Target: NodeSigner, + SP::Target: SignerProvider, + F::Target: FeeEstimator, + R::Target: Router, + MR::Target: MessageRouter, + L::Target: Logger, +{ + fn handle_dnssec_query( + &self, _message: DNSSECQuery, _responder: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + + fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { + let offer_opt = self.hrn_resolver.handle_dnssec_proof_for_offer(message, context); + if let Some((completed_requests, offer)) = offer_opt { + for (name, payment_id) in completed_requests { + if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) { + let offer_pay_res = + self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), + |invoice_request, nonce| { + let retryable_invoice_request = RetryableInvoiceRequest { + invoice_request: invoice_request.clone(), + nonce, + }; + self.pending_outbound_payments + .received_offer(payment_id, Some(retryable_invoice_request)) + .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) + }); + if offer_pay_res.is_err() { + // 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. + // Note that the PaymentFailureReason should be ignored for an + // AwaitingInvoice payment. + self.pending_outbound_payments.abandon_payment( + payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events, + ); + } + } + } + } + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) + } +} + impl NodeIdLookUp for ChannelManager where @@ -13207,6 +13364,11 @@ where logger: args.logger, default_configuration: args.default_config, + + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height), + #[cfg(feature = "dnssec")] + pending_dns_onion_messages: Mutex::new(Vec::new()), }; for (_, monitor) in args.channel_monitors.iter() { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 8cdeadeaa63..a33fca9b117 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1639,6 +1639,62 @@ impl OutboundPayments { (payment, onion_session_privs) } + #[cfg(feature = "dnssec")] + pub(super) fn add_new_awaiting_offer( + &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, + max_total_routing_fee_msat: Option, amount_msats: u64, + ) -> Result<(), ()> { + let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); + match pending_outbounds.entry(payment_id) { + hash_map::Entry::Occupied(_) => Err(()), + hash_map::Entry::Vacant(entry) => { + entry.insert(PendingOutboundPayment::AwaitingOffer { + expiration, + retry_strategy, + max_total_routing_fee_msat, + amount_msats, + }); + + Ok(()) + }, + } + } + + #[cfg(feature = "dnssec")] + pub(super) fn amt_msats_for_payment_awaiting_offer(&self, payment_id: PaymentId) -> Result { + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::AwaitingOffer { amount_msats, .. } => Ok(*amount_msats), + _ => Err(()), + }, + _ => Err(()), + } + } + + #[cfg(feature = "dnssec")] + pub(super) fn received_offer( + &self, payment_id: PaymentId, retryable_invoice_request: Option, + ) -> Result<(), ()> { + match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { + hash_map::Entry::Occupied(entry) => match entry.get() { + PendingOutboundPayment::AwaitingOffer { + expiration, retry_strategy, max_total_routing_fee_msat, .. + } => { + let mut new_val = PendingOutboundPayment::AwaitingInvoice { + expiration: *expiration, + retry_strategy: *retry_strategy, + max_total_routing_fee_msat: *max_total_routing_fee_msat, + retryable_invoice_request, + }; + core::mem::swap(&mut new_val, entry.into_mut()); + Ok(()) + }, + _ => Err(()), + }, + hash_map::Entry::Vacant(_) => Err(()), + } + } + pub(super) fn add_new_awaiting_invoice( &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, max_total_routing_fee_msat: Option, retryable_invoice_request: Option From 1d69dd58e4f9cba49fa2f8c74c78fbaa919c50fd Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 14 Jul 2024 01:17:03 +0000 Subject: [PATCH 6/8] Use `ChannelManager` as `DNSResolverMessageHandler` by default Now that `ChannelManager` supports using bLIP 32 to resolve BIP 353 Human Readable Names we should encourage users to use that feature by making the "default" (in various type aliases) to use `ChannelManager` as the `DNSResolverMessageHandler`. --- lightning/src/ln/functional_test_utils.rs | 23 ++++++++++- lightning/src/onion_message/messenger.rs | 50 ++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index de0b4c7d4bb..90a0ec85c22 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -408,6 +408,7 @@ type TestChannelManager<'node_cfg, 'chan_mon_cfg> = ChannelManager< &'chan_mon_cfg test_utils::TestLogger, >; +#[cfg(not(feature = "dnssec"))] type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< DedicatedEntropy, &'node_cfg test_utils::TestKeysInterface, @@ -416,7 +417,20 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) + IgnoringMessageHandler, + IgnoringMessageHandler, +>; + +#[cfg(feature = "dnssec")] +type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< + DedicatedEntropy, + &'node_cfg test_utils::TestKeysInterface, + &'chan_mon_cfg test_utils::TestLogger, + &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, + &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, + &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, IgnoringMessageHandler, >; @@ -3282,6 +3296,13 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec = OnionMessenger< Arc, Arc, @@ -1844,7 +1845,51 @@ pub type SimpleArcOnionMessenger = OnionMessenger< Arc>>, Arc, Arc>>, Arc>, Arc>, - IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) + Arc>, + IgnoringMessageHandler +>; + +/// Useful for simplifying the parameters of [`SimpleArcChannelManager`] and +/// [`SimpleArcPeerManager`]. See their docs for more details. +/// +/// This is not exported to bindings users as type aliases aren't supported in most languages. +/// +/// [`SimpleArcChannelManager`]: crate::ln::channelmanager::SimpleArcChannelManager +/// [`SimpleArcPeerManager`]: crate::ln::peer_handler::SimpleArcPeerManager +#[cfg(not(c_bindings))] +#[cfg(not(feature = "dnssec"))] +pub type SimpleArcOnionMessenger = OnionMessenger< + Arc, + Arc, + Arc, + Arc>, + Arc>>, Arc, Arc>>, + Arc>, + Arc>, + IgnoringMessageHandler, + IgnoringMessageHandler +>; + +/// Useful for simplifying the parameters of [`SimpleRefChannelManager`] and +/// [`SimpleRefPeerManager`]. See their docs for more details. +/// +/// This is not exported to bindings users as type aliases aren't supported in most languages. +/// +/// [`SimpleRefChannelManager`]: crate::ln::channelmanager::SimpleRefChannelManager +/// [`SimpleRefPeerManager`]: crate::ln::peer_handler::SimpleRefPeerManager +#[cfg(not(c_bindings))] +#[cfg(feature = "dnssec")] +pub type SimpleRefOnionMessenger< + 'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, M, T, F, L +> = OnionMessenger< + &'a KeysManager, + &'a KeysManager, + &'b L, + &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, + &'i DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, + &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, + &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, + &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, IgnoringMessageHandler >; @@ -1856,6 +1901,7 @@ pub type SimpleArcOnionMessenger = OnionMessenger< /// [`SimpleRefChannelManager`]: crate::ln::channelmanager::SimpleRefChannelManager /// [`SimpleRefPeerManager`]: crate::ln::peer_handler::SimpleRefPeerManager #[cfg(not(c_bindings))] +#[cfg(not(feature = "dnssec"))] pub type SimpleRefOnionMessenger< 'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, M, T, F, L > = OnionMessenger< @@ -1866,7 +1912,7 @@ pub type SimpleRefOnionMessenger< &'i DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, - IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature) + IgnoringMessageHandler, IgnoringMessageHandler >; From 7fb6964ef2dc1488725b36546f8bcac0b09eb22d Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 14 Jul 2024 13:08:04 +0000 Subject: [PATCH 7/8] Add a `lightning-dns-resolver` crate which answers bLIP 32 queries When a lightning node wishes to send payments to a BIP 353 human readable name (using BOLT 12), it first has to resolve that name to a DNS TXT record. bLIP 32 defines a way to do so over onion messages, and this completes our implementation thereof by adding the server side. It operates by simply accepting new messages and spawning tokio tasks to do DNS lookups using the `dnsse_prover` crate. It also contains full end-to-end tests of the BIP 353 -> BOLT 12 -> payment logic using the new server code to do the resolution. Note that because we now have a workspace crate which sets the "lightning/dnssec" feature in its `dev-dependencies`, a naive `cargo test` will test the "dnssec" feature. --- .gitignore | 1 + Cargo.toml | 1 + ci/ci-tests.sh | 5 +- lightning-dns-resolver/Cargo.toml | 18 + lightning-dns-resolver/src/lib.rs | 443 +++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 23 +- lightning/src/onion_message/messenger.rs | 4 +- 7 files changed, 489 insertions(+), 6 deletions(-) create mode 100644 lightning-dns-resolver/Cargo.toml create mode 100644 lightning-dns-resolver/src/lib.rs diff --git a/.gitignore b/.gitignore index fbeffa8a9c9..8507aea8368 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ lightning/net_graph-*.bin lightning-rapid-gossip-sync/res/full_graph.lngossip lightning-custom-message/target lightning-transaction-sync/target +lightning-dns-resolver/target no-std-check/target msrv-no-dev-deps-check/target diff --git a/Cargo.toml b/Cargo.toml index f0f09f547f4..b4ba58bac9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "lightning-custom-message", "lightning-transaction-sync", "lightning-macros", + "lightning-dns-resolver", "possiblyrandom", ] diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 1ee6eff9814..e49691228ec 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -51,6 +51,7 @@ WORKSPACE_MEMBERS=( lightning-custom-message lightning-transaction-sync lightning-macros + lightning-dns-resolver possiblyrandom ) @@ -61,10 +62,6 @@ for DIR in "${WORKSPACE_MEMBERS[@]}"; do cargo doc -p "$DIR" --document-private-items done -echo -e "\n\nChecking and testing lightning crate with dnssec feature" -cargo test -p lightning --verbose --color always --features dnssec -cargo check -p lightning --verbose --color always --features dnssec - echo -e "\n\nChecking and testing Block Sync Clients with features" cargo test -p lightning-block-sync --verbose --color always --features rest-client diff --git a/lightning-dns-resolver/Cargo.toml b/lightning-dns-resolver/Cargo.toml new file mode 100644 index 00000000000..14304678d08 --- /dev/null +++ b/lightning-dns-resolver/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lightning-dns-resolver" +version = "0.1.0" +authors = ["Matt Corallo"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/lightningdevkit/rust-lightning/" +description = "A crate which implements DNSSEC resolution for lightning clients over bLIP 32 using `tokio` and the `dnssec-prover` crate." +edition = "2021" + +[dependencies] +lightning = { version = "0.0.124", path = "../lightning", default-features = false } +dnssec-prover = { version = "0.6", default-features = false, features = [ "std", "tokio" ] } +tokio = { version = "1.0", default-features = false, features = ["rt"] } + +[dev-dependencies] +bitcoin = { version = "0.32" } +tokio = { version = "1.0", default-features = false, features = ["macros", "time"] } +lightning = { version = "0.0.124", path = "../lightning", features = ["dnssec", "_test_utils"] } diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs new file mode 100644 index 00000000000..df786a8b3ac --- /dev/null +++ b/lightning-dns-resolver/src/lib.rs @@ -0,0 +1,443 @@ +//! A simple crate which uses [`dnssec_prover`] to create DNSSEC Proofs in response to bLIP 32 +//! Onion Message DNSSEC Proof Queries. + +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::private_intra_doc_links)] + +use std::net::SocketAddr; +use std::ops::Deref; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use dnssec_prover::query::build_txt_proof_async; + +use lightning::blinded_path::message::DNSResolverContext; +use lightning::ln::peer_handler::IgnoringMessageHandler; +use lightning::onion_message::dns_resolution::{ + DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, +}; +use lightning::onion_message::messenger::{ + MessageSendInstructions, Responder, ResponseInstruction, +}; + +use tokio::runtime::Handle; + +#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] +const WE_REQUIRE_32_OR_64_BIT_USIZE: u8 = 424242; + +/// A resolver which implements [`DNSResolverMessageHandler`] and replies to [`DNSSECQuery`] +/// messages with with [`DNSSECProof`]s. +pub struct OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + state: Arc, + proof_handler: Option, + runtime_handle: Handle, +} + +const MAX_PENDING_RESPONSES: usize = 1024; +struct OMResolverState { + resolver: SocketAddr, + pending_replies: Mutex>, + pending_query_count: AtomicUsize, +} + +impl OMDomainResolver { + /// Creates a new [`OMDomainResolver`] given the [`SocketAddr`] of a DNS resolver listening on + /// TCP (e.g. 8.8.8.8:53, 1.1.1.1:53 or your local DNS resolver). + /// + /// Ignores any incoming [`DNSSECProof`] messages. + pub fn ignoring_incoming_proofs(resolver: SocketAddr) -> Self { + Self::new(resolver, None) + } +} + +impl OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + /// Creates a new [`OMDomainResolver`] given the [`SocketAddr`] of a DNS resolver listening on + /// TCP (e.g. 8.8.8.8:53, 1.1.1.1:53 or your local DNS resolver). + /// + /// Uses `tokio`'s [`Handle::current`] to fetch the async runtime on which futures will be + /// spawned. + /// + /// The optional `proof_handler` can be provided to pass proofs coming back to us to the + /// underlying handler. This is useful when this resolver is handling incoming resolution + /// requests but some other handler is making proof requests of remote nodes and wants to get + /// results. + pub fn new(resolver: SocketAddr, proof_handler: Option) -> Self { + Self::with_runtime(resolver, proof_handler, Handle::current()) + } + + /// Creates a new [`OMDomainResolver`] given the [`SocketAddr`] of a DNS resolver listening on + /// TCP (e.g. 8.8.8.8:53, 1.1.1.1:53 or your local DNS resolver) and a `tokio` runtime + /// [`Handle`] on which futures will be spawned. + /// + /// The optional `proof_handler` can be provided to pass proofs coming back to us to the + /// underlying handler. This is useful when this resolver is handling incoming resolution + /// requests but some other handler is making proof requests of remote nodes and wants to get + /// results. + pub fn with_runtime( + resolver: SocketAddr, proof_handler: Option, runtime_handle: Handle, + ) -> Self { + Self { + state: Arc::new(OMResolverState { + resolver, + pending_replies: Mutex::new(Vec::new()), + pending_query_count: AtomicUsize::new(0), + }), + proof_handler, + runtime_handle, + } + } +} + +impl DNSResolverMessageHandler for OMDomainResolver +where + PH::Target: DNSResolverMessageHandler, +{ + fn handle_dnssec_proof(&self, proof: DNSSECProof, context: DNSResolverContext) { + if let Some(proof_handler) = &self.proof_handler { + proof_handler.handle_dnssec_proof(proof, context); + } + } + + fn handle_dnssec_query( + &self, q: DNSSECQuery, responder_opt: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + let responder = match responder_opt { + Some(responder) => responder, + None => return None, + }; + if self.state.pending_query_count.fetch_add(1, Ordering::Relaxed) > MAX_PENDING_RESPONSES { + self.state.pending_query_count.fetch_sub(1, Ordering::Relaxed); + return None; + } + let us = Arc::clone(&self.state); + self.runtime_handle.spawn(async move { + if let Ok((proof, _ttl)) = build_txt_proof_async(us.resolver, &q.0).await { + let contents = DNSResolverMessage::DNSSECProof(DNSSECProof { name: q.0, proof }); + let instructions = responder.respond().into_instructions(); + us.pending_replies.lock().unwrap().push((contents, instructions)); + us.pending_query_count.fetch_sub(1, Ordering::Relaxed); + } + }); + None + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut *self.state.pending_replies.lock().unwrap()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; + use bitcoin::Block; + + use lightning::blinded_path::message::{BlindedMessagePath, MessageContext}; + use lightning::blinded_path::NodeIdLookUp; + use lightning::events::{Event, PaymentPurpose}; + use lightning::ln::channelmanager::{PaymentId, Retry}; + use lightning::ln::functional_test_utils::*; + use lightning::ln::msgs::{ChannelMessageHandler, Init, OnionMessageHandler}; + use lightning::ln::peer_handler::IgnoringMessageHandler; + use lightning::onion_message::dns_resolution::{HumanReadableName, OMNameResolver}; + use lightning::onion_message::messenger::{ + AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, + }; + use lightning::sign::{KeysManager, NodeSigner, Recipient}; + use lightning::types::features::InitFeatures; + use lightning::types::payment::PaymentHash; + use lightning::util::logger::Logger; + + use lightning::{ + commitment_signed_dance, expect_payment_claimed, expect_pending_htlcs_forwardable, + get_htlc_update_msgs, + }; + + use std::ops::Deref; + use std::sync::Mutex; + use std::time::{Duration, Instant, SystemTime}; + + struct TestLogger { + node: &'static str, + } + impl Logger for TestLogger { + fn log(&self, record: lightning::util::logger::Record) { + eprintln!("{}: {}", self.node, record.args); + } + } + impl Deref for TestLogger { + type Target = TestLogger; + fn deref(&self) -> &TestLogger { + self + } + } + + struct DummyNodeLookup {} + impl NodeIdLookUp for DummyNodeLookup { + fn next_node_id(&self, _: u64) -> Option { + None + } + } + impl Deref for DummyNodeLookup { + type Target = DummyNodeLookup; + fn deref(&self) -> &DummyNodeLookup { + self + } + } + + struct DirectlyConnectedRouter {} + impl MessageRouter for DirectlyConnectedRouter { + fn find_path( + &self, _sender: PublicKey, _peers: Vec, destination: Destination, + ) -> Result { + Ok(OnionMessagePath { + destination, + first_node_addresses: None, + intermediate_nodes: Vec::new(), + }) + } + + fn create_blinded_paths( + &self, recipient: PublicKey, context: MessageContext, _peers: Vec, + secp_ctx: &Secp256k1, + ) -> Result, ()> { + let keys = KeysManager::new(&[0; 32], 42, 43); + Ok(vec![BlindedMessagePath::one_hop(recipient, context, &keys, secp_ctx).unwrap()]) + } + } + impl Deref for DirectlyConnectedRouter { + type Target = DirectlyConnectedRouter; + fn deref(&self) -> &DirectlyConnectedRouter { + self + } + } + + struct URIResolver { + resolved_uri: Mutex>, + resolver: OMNameResolver, + pending_messages: Mutex>, + } + impl DNSResolverMessageHandler for URIResolver { + fn handle_dnssec_query( + &self, _: DNSSECQuery, _: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + panic!(); + } + + fn handle_dnssec_proof(&self, msg: DNSSECProof, context: DNSResolverContext) { + let mut proof = self.resolver.handle_dnssec_proof_for_uri(msg, context).unwrap(); + assert_eq!(proof.0.len(), 1); + let payment = proof.0.pop().unwrap(); + let mut result = Some((payment.0, payment.1, proof.1)); + core::mem::swap(&mut *self.resolved_uri.lock().unwrap(), &mut result); + assert!(result.is_none()); + } + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + core::mem::take(&mut *self.pending_messages.lock().unwrap()) + } + } + + fn create_resolver() -> (impl AOnionMessenger, PublicKey) { + let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43)); + let resolver_logger = TestLogger { node: "resolver" }; + let resolver = OMDomainResolver::ignoring_incoming_proofs("8.8.8.8:53".parse().unwrap()); + let resolver = Arc::new(resolver); + ( + OnionMessenger::new( + Arc::clone(&resolver_keys), + Arc::clone(&resolver_keys), + resolver_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&resolver), + IgnoringMessageHandler {}, + ), + resolver_keys.get_node_id(Recipient::Node).unwrap(), + ) + } + + fn get_om_init() -> Init { + let mut init_msg = + Init { features: InitFeatures::empty(), networks: None, remote_network_address: None }; + init_msg.features.set_onion_messages_optional(); + init_msg + } + + #[tokio::test] + async fn resolution_test() { + let secp_ctx = Secp256k1::new(); + + let (resolver_messenger, resolver_id) = create_resolver(); + + let resolver_dest = Destination::Node(resolver_id); + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + + let payment_id = PaymentId([42; 32]); + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43)); + let payer_logger = TestLogger { node: "payer" }; + let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); + let payer = Arc::new(URIResolver { + resolved_uri: Mutex::new(None), + resolver: OMNameResolver::new(now as u32, 1), + pending_messages: Mutex::new(Vec::new()), + }); + let payer_messenger = Arc::new(OnionMessenger::new( + Arc::clone(&payer_keys), + Arc::clone(&payer_keys), + payer_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&payer), + IgnoringMessageHandler {}, + )); + + let init_msg = get_om_init(); + payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + + let (msg, context) = + payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); + let query_context = MessageContext::DNSResolver(context); + let reply_path = + BlindedMessagePath::one_hop(payer_id, query_context, &*payer_keys, &secp_ctx).unwrap(); + payer.pending_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(msg), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: resolver_dest, + reply_path, + }, + )); + + let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.get_om().handle_onion_message(payer_id, &query); + + assert!(resolver_messenger.get_om().next_onion_message_for_peer(payer_id).is_none()); + let start = Instant::now(); + let response = loop { + tokio::time::sleep(Duration::from_millis(10)).await; + if let Some(msg) = resolver_messenger.get_om().next_onion_message_for_peer(payer_id) { + break msg; + } + assert!(start.elapsed() < Duration::from_secs(10), "Resolution took too long"); + }; + + payer_messenger.handle_onion_message(resolver_id, &response); + let resolution = payer.resolved_uri.lock().unwrap().take().unwrap(); + assert_eq!(resolution.0, name); + assert_eq!(resolution.1, payment_id); + assert!(resolution.2[.."bitcoin:".len()].eq_ignore_ascii_case("bitcoin:")); + } + + #[tokio::test] + async fn end_to_end_test() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + + // The DNSSEC validation will only work with the current time, so set the time on the + // resolver. + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + let block = Block { + header: create_dummy_header(nodes[0].best_block_hash(), now as u32), + txdata: Vec::new(), + }; + connect_block(&nodes[0], &block); + connect_block(&nodes[1], &block); + + let payer_id = nodes[0].node.get_our_node_id(); + let payee_id = nodes[1].node.get_our_node_id(); + + let (resolver_messenger, resolver_id) = create_resolver(); + let init_msg = get_om_init(); + nodes[0].onion_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + // When we get the proof back, override its contents to an offer from nodes[1] + let bs_offer = nodes[1].node.create_offer_builder(None).unwrap().build().unwrap(); + nodes[0] + .node + .testing_dnssec_proof_offer_resolution_override + .lock() + .unwrap() + .insert(name.clone(), bs_offer); + + let payment_id = PaymentId([42; 32]); + let resolvers = vec![Destination::Node(resolver_id)]; + let retry = Retry::Attempts(0); + let amt = 42_000; + nodes[0] + .node + .pay_for_offer_from_human_readable_name(name, amt, payment_id, retry, None, resolvers) + .unwrap(); + + let query = nodes[0].onion_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.get_om().handle_onion_message(payer_id, &query); + + assert!(resolver_messenger.get_om().next_onion_message_for_peer(payer_id).is_none()); + let start = Instant::now(); + let response = loop { + tokio::time::sleep(Duration::from_millis(10)).await; + if let Some(msg) = resolver_messenger.get_om().next_onion_message_for_peer(payer_id) { + break msg; + } + assert!(start.elapsed() < Duration::from_secs(10), "Resolution took too long"); + }; + + nodes[0].onion_messenger.handle_onion_message(resolver_id, &response); + + let invreq = nodes[0].onion_messenger.next_onion_message_for_peer(payee_id).unwrap(); + nodes[1].onion_messenger.handle_onion_message(payer_id, &invreq); + + let inv = nodes[1].onion_messenger.next_onion_message_for_peer(payer_id).unwrap(); + nodes[0].onion_messenger.handle_onion_message(payee_id, &inv); + + check_added_monitors(&nodes[0], 1); + let updates = get_htlc_update_msgs!(nodes[0], payee_id); + nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]); + commitment_signed_dance!(nodes[1], nodes[0], updates.commitment_signed, false); + expect_pending_htlcs_forwardable!(nodes[1]); + + let claimable_events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(claimable_events.len(), 1); + let our_payment_preimage; + if let Event::PaymentClaimable { purpose, amount_msat, .. } = &claimable_events[0] { + assert_eq!(*amount_msat, amt); + if let PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. } = purpose { + our_payment_preimage = payment_preimage.unwrap(); + nodes[1].node.claim_funds(our_payment_preimage); + let payment_hash: PaymentHash = our_payment_preimage.into(); + expect_payment_claimed!(nodes[1], payment_hash, amt); + } else { + panic!(); + } + } else { + panic!(); + } + + check_added_monitors(&nodes[1], 1); + let updates = get_htlc_update_msgs!(nodes[1], payer_id); + nodes[0].node.handle_update_fulfill_htlc(payee_id, &updates.update_fulfill_htlcs[0]); + commitment_signed_dance!(nodes[0], nodes[1], updates.commitment_signed, false); + + expect_payment_sent(&nodes[0], our_payment_preimage, None, true, true); + } +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ee1cbfa758b..c11789247f4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2574,6 +2574,14 @@ where #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex>, + #[cfg(feature = "_test_utils")] + /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an + /// offer generated in the test. + /// + /// This allows for doing so, validating proofs as normal, but, if they pass, replacing the + /// offer they resolve to to the given one. + pub testing_dnssec_proof_offer_resolution_override: Mutex>, + entropy_source: ES, node_signer: NS, signer_provider: SP, @@ -3402,6 +3410,9 @@ where hrn_resolver: OMNameResolver::new(current_timestamp, params.best_block.height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), } } @@ -11645,8 +11656,15 @@ where fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { let offer_opt = self.hrn_resolver.handle_dnssec_proof_for_offer(message, context); - if let Some((completed_requests, offer)) = offer_opt { + #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] + if let Some((completed_requests, mut offer)) = offer_opt { for (name, payment_id) in completed_requests { + #[cfg(feature = "_test_utils")] + if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { + // If we have multiple pending requests we may end up over-using the override + // offer, but tests can deal with that. + offer = replacement_offer; + } if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) { let offer_pay_res = self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), @@ -13369,6 +13387,9 @@ where hrn_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height), #[cfg(feature = "dnssec")] pending_dns_onion_messages: Mutex::new(Vec::new()), + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), }; for (_, monitor) in args.channel_monitors.iter() { diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 001fa6a4962..acac9dc006a 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -406,7 +406,9 @@ pub struct ResponseInstruction { } impl ResponseInstruction { - fn into_instructions(self) -> MessageSendInstructions { + /// Converts this [`ResponseInstruction`] into a [`MessageSendInstructions`] so that it can be + /// used to send the response via a normal message sending method. + pub fn into_instructions(self) -> MessageSendInstructions { MessageSendInstructions::ForReply { instructions: self } } } From 31e16f89c1f5bd7a3140b34eb99d006f711f82ce Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 2 Oct 2024 18:23:31 +0000 Subject: [PATCH 8/8] Set the `dns_resolution` feature in `OMDomainResolver` `OMDomainResolver` actually does support building DNSSECProofs, so should be setting the `dns_resolution` `NodeFeature` flag. --- lightning-dns-resolver/Cargo.toml | 1 + lightning-dns-resolver/src/lib.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lightning-dns-resolver/Cargo.toml b/lightning-dns-resolver/Cargo.toml index 14304678d08..1c2ebe615b2 100644 --- a/lightning-dns-resolver/Cargo.toml +++ b/lightning-dns-resolver/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] lightning = { version = "0.0.124", path = "../lightning", default-features = false } +lightning-types = { version = "0.1", path = "../lightning-types", default-features = false } dnssec-prover = { version = "0.6", default-features = false, features = [ "std", "tokio" ] } tokio = { version = "1.0", default-features = false, features = ["rt"] } diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index df786a8b3ac..50474077a48 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -21,6 +21,8 @@ use lightning::onion_message::messenger::{ MessageSendInstructions, Responder, ResponseInstruction, }; +use lightning_types::features::NodeFeatures; + use tokio::runtime::Handle; #[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] @@ -128,6 +130,12 @@ where None } + fn provided_node_features(&self) -> NodeFeatures { + let mut features = NodeFeatures::empty(); + features.set_dns_resolution_optional(); + features + } + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { core::mem::take(&mut *self.state.pending_replies.lock().unwrap()) }