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 406fee2455b..4ec484e77e1 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -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/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-dns-resolver/Cargo.toml b/lightning-dns-resolver/Cargo.toml new file mode 100644 index 00000000000..1c2ebe615b2 --- /dev/null +++ b/lightning-dns-resolver/Cargo.toml @@ -0,0 +1,19 @@ +[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 } +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"] } + +[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..8f855cb5fb7 --- /dev/null +++ b/lightning-dns-resolver/src/lib.rs @@ -0,0 +1,462 @@ +//! 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 lightning_types::features::NodeFeatures; + +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: Mutex>, +} + +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, 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, runtime_handle: Option, + ) -> 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 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, + }; + 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 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()) + } +} + +#[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/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/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 575a1b2015f..9ac08440430 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, @@ -2564,6 +2570,19 @@ where /// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate. last_days_feerates: Mutex>, + #[cfg(feature = "dnssec")] + hrn_resolver: OMNameResolver, + #[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, @@ -3386,6 +3405,14 @@ 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()), + + #[cfg(feature = "_test_utils")] + testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), } } @@ -3605,11 +3632,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 { .. } => { @@ -9579,6 +9606,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; @@ -9602,6 +9649,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); @@ -9613,17 +9664,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) } @@ -9764,6 +9805,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. /// @@ -10387,6 +10495,10 @@ where } } max_time!(self.highest_seen_timestamp); + #[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)> { @@ -11637,6 +11749,69 @@ 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); + #[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), + |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 @@ -12254,6 +12429,7 @@ where } } PendingOutboundPayment::AwaitingInvoice { .. } => {}, + PendingOutboundPayment::AwaitingOffer { .. } => {}, PendingOutboundPayment::InvoiceReceived { .. } => {}, PendingOutboundPayment::StaticInvoiceReceived { .. } => {}, PendingOutboundPayment::Fulfilled { .. } => {}, @@ -13320,6 +13496,14 @@ 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()), + + #[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/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 9d6da1c8f2e..a6e07a8bc44 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, >; @@ -3294,6 +3308,13 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec, }, + /// 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 @@ -1622,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 @@ -1910,7 +1983,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 +2171,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 +2264,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 +2367,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)] diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index b1d7660f224..e7e4596901b 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, + offer_from_hrn: 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, + offer_from_hrn: 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..f7f1c228779 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, + offer_from_hrn: None, #[cfg(test)] experimental_bar: None, } @@ -301,6 +303,14 @@ macro_rules! invoice_request_builder_methods { ( $return_value } + /// Sets the [`InvoiceRequest::offer_from_hrn`]. + /// + /// 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.offer_from_hrn = 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, + offer_from_hrn: 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 offer_from_hrn(&$self) -> &Option { + $contents.offer_from_hrn() + } } } 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; @@ -1015,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.offer_from_hrn().clone(), } } } @@ -1049,6 +1065,10 @@ impl InvoiceRequestContents { .map(|payer_note| PrintableString(payer_note.as_str())) } + pub(super) fn offer_from_hrn(&self) -> &Option { + &self.inner.offer_from_hrn + } + 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 +1105,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey { quantity: self.quantity, payer_id: None, payer_note: self.payer_note.as_ref(), + offer_from_hrn: self.offer_from_hrn.as_ref(), paths: None, }; @@ -1142,6 +1163,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, offer_from_hrn: HumanReadableName), }); /// Valid type range for experimental invoice_request TLV records. @@ -1266,6 +1288,7 @@ impl TryFrom for InvoiceRequestContents { offer_tlv_stream, InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note, paths, + offer_from_hrn, }, experimental_offer_tlv_stream, ExperimentalInvoiceRequestTlvStream { @@ -1305,6 +1328,7 @@ impl TryFrom for InvoiceRequestContents { Ok(InvoiceRequestContents { inner: InvoiceRequestContentsWithoutPayerSigningPubkey { payer, offer, chain, amount_msats: amount, features, quantity, payer_note, + offer_from_hrn, #[cfg(test)] experimental_bar, }, @@ -1327,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`]. @@ -1336,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), }); @@ -1347,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))), }); @@ -1355,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, }) } } @@ -1484,6 +1514,7 @@ mod tests { payer_id: Some(&payer_pubkey()), payer_note: None, paths: None, + offer_from_hrn: None, }, SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) }, ExperimentalOfferTlvStreamRef { @@ -2709,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, } ); 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..7a47ad99b3b 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(), + offer_from_hrn: 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, + offer_from_hrn, }, ExperimentalOfferTlvStream { #[cfg(test)] @@ -940,6 +942,11 @@ impl TryFrom for RefundContents { return Err(Bolt12SemanticError::UnexpectedIssuerSigningPubkey); } + if offer_from_hrn.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, + offer_from_hrn: None, }, ExperimentalOfferTlvStreamRef { experimental_foo: None, 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(()); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 0331a1060b8..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 } } } @@ -1836,6 +1838,7 @@ where /// [`SimpleArcChannelManager`]: crate::ln::channelmanager::SimpleArcChannelManager /// [`SimpleArcPeerManager`]: crate::ln::peer_handler::SimpleArcPeerManager #[cfg(not(c_bindings))] +#[cfg(feature = "dnssec")] pub type SimpleArcOnionMessenger = OnionMessenger< Arc, Arc, @@ -1844,7 +1847,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 +1903,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 +1914,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 >;