Merge pull request #3283 from TheBlueMatt/2024-07-human-readable-names-resolution

Support paying directly to Human Readable Names using bLIP 32
This commit is contained in:
Elias Rohrer 2024-11-12 18:30:01 +01:00 committed by GitHub
commit f152689d0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 935 additions and 49 deletions

1
.gitignore vendored
View 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

View File

@ -15,6 +15,7 @@ members = [
"lightning-custom-message",
"lightning-transaction-sync",
"lightning-macros",
"lightning-dns-resolver",
"possiblyrandom",
]

View File

@ -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

View File

@ -89,6 +89,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>(
payer_note_truncated: invoice_request
.payer_note()
.map(|s| UntrustedString(s.to_string())),
human_readable_name: None,
},
});
let payee_tlvs = ReceiveTlvs {

View File

@ -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"] }

View File

@ -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<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 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<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);
}
}

View File

@ -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,

View File

@ -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<VecDeque<(u32, u32)>>,
#[cfg(feature = "dnssec")]
hrn_resolver: OMNameResolver,
#[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,
@ -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<RecentPaymentDetails> {
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 })
},
PendingOutboundPayment::AwaitingInvoice { .. }
| PendingOutboundPayment::AwaitingOffer { .. }
// InvoiceReceived is an intermediate state and doesn't need to be exposed
PendingOutboundPayment::InvoiceReceived { .. } => {
| PendingOutboundPayment::InvoiceReceived { .. } =>
{
Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id })
},
PendingOutboundPayment::StaticInvoiceReceived { .. } => {
@ -9579,6 +9606,26 @@ where
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
payer_note: Option<String>, payment_id: PaymentId, retry_strategy: Retry,
max_total_routing_fee_msat: Option<u64>
) -> 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<CPP: FnOnce(&InvoiceRequest, Nonce) -> Result<(), Bolt12SemanticError>>(
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
payer_note: Option<String>, payment_id: PaymentId,
human_readable_name: Option<HumanReadableName>, 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<u64>,
dns_resolvers: Vec<Destination>,
) -> 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<BlockHash>)> {
@ -11637,6 +11749,69 @@ where
}
}
#[cfg(feature = "dnssec")]
impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, MR: Deref, L: Deref>
DNSResolverMessageHandler for ChannelManager<M, T, ES, NS, SP, F, R, MR, L>
where
M::Target: chain::Watch<<SP::Target as SignerProvider>::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<Responder>,
) -> 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<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, MR: Deref, L: Deref>
NodeIdLookUp for ChannelManager<M, T, ES, NS, SP, F, R, MR, L>
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() {

View File

@ -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<NodeC
for i in 0..node_count {
let dedicated_entropy = DedicatedEntropy(RandomBytes::new([i as u8; 32]));
#[cfg(feature = "dnssec")]
let onion_messenger = OnionMessenger::new(
dedicated_entropy, cfgs[i].keys_manager, cfgs[i].logger, &chan_mgrs[i],
&cfgs[i].message_router, &chan_mgrs[i], &chan_mgrs[i], &chan_mgrs[i],
IgnoringMessageHandler {},
);
#[cfg(not(feature = "dnssec"))]
let onion_messenger = OnionMessenger::new(
dedicated_entropy, cfgs[i].keys_manager, cfgs[i].logger, &chan_mgrs[i],
&cfgs[i].message_router, &chan_mgrs[i], &chan_mgrs[i], IgnoringMessageHandler {},

View File

@ -564,6 +564,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
assert_eq!(invoice_request.amount_msats(), None);
@ -724,6 +725,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
assert_eq!(invoice_request.amount_msats(), None);
@ -844,6 +846,7 @@ fn pays_for_offer_without_blinded_paths() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
@ -1111,6 +1114,7 @@ fn creates_and_pays_for_offer_with_retry() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
assert_eq!(invoice_request.amount_msats(), None);
@ -1175,6 +1179,7 @@ fn pays_bolt12_invoice_asynchronously() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
@ -1264,6 +1269,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() {
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
quantity: None,
payer_note_truncated: None,
human_readable_name: None,
},
});
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);

View File

