From 0fdf0cf245d6a9970f6b6ff773e30ca25a62aaf5 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sat, 28 Jun 2025 21:50:57 +0000 Subject: [PATCH 1/3] Switch to LDK's `HumanReadableName` instead of our own Previously I was hoping we could swap the dependency order between LDK and `bitcoin-payment-instructions`, but that turned out to be untennable, so instead we should reuse the LDK `HumanReadableName`. --- fuzz/src/parse.rs | 4 +-- src/dns_resolver.rs | 5 ++- src/hrn.rs | 83 ------------------------------------------- src/hrn_resolution.rs | 3 +- src/http_resolver.rs | 3 +- src/lib.rs | 5 +-- 6 files changed, 8 insertions(+), 95 deletions(-) delete mode 100644 src/hrn.rs diff --git a/fuzz/src/parse.rs b/fuzz/src/parse.rs index 6252e34..753ac9c 100644 --- a/fuzz/src/parse.rs +++ b/fuzz/src/parse.rs @@ -10,9 +10,9 @@ use bitcoin::Network; use bitcoin_payment_instructions::amount::Amount; -use bitcoin_payment_instructions::hrn::HumanReadableName; use bitcoin_payment_instructions::hrn_resolution::{ - DummyHrnResolver, HrnResolution, HrnResolutionFuture, HrnResolver, LNURLResolutionFuture, + DummyHrnResolver, HrnResolution, HrnResolutionFuture, HrnResolver, HumanReadableName, + LNURLResolutionFuture, }; use bitcoin_payment_instructions::PaymentInstructions; diff --git a/src/dns_resolver.rs b/src/dns_resolver.rs index 16ca973..1937274 100644 --- a/src/dns_resolver.rs +++ b/src/dns_resolver.rs @@ -9,9 +9,8 @@ use dnssec_prover::rr::Name; use crate::amount::Amount; use crate::dnssec_utils::resolve_proof; -use crate::hrn::HumanReadableName; use crate::hrn_resolution::{ - HrnResolution, HrnResolutionFuture, HrnResolver, LNURLResolutionFuture, + HrnResolution, HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, }; /// An [`HrnResolver`] which resolves BIP 353 Human Readable Names to payment instructions using a @@ -59,7 +58,7 @@ mod tests { use crate::*; #[tokio::test] - async fn test_http_hrn_resolver() { + async fn test_dns_hrn_resolver() { let resolver = DNSHrnResolver(SocketAddr::from_str("8.8.8.8:53").unwrap()); let instructions = PaymentInstructions::parse( "send.some@satsto.me", diff --git a/src/hrn.rs b/src/hrn.rs deleted file mode 100644 index f103f15..0000000 --- a/src/hrn.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! A type for storing Human Readable Names (HRNs) which can be resolved using BIP 353 and the DNS -//! or LNURL-Pay and LN-Address. - -// Note that `REQUIRED_EXTRA_LEN` includes the (implicit) trailing `.` -const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1; - -/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts. -/// -/// The `user` and `domain` parts, together, cannot exceed 231 bytes in length, and both must be -/// non-empty. -/// -/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks] -/// and do punycode en-/de-coding yourself. This struc will always handle only plain ASCII `user` -/// and `domain` parts. -/// -/// This struct can also be used for LN-Address recipients. -/// -/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct HumanReadableName { - contents: [u8; 255 - REQUIRED_EXTRA_LEN], - user_len: u8, - domain_len: u8, -} - -/// Check if the chars in `s` are allowed to be included in a hostname. -pub(crate) fn str_chars_allowed(s: &str) -> bool { - s.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-') -} - -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: &str, mut domain: &str) -> Result { - // First normalize domain and remove the optional trailing `.` - if domain.ends_with('.') { - domain = &domain[..domain.len() - 1]; - } - if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 { - return Err(()); - } - if user.is_empty() || domain.is_empty() { - return Err(()); - } - if !str_chars_allowed(user) || !str_chars_allowed(domain) { - return Err(()); - } - let mut contents = [0; 255 - REQUIRED_EXTRA_LEN]; - contents[..user.len()].copy_from_slice(user.as_bytes()); - contents[user.len()..user.len() + domain.len()].copy_from_slice(domain.as_bytes()); - Ok(HumanReadableName { - contents, - user_len: user.len() as u8, - domain_len: domain.len() as u8, - }) - } - - /// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`. - /// - /// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by - /// BIP 353. - pub fn from_encoded(encoded: &str) -> Result { - if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once('@') - { - Self::new(user, domain) - } else { - Err(()) - } - } - - /// Gets the `user` part of this Human Readable Name - pub fn user(&self) -> &str { - let bytes = &self.contents[..self.user_len as usize]; - core::str::from_utf8(bytes).expect("Checked in constructor") - } - - /// Gets the `domain` part of this Human Readable Name - pub fn domain(&self) -> &str { - let user_len = self.user_len as usize; - let bytes = &self.contents[user_len..user_len + self.domain_len as usize]; - core::str::from_utf8(bytes).expect("Checked in constructor") - } -} diff --git a/src/hrn_resolution.rs b/src/hrn_resolution.rs index 1d47a03..7852e33 100644 --- a/src/hrn_resolution.rs +++ b/src/hrn_resolution.rs @@ -7,10 +7,11 @@ //! associated types in this module. use crate::amount::Amount; -use crate::hrn::HumanReadableName; use lightning_invoice::Bolt11Invoice; +pub use lightning::onion_message::dns_resolution::HumanReadableName; + use core::future::Future; use core::pin::Pin; diff --git a/src/http_resolver.rs b/src/http_resolver.rs index ea02610..de7bbe6 100644 --- a/src/http_resolver.rs +++ b/src/http_resolver.rs @@ -17,9 +17,8 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef}; use crate::amount::Amount; use crate::dnssec_utils::resolve_proof; -use crate::hrn::HumanReadableName; use crate::hrn_resolution::{ - HrnResolution, HrnResolutionFuture, HrnResolver, LNURLResolutionFuture, + HrnResolution, HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, }; const DOH_ENDPOINT: &'static str = "https://dns.google/dns-query?dns="; diff --git a/src/lib.rs b/src/lib.rs index 04dcd07..9ad47fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,11 +63,8 @@ pub mod receive; pub mod hrn_resolution; -pub mod hrn; - use amount::Amount; -use hrn::HumanReadableName; -use hrn_resolution::{HrnResolution, HrnResolver}; +use hrn_resolution::{HrnResolution, HrnResolver, HumanReadableName}; /// A method which can be used to make a payment #[derive(Clone, Debug, PartialEq, Eq)] From 543c909aaa3a5d0fa943d4ad888d617d83f6c4e5 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 29 Jun 2025 01:42:00 +0000 Subject: [PATCH 2/3] Add an onion message-based DNSSEC HRN Resolver Doing HRN resolution natively over the normal internet tends to be horrendous for privacy. One of the main motivations for BIP 353 was to limit the impacts of this by allowing for easier proxying of DNS requests. Here we add one such proxied request, specifically using lightning onion messages to do the DNS requests. --- Cargo.toml | 6 +- ci/ci-tests.sh | 5 +- src/lib.rs | 3 + src/onion_message_resolver.rs | 392 ++++++++++++++++++++++++++++++++++ 4 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 src/onion_message_resolver.rs diff --git a/Cargo.toml b/Cargo.toml index 11b4dc7..3c14386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,14 @@ rustdoc-args = ["--cfg", "docsrs"] [features] http = ["reqwest", "std", "serde", "serde_json"] -std = ["dnssec-prover"] +std = ["dnssec-prover", "getrandom"] default = ["std"] [dependencies] lightning-invoice = { version = "0.33", default-features = false } lightning = { version = "0.1", default-features = false, features = ["dnssec"] } bitcoin = { version = "0.32", default-features = false } -getrandom = { version = "0.3", default-features = false } +getrandom = { version = "0.3", default-features = false, optional = true } dnssec-prover = { version = "0.6", default-features = false, optional = true, features = ["validation", "std", "tokio"] } reqwest = { version = "0.11", default-features = false, optional = true, features = ["rustls-tls-webpki-roots", "json"] } serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } @@ -30,3 +30,5 @@ serde_json = { version = "1.0", default-features = false, optional = true, featu [dev-dependencies] tokio = { version = "1.0", default-features = false, features = ["rt", "macros"] } +lightning = { version = "0.1", features = ["std"] } +lightning-net-tokio = { version = "0.1", default-features = false } diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index a5f071b..06082ba 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -13,7 +13,10 @@ cargo check --verbose --color always cargo check --release --verbose --color always cargo test --no-default-features [ "$RUSTC_MINOR_VERSION" -gt 81 ] && cargo test --features http -cargo test --features std +# One std test syncs much of the lightning network graph, so --release is a must +# At least until https://github.com/lightningdevkit/rust-lightning/pull/3687 gets backported +export RUSTFLAGS="-C debug-assertions=on" +cargo test --features std --release [ "$RUSTC_MINOR_VERSION" -gt 81 ] && cargo doc --document-private-items --no-default-features [ "$RUSTC_MINOR_VERSION" -gt 81 ] && cargo doc --document-private-items --features http,std exit 0 diff --git a/src/lib.rs b/src/lib.rs index 9ad47fd..6ef9834 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,9 @@ pub mod dns_resolver; #[cfg(feature = "http")] pub mod http_resolver; +#[cfg(feature = "std")] // TODO: Drop once we upgrade to LDK 0.2 +pub mod onion_message_resolver; + pub mod amount; pub mod receive; diff --git a/src/onion_message_resolver.rs b/src/onion_message_resolver.rs new file mode 100644 index 0000000..39f42bb --- /dev/null +++ b/src/onion_message_resolver.rs @@ -0,0 +1,392 @@ +//! A [`HrnResolver`] which uses lightning onion messages and DNSSEC proofs to request DNS +//! resolution directly from untrusted lightning nodes, providing privacy through onion routing. + +use std::boxed::Box; +use std::collections::HashMap; +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; +use std::vec::Vec; + +use lightning::blinded_path::message::{DNSResolverContext, MessageContext}; +use lightning::ln::channelmanager::PaymentId; +use lightning::onion_message::dns_resolution::{ + DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, OMNameResolver, +}; +use lightning::onion_message::messenger::{ + Destination, MessageSendInstructions, Responder, ResponseInstruction, +}; +use lightning::routing::gossip::NetworkGraph; +use lightning::sign::EntropySource; +use lightning::util::logger::Logger; + +use crate::hrn_resolution::{ + HrnResolution, HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, +}; +use crate::Amount; + +struct OsRng; +impl EntropySource for OsRng { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let mut res = [0; 32]; + getrandom::fill(&mut res).expect("Fetching system randomness should always succeed"); + res + } +} + +struct ChannelState { + waker: Option, + result: Option, +} + +struct ChannelSend(Arc>); + +impl ChannelSend { + fn complete(self, result: HrnResolution) { + let mut state = self.0.lock().unwrap(); + state.result = Some(result); + if let Some(waker) = state.waker.take() { + waker.wake(); + } + } + + fn receiver_alive(&self) -> bool { + Arc::strong_count(&self.0) > 1 + } +} + +struct ChannelRecv(Arc>); + +impl Future for ChannelRecv { + type Output = HrnResolution; + fn poll(self: Pin<&mut Self>, context: &mut Context) -> Poll { + let mut state = self.0.lock().unwrap(); + if let Some(res) = state.result.take() { + state.waker = None; + Poll::Ready(res) + } else { + state.waker = Some(context.waker().clone()); + Poll::Pending + } + } +} + +fn channel() -> (ChannelSend, ChannelRecv) { + let state = Arc::new(Mutex::new(ChannelState { waker: None, result: None })); + (ChannelSend(Arc::clone(&state)), ChannelRecv(state)) +} + +/// A [`HrnResolver`] which uses lightning onion messages and DNSSEC proofs to request DNS +/// resolution directly from untrusted lightning nodes, providing privacy through onion routing. +/// +/// This implements LDK's [`DNSResolverMessageHandler`], which it uses to send onion messages (you +/// should make sure to call LDK's [`PeerManager::process_events`] after a query begins) and +/// process response messages. +/// +/// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events +pub struct LDKOnionMessageDNSSECHrnResolver>, L: Deref> +where + L::Target: Logger, +{ + network_graph: N, + resolver: OMNameResolver, + next_id: AtomicUsize, + pending_resolutions: Mutex>>, + message_queue: Mutex>, +} + +impl>, L: Deref> LDKOnionMessageDNSSECHrnResolver +where + L::Target: Logger, +{ + /// Constructs a new [`LDKOnionMessageDNSSECHrnResolver`]. + /// + /// See the struct-level documentation for more info. + pub fn new(network_graph: N) -> Self { + Self { + network_graph, + next_id: AtomicUsize::new(0), + // TODO: Swap for `new_without_expiry_validation` when we upgrade to LDK 0.2 + resolver: OMNameResolver::new(0, 0), + pending_resolutions: Mutex::new(HashMap::new()), + message_queue: Mutex::new(Vec::new()), + } + } + + fn init_resolve_hrn<'a>( + &'a self, hrn: &HumanReadableName, + ) -> Result { + #[cfg(feature = "std")] + { + use std::time::SystemTime; + let clock_err = + "DNSSEC validation relies on having a correct system clock. It is currently set before 1970."; + let now = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|_| clock_err)?; + // Use `now / 60` as the block height to expire pending requests after 1-2 minutes. + self.resolver.new_best_block((now.as_secs() / 60) as u32, now.as_secs() as u32); + } + + let mut dns_resolvers = Vec::new(); + for (node_id, node) in self.network_graph.read_only().nodes().unordered_iter() { + if let Some(info) = &node.announcement_info { + // Sadly, 31 nodes currently squat on the DNS Resolver feature bit + // without speaking it. + // Its unclear why they're doing so, but none of them currently + // also have the onion messaging feature bit set, so here we check + // for both. + let supports_dns = info.features().supports_dns_resolution(); + let supports_om = info.features().supports_onion_messages(); + if supports_dns && supports_om { + if let Ok(pubkey) = node_id.as_pubkey() { + dns_resolvers.push(Destination::Node(pubkey)); + } + } + } + if dns_resolvers.len() > 5 { + break; + } + } + if dns_resolvers.is_empty() { + return Err( + "Failed to find any DNS resolving nodes, check your network graph is synced", + ); + } + + let counter = self.next_id.fetch_add(1, Ordering::Relaxed) as u64; + let mut payment_id = [0; 32]; + payment_id[..8].copy_from_slice(&counter.to_ne_bytes()); + let payment_id = PaymentId(payment_id); + + let err = "The provided HRN did not fit in a DNS request"; + // TODO: Once LDK 0.2 ships with a new context authentication method, we shouldn't need the + // RNG here and can stop depending on std. + let (query, dns_context) = + self.resolver.resolve_name(payment_id, hrn.clone(), &OsRng).map_err(|_| err)?; + let context = MessageContext::DNSResolver(dns_context); + + let mut queue = self.message_queue.lock().unwrap(); + for destination in dns_resolvers { + let instructions = + MessageSendInstructions::WithReplyPath { destination, context: context.clone() }; + queue.push((DNSResolverMessage::DNSSECQuery(query.clone()), instructions)); + } + + let (send, recv) = channel(); + let mut pending_resolutions = self.pending_resolutions.lock().unwrap(); + let senders = pending_resolutions.entry(hrn.clone()).or_insert_with(Vec::new); + senders.push((payment_id, send)); + + // If we're running in no-std, we won't expire lookups with the time updates above, so walk + // the pending resolution list and expire them here. + pending_resolutions.retain(|_name, resolutions| { + resolutions.retain(|(_payment_id, resolution)| { + let has_receiver = resolution.receiver_alive(); + if !has_receiver { + // TODO: Once LDK 0.2 ships, expire the pending resolution in the resolver: + // self.resolver.expire_pending_resolution(name, payment_id); + } + has_receiver + }); + !resolutions.is_empty() + }); + + Ok(recv) + } +} + +impl>, L: Deref> DNSResolverMessageHandler + for LDKOnionMessageDNSSECHrnResolver +where + L::Target: Logger, +{ + fn handle_dnssec_query( + &self, _: DNSSECQuery, _: Option, + ) -> Option<(DNSResolverMessage, ResponseInstruction)> { + None + } + + fn handle_dnssec_proof(&self, msg: DNSSECProof, context: DNSResolverContext) { + let results = self.resolver.handle_dnssec_proof_for_uri(msg.clone(), context); + if let Some((resolved, res)) = results { + let mut pending_resolutions = self.pending_resolutions.lock().unwrap(); + for (name, _payment_id) in resolved { + if let Some(requests) = pending_resolutions.remove(&name) { + for (_id, send) in requests { + send.complete(HrnResolution::DNSSEC { + proof: Some(msg.proof.clone()), + result: res.clone(), + }); + } + } + } + } + } + + fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { + std::mem::take(&mut self.message_queue.lock().unwrap()) + } +} + +impl> + Sync, L: Deref> HrnResolver + for LDKOnionMessageDNSSECHrnResolver +where + L::Target: Logger, +{ + fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> { + match self.init_resolve_hrn(hrn) { + Err(e) => Box::pin(async move { Err(e) }), + Ok(recv) => Box::pin(async move { Ok(recv.await) }), + } + } + + fn resolve_lnurl<'a>(&'a self, _url: &'a str) -> HrnResolutionFuture<'a> { + let err = "DNS resolver does not support LNURL resolution"; + Box::pin(async move { Err(err) }) + } + + fn resolve_lnurl_to_invoice<'a>( + &'a self, _: String, _: Amount, _: [u8; 32], + ) -> LNURLResolutionFuture<'a> { + let err = "resolve_lnurl_to_invoice shouldn't be called when we don't resolve LNURL"; + debug_assert!(false, "{err}"); + Box::pin(async move { Err(err) }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + + use std::net::ToSocketAddrs; + + use bitcoin::hex::FromHex; + use bitcoin::secp256k1::PublicKey; + + use lightning::blinded_path::NodeIdLookUp; + use lightning::ln::peer_handler::{ + ErroringMessageHandler, IgnoringMessageHandler, MessageHandler, PeerManager, + }; + use lightning::onion_message::messenger::{DefaultMessageRouter, OnionMessenger}; + use lightning::routing::gossip::{NodeId, P2PGossipSync}; + use lightning::routing::utxo::UtxoLookup; + use lightning::sign::KeysManager; + use lightning::util::logger::Record; + + struct TestLogger; + impl Logger for TestLogger { + fn log(&self, r: Record) { + eprintln!("{}", r.args); + } + } + + struct NoPeers; + impl NodeIdLookUp for NoPeers { + fn next_node_id(&self, _scid: u64) -> Option { + None + } + } + + #[tokio::test] + async fn test_dns_om_hrn_resolver() { + let graph = Arc::new(NetworkGraph::new(Network::Bitcoin, &TestLogger)); + let resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&graph))); + let signer = Arc::new(KeysManager::new(&OsRng.get_secure_random_bytes(), 0, 0)); + let message_router = Arc::new(DefaultMessageRouter::new(Arc::clone(&graph), &OsRng)); + let messenger = Arc::new(OnionMessenger::new( + &OsRng, + Arc::clone(&signer), + &TestLogger, + &NoPeers, + message_router, + &IgnoringMessageHandler {}, + &IgnoringMessageHandler {}, + Arc::clone(&resolver), + &IgnoringMessageHandler {}, + )); + let no_utxos = None::<&(dyn UtxoLookup + Sync + Send)>; + let handlers = MessageHandler { + chan_handler: Arc::new(ErroringMessageHandler::new()), + route_handler: Arc::new(P2PGossipSync::new(Arc::clone(&graph), no_utxos, &TestLogger)), + onion_message_handler: Arc::clone(&messenger), + custom_message_handler: &IgnoringMessageHandler {}, + }; + let rand = OsRng.get_secure_random_bytes(); + let peer_manager = + Arc::new(PeerManager::new(handlers, 0, &rand, &TestLogger, Arc::clone(&signer))); + + // Connect to a static LDK node which we know will do DNS resolutions for us. + let their_id_hex = "03db10aa09ff04d3568b0621750794063df401e6853c79a21a83e1a3f3b5bfb0c8"; + let their_id = PublicKey::from_slice(&Vec::::from_hex(their_id_hex).unwrap()).unwrap(); + let addr = "ldk-ln-node.bitcoin.ninja:9735".to_socket_addrs().unwrap().next().unwrap(); + let _ = lightning_net_tokio::connect_outbound(Arc::clone(&peer_manager), their_id, addr) + .await + .unwrap(); + + let pm_reference = Arc::clone(&peer_manager); + tokio::spawn(async move { + pm_reference.process_events(); + tokio::time::sleep(Duration::from_micros(10)).await; + }); + + let their_node_id = NodeId::from_pubkey(&their_id); + loop { + { + let graph = graph.read_only(); + let have_announcement = + graph.nodes().get(&their_node_id).map(|node| node.announcement_info.is_some()); + if have_announcement.unwrap_or(false) { + break; + } + } + tokio::time::sleep(Duration::from_millis(5)).await; + peer_manager.process_events(); + } + + let instructions = PaymentInstructions::parse( + "send.some@satsto.me", + bitcoin::Network::Bitcoin, + &*resolver, + true, + ) + .await + .unwrap(); + + let resolved = if let PaymentInstructions::ConfigurableAmount(instr) = instructions { + assert_eq!(instr.min_amt(), None); + assert_eq!(instr.max_amt(), None); + + assert_eq!(instr.pop_callback(), None); + assert!(instr.bip_353_dnssec_proof().is_some()); + + let hrn = instr.human_readable_name().as_ref().unwrap(); + assert_eq!(hrn.user(), "send.some"); + assert_eq!(hrn.domain(), "satsto.me"); + + instr.set_amount(Amount::from_sats(100_000).unwrap(), &*resolver).await.unwrap() + } else { + panic!(); + }; + + assert_eq!(resolved.pop_callback(), None); + assert!(resolved.bip_353_dnssec_proof().is_some()); + + let hrn = resolved.human_readable_name().as_ref().unwrap(); + assert_eq!(hrn.user(), "send.some"); + assert_eq!(hrn.domain(), "satsto.me"); + + for method in resolved.methods() { + match method { + PaymentMethod::LightningBolt11(_) => { + panic!("Should only have static payment instructions"); + }, + PaymentMethod::LightningBolt12(_) => {}, + PaymentMethod::OnChain { .. } => {}, + } + } + } +} From 45e53e28e38fbdd5c842f45caf90d17c2098dede Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Fri, 11 Jul 2025 20:07:33 +0000 Subject: [PATCH 3/3] f add callbacker --- src/onion_message_resolver.rs | 76 +++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/src/onion_message_resolver.rs b/src/onion_message_resolver.rs index 39f42bb..136af21 100644 --- a/src/onion_message_resolver.rs +++ b/src/onion_message_resolver.rs @@ -7,7 +7,7 @@ use std::future::Future; use std::ops::Deref; use std::pin::Pin; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::task::{Context, Poll, Waker}; use std::vec::Vec; @@ -82,10 +82,13 @@ fn channel() -> (ChannelSend, ChannelRecv) { /// A [`HrnResolver`] which uses lightning onion messages and DNSSEC proofs to request DNS /// resolution directly from untrusted lightning nodes, providing privacy through onion routing. /// -/// This implements LDK's [`DNSResolverMessageHandler`], which it uses to send onion messages (you -/// should make sure to call LDK's [`PeerManager::process_events`] after a query begins) and +/// This implements LDK's [`DNSResolverMessageHandler`], which it uses to send onion messages and /// process response messages. /// +/// Note that after a query begines, [`PeerManager::process_events`] should be called to ensure the +/// query message goes out in a timely manner. You can call [`Self::register_post_queue_action`] to +/// have this happen automatically. +/// /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events pub struct LDKOnionMessageDNSSECHrnResolver>, L: Deref> where @@ -96,6 +99,7 @@ where next_id: AtomicUsize, pending_resolutions: Mutex>>, message_queue: Mutex>, + pm_event_poker: RwLock>>, } impl>, L: Deref> LDKOnionMessageDNSSECHrnResolver @@ -113,9 +117,18 @@ where resolver: OMNameResolver::new(0, 0), pending_resolutions: Mutex::new(HashMap::new()), message_queue: Mutex::new(Vec::new()), + pm_event_poker: RwLock::new(None), } } + /// Sets a callback which is called any time a new resolution begins and a message is available + /// to be sent. This should generally call [`PeerManager::process_events`]. + /// + /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events + pub fn register_post_queue_action(&self, callback: Box) { + *self.pm_event_poker.write().unwrap() = Some(callback); + } + fn init_resolve_hrn<'a>( &'a self, hrn: &HumanReadableName, ) -> Result { @@ -168,31 +181,42 @@ where self.resolver.resolve_name(payment_id, hrn.clone(), &OsRng).map_err(|_| err)?; let context = MessageContext::DNSResolver(dns_context); - let mut queue = self.message_queue.lock().unwrap(); - for destination in dns_resolvers { - let instructions = - MessageSendInstructions::WithReplyPath { destination, context: context.clone() }; - queue.push((DNSResolverMessage::DNSSECQuery(query.clone()), instructions)); - } - let (send, recv) = channel(); - let mut pending_resolutions = self.pending_resolutions.lock().unwrap(); - let senders = pending_resolutions.entry(hrn.clone()).or_insert_with(Vec::new); - senders.push((payment_id, send)); - - // If we're running in no-std, we won't expire lookups with the time updates above, so walk - // the pending resolution list and expire them here. - pending_resolutions.retain(|_name, resolutions| { - resolutions.retain(|(_payment_id, resolution)| { - let has_receiver = resolution.receiver_alive(); - if !has_receiver { - // TODO: Once LDK 0.2 ships, expire the pending resolution in the resolver: - // self.resolver.expire_pending_resolution(name, payment_id); - } - has_receiver + { + let mut pending_resolutions = self.pending_resolutions.lock().unwrap(); + let senders = pending_resolutions.entry(hrn.clone()).or_insert_with(Vec::new); + senders.push((payment_id, send)); + + // If we're running in no-std, we won't expire lookups with the time updates above, so walk + // the pending resolution list and expire them here. + pending_resolutions.retain(|_name, resolutions| { + resolutions.retain(|(_payment_id, resolution)| { + let has_receiver = resolution.receiver_alive(); + if !has_receiver { + // TODO: Once LDK 0.2 ships, expire the pending resolution in the resolver: + // self.resolver.expire_pending_resolution(name, payment_id); + } + has_receiver + }); + !resolutions.is_empty() }); - !resolutions.is_empty() - }); + } + + { + let mut queue = self.message_queue.lock().unwrap(); + for destination in dns_resolvers { + let instructions = MessageSendInstructions::WithReplyPath { + destination, + context: context.clone(), + }; + queue.push((DNSResolverMessage::DNSSECQuery(query.clone()), instructions)); + } + } + + let callback = self.pm_event_poker.read().unwrap(); + if let Some(callback) = &*callback { + callback(); + } Ok(recv) }