Support paying Human Readable Names directly from ChannelManager

Now that we have the ability to resolve BIP 353 Human Readable
Names directly and have tracking for outbound payments waiting on
an offer resolution, we can implement full BIP 353 support in
`ChannelManager`.

Users will need one or more known nodes which offer DNS resolution
service over onion messages using bLIP 32, which they pass to
`ChannelManager::pay_for_offer_from_human_readable_name`, as well
as the `HumanReadableName` itself.

From there, `ChannelManager` asks the DNS resolver to provide a
DNSSEC proof, which it verifies, parses into an `Offer`, and then
pays.

For those who wish to support on-chain fallbacks, sadly, this will
not work, and they'll still have to use `OMNameResolver` directly
in order to use their existing `bitcoin:` URI parsing.
This commit is contained in:
Matt Corallo 2024-11-07 15:05:26 +00:00
parent 8d8416b956
commit 99d00930a4
2 changed files with 229 additions and 11 deletions

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,11 @@ 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)>>,
entropy_source: ES,
node_signer: NS,
signer_provider: SP,
@ -3386,6 +3397,11 @@ where
signer_provider,
logger,
#[cfg(feature = "dnssec")]
hrn_resolver: OMNameResolver::new(current_timestamp, params.best_block.height),
#[cfg(feature = "dnssec")]
pending_dns_onion_messages: Mutex::new(Vec::new()),
}
}
@ -9579,6 +9595,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 +9638,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 +9653,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 +9794,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 +10484,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 +11738,62 @@ 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);
if let Some((completed_requests, offer)) = offer_opt {
for (name, payment_id) in completed_requests {
if let Ok(amt_msats) = self.pending_outbound_payments.amt_msats_for_payment_awaiting_offer(payment_id) {
let offer_pay_res =
self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name),
|invoice_request, nonce| {
let retryable_invoice_request = RetryableInvoiceRequest {
invoice_request: invoice_request.clone(),
nonce,
};
self.pending_outbound_payments
.received_offer(payment_id, Some(retryable_invoice_request))
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)
});
if offer_pay_res.is_err() {
// The offer we tried to pay is the canonical current offer for the name we
// wanted to pay. If we can't pay it, there's no way to recover so fail the
// payment.
// Note that the PaymentFailureReason should be ignored for an
// AwaitingInvoice payment.
self.pending_outbound_payments.abandon_payment(
payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events,
);
}
}
}
}
}
fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> {
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
}
}
impl<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
@ -13321,6 +13478,11 @@ where
logger: args.logger,
default_configuration: args.default_config,
#[cfg(feature = "dnssec")]
hrn_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height),
#[cfg(feature = "dnssec")]
pending_dns_onion_messages: Mutex::new(Vec::new()),
};
for (_, monitor) in args.channel_monitors.iter() {

View file

@ -1639,6 +1639,62 @@ impl OutboundPayments {
(payment, onion_session_privs)
}
#[cfg(feature = "dnssec")]
pub(super) fn add_new_awaiting_offer(
&self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry,
max_total_routing_fee_msat: Option<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>