@ -58,6 +58,15 @@ pub(crate) enum PendingOutboundPayment {
Legacy {
session_privs: HashSet<[u8; 32]>,
},
/// Used when we are waiting for an Offer to come back from a BIP 353 resolution
AwaitingOffer {
expiration: StaleExpiration,
retry_strategy: Retry,
max_total_routing_fee_msat: Option<u64>,
/// 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<PaymentHash> {
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,6 +227,7 @@ impl PendingOutboundPayment {
PendingOutboundPayment::Retryable { session_privs, .. } |
PendingOutboundPayment::Fulfilled { session_privs, .. } |
PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs,
PendingOutboundPayment::AwaitingOffer { .. } |
PendingOutboundPayment::AwaitingInvoice { .. } |
PendingOutboundPayment::InvoiceReceived { .. } |
PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); return; },
@ -258,6 +269,7 @@ impl PendingOutboundPayment {
PendingOutboundPayment::Abandoned { session_privs, .. } => {
session_privs.remove(session_priv)
},
PendingOutboundPayment::AwaitingOffer { .. } |
PendingOutboundPayment::AwaitingInvoice { .. } |
PendingOutboundPayment::InvoiceReceived { .. } |
PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); false },
@ -288,6 +300,7 @@ impl PendingOutboundPayment {
PendingOutboundPayment::Retryable { session_privs, .. } => {
session_privs.insert(session_priv)
},
PendingOutboundPayment::AwaitingOffer { .. } |
PendingOutboundPayment::AwaitingInvoice { .. } |
PendingOutboundPayment::InvoiceReceived { .. } |
PendingOutboundPayment::StaticInvoiceReceived { .. } => { debug_assert!(false); 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<u64>, 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<u64, ()> {
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<RetryableInvoiceRequest>,
) -> 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<u64>, retryable_invoice_request: Option<RetryableInvoiceRequest>
@ -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,7 +2171,8 @@ 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() {
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,
@ -2105,13 +2181,18 @@ impl OutboundPayments {
}, None));
payment.remove();
}
} else if let PendingOutboundPayment::AwaitingInvoice { .. } = payment.get() {
},
PendingOutboundPayment::AwaitingInvoice { .. }
| PendingOutboundPayment::AwaitingOffer { .. } =>
{
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
payment_id,
payment_hash: None,
reason: Some(reason),
}, None));
payment.remove();
},
_ => {},
}
}
}
@ -2183,6 +2264,7 @@ 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::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)]

View File

@ -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()))),

View File

@ -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<Keypair>, Option<&'b Secp256k1<$secp_context>>),
Bolt12SemanticError
@ -699,6 +709,7 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
features: InvoiceRequestFeatures,
quantity: Option<u64>,
payer_note: Option<String>,
offer_from_hrn: Option<HumanReadableName>,
#[cfg(test)]
experimental_bar: Option<u64>,
}
@ -745,6 +756,12 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
pub fn payer_note(&$self) -> Option<PrintableString> {
$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<HumanReadableName> {
$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<HumanReadableName> {
&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<BlindedMessagePath>, WithoutLength)),
(91, offer_from_hrn: HumanReadableName),
});
/// Valid type range for experimental invoice_request TLV records.
@ -1266,6 +1288,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> 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<PartialInvoiceRequestTlvStream> 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<UntrustedString>,
/// The Human Readable Name which the sender indicated they were paying to.
pub human_readable_name: Option<HumanReadableName>,
}
/// The maximum number of characters included in [`InvoiceRequestFields::payer_note_truncated`].
@ -1336,6 +1363,7 @@ impl Writeable for InvoiceRequestFields {
fn write<W: Writer>(&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<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
_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,
}
);

View File

@ -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<CheckedHrpstringError> for Bolt12ParseError {

View File

@ -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<RefundTlvStream> 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<RefundTlvStream> 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,

View File

@ -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<HumanReadableName, ()> {
pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
// 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(());

View 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 }
}
}
@ -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<M, T, F, L> = OnionMessenger<
Arc<KeysManager>,
Arc<KeysManager>,
@ -1844,7 +1847,28 @@ pub type SimpleArcOnionMessenger<M, T, F, L> = OnionMessenger<
Arc<DefaultMessageRouter<Arc<NetworkGraph<Arc<L>>>, Arc<L>, Arc<KeysManager>>>,
Arc<SimpleArcChannelManager<M, T, F, L>>,
Arc<SimpleArcChannelManager<M, T, F, L>>,
IgnoringMessageHandler, // TODO: Swap for ChannelManager (when built with the "dnssec" feature)
Arc<SimpleArcChannelManager<M, T, F, L>>,
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<M, T, F, L> = OnionMessenger<
Arc<KeysManager>,
Arc<KeysManager>,
Arc<L>,
Arc<SimpleArcChannelManager<M, T, F, L>>,
Arc<DefaultMessageRouter<Arc<NetworkGraph<Arc<L>>>, Arc<L>, Arc<KeysManager>>>,
Arc<SimpleArcChannelManager<M, T, F, L>>,
Arc<SimpleArcChannelManager<M, T, F, L>>,
IgnoringMessageHandler,
IgnoringMessageHandler
>;
@ -1856,6 +1880,7 @@ pub type SimpleArcOnionMessenger<M, T, F, L> = OnionMessenger<
/// [`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<
@ -1866,7 +1891,30 @@ 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)
&'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>,
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(not(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>,
IgnoringMessageHandler,
IgnoringMessageHandler
>;