]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add a `lightning-dns-resolver` crate which answers bLIP 32 queries
authorMatt Corallo <git@bluematt.me>
Sun, 14 Jul 2024 13:08:04 +0000 (13:08 +0000)
committerMatt Corallo <git@bluematt.me>
Tue, 12 Nov 2024 15:53:35 +0000 (15:53 +0000)
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
Cargo.toml
ci/ci-tests.sh
lightning-dns-resolver/Cargo.toml [new file with mode: 0644]
lightning-dns-resolver/src/lib.rs [new file with mode: 0644]
lightning/src/ln/channelmanager.rs
lightning/src/onion_message/messenger.rs

index fbeffa8a9c9f5e3fc0641149f7b2b8cd0afc2a8a..8507aea83683e9e99da809d398c0c353a2a41714 100644 (file)
@@ -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
index f0f09f547f46384befebb6f1def81cfa564aae6d..b4ba58bac9fc2d221dbc53c381a99f6dcd9ac4cf 100644 (file)
@@ -15,6 +15,7 @@ members = [
     "lightning-custom-message",
     "lightning-transaction-sync",
     "lightning-macros",
+    "lightning-dns-resolver",
     "possiblyrandom",
 ]
 
index 406fee2455b579bfe22275a460ed7b423f7ed415..4ec484e77e19bbeb99cf85c090448970a3ca3a43 100755 (executable)
@@ -54,6 +54,7 @@ WORKSPACE_MEMBERS=(
        lightning-custom-message
        lightning-transaction-sync
        lightning-macros
+       lightning-dns-resolver
        possiblyrandom
 )
 
@@ -64,10 +65,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 (file)
index 0000000..1430467
--- /dev/null
@@ -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 (file)
index 0000000..83a2ade
--- /dev/null
@@ -0,0 +1,454 @@
+//! 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<PH: Deref>
+where
+       PH::Target: DNSResolverMessageHandler,
+{
+       state: Arc<OMResolverState>,
+       proof_handler: Option<PH>,
+       runtime_handle: Mutex<Option<Handle>>,
+}
+
+const MAX_PENDING_RESPONSES: usize = 1024;
+struct OMResolverState {
+       resolver: SocketAddr,
+       pending_replies: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>,
+       pending_query_count: AtomicUsize,
+}
+
+impl OMDomainResolver<IgnoringMessageHandler> {
+       /// 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<PH: Deref> OMDomainResolver<PH>
+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<PH>) -> Self {
+               Self::with_runtime(resolver, proof_handler, Some(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. If no runtime is provided, `set_runtime` must
+       /// be called before any queries will be handled.
+       ///
+       /// 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<PH>, runtime_handle: Option<Handle>,
+       ) -> Self {
+               Self {
+                       state: Arc::new(OMResolverState {
+                               resolver,
+                               pending_replies: Mutex::new(Vec::new()),
+                               pending_query_count: AtomicUsize::new(0),
+                       }),
+                       proof_handler,
+                       runtime_handle: Mutex::new(runtime_handle),
+               }
+       }
+
+       /// Sets the runtime on which futures will be spawned.
+       pub fn set_runtime(&self, runtime_handle: Handle) {
+               *self.runtime_handle.lock().unwrap() = Some(runtime_handle);
+       }
+}
+
+impl<PH: Deref> DNSResolverMessageHandler for OMDomainResolver<PH>
+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<Responder>,
+       ) -> Option<(DNSResolverMessage, ResponseInstruction)> {
+               let responder = match responder_opt {
+                       Some(responder) => responder,
+                       None => return None,
+               };
+               let runtime = if let Some(runtime) = self.runtime_handle.lock().unwrap().clone() {
+                       runtime
+               } else {
+                       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);
+               runtime.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<PublicKey> {
+                       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<PublicKey>, destination: Destination,
+               ) -> Result<OnionMessagePath, ()> {
+                       Ok(OnionMessagePath {
+                               destination,
+                               first_node_addresses: None,
+                               intermediate_nodes: Vec::new(),
+                       })
+               }
+
+               fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>(
+                       &self, recipient: PublicKey, context: MessageContext, _peers: Vec<PublicKey>,
+                       secp_ctx: &Secp256k1<T>,
+               ) -> Result<Vec<BlindedMessagePath>, ()> {
+                       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<Option<(HumanReadableName, PaymentId, String)>>,
+               resolver: OMNameResolver,
+               pending_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>,
+       }
+       impl DNSResolverMessageHandler for URIResolver {
+               fn handle_dnssec_query(
+                       &self, _: DNSSECQuery, _: Option<Responder>,
+               ) -> 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);
+       }
+}
index 8ddb2aa116376c5cbb0b7b1822f541198a782417..9ac08440430303b7adb087fc90e13300b3546e76 100644 (file)
@@ -2575,6 +2575,14 @@ where
        #[cfg(feature = "dnssec")]
        pending_dns_onion_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>,
 
+       #[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<HashMap<HumanReadableName, Offer>>,
+
        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()),
                }
        }
 
@@ -11760,8 +11771,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),
@@ -13483,6 +13501,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() {
index 001fa6a4962dc3b4d4405f08f7b7466ca38fb37f..acac9dc006abf4bec6fe169d96f8a8fd65d9e2b3 100644 (file)
@@ -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 }
        }
 }