Skip to content

Commit 6ed0ab1

Browse files
committed
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.
1 parent d62982a commit 6ed0ab1

File tree

2 files changed

+220
-6
lines changed

2 files changed

+220
-6
lines changed

lightning/src/ln/channelmanager.rs

+166-6
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ use crate::offers::parse::Bolt12SemanticError;
7272
use crate::offers::refund::{Refund, RefundBuilder};
7373
use crate::offers::signer;
7474
use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler};
75+
use crate::onion_message::dns_resolution::HumanReadableName;
7576
use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions};
7677
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
7778
use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider};
@@ -84,6 +85,11 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe
8485
use crate::util::logger::{Level, Logger, WithContext};
8586
use crate::util::errors::APIError;
8687

88+
#[cfg(feature = "dnssec")]
89+
use crate::blinded_path::message::DNSResolverContext;
90+
#[cfg(feature = "dnssec")]
91+
use crate::onion_message::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler, DNSSECQuery, DNSSECProof, OMNameResolver};
92+
8793
#[cfg(not(c_bindings))]
8894
use {
8995
crate::offers::offer::DerivedMetadata,
@@ -2300,6 +2306,11 @@ where
23002306
/// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate.
23012307
last_days_feerates: Mutex<VecDeque<(u32, u32)>>,
23022308

2309+
#[cfg(feature = "dnssec")]
2310+
dns_resolver: OMNameResolver,
2311+
#[cfg(feature = "dnssec")]
2312+
pending_dns_onion_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>,
2313+
23032314
entropy_source: ES,
23042315
node_signer: NS,
23052316
signer_provider: SP,
@@ -3110,6 +3121,11 @@ where
31103121
signer_provider,
31113122

31123123
logger,
3124+
3125+
#[cfg(feature = "dnssec")]
3126+
dns_resolver: OMNameResolver::new(current_timestamp, params.best_block.height),
3127+
#[cfg(feature = "dnssec")]
3128+
pending_dns_onion_messages: Mutex::new(Vec::new()),
31133129
}
31143130
}
31153131

@@ -9059,6 +9075,21 @@ where
90599075
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
90609076
payer_note: Option<String>, payment_id: PaymentId, retry_strategy: Retry,
90619077
max_total_routing_fee_msat: Option<u64>
9078+
) -> Result<(), Bolt12SemanticError> {
9079+
self.pay_for_offer_intern(offer, quantity, amount_msats, payer_note, payment_id, None, || {
9080+
let expiration = StaleExpiration::TimerTicks(1);
9081+
self.pending_outbound_payments
9082+
.add_new_awaiting_invoice(
9083+
payment_id, expiration, retry_strategy, max_total_routing_fee_msat
9084+
)
9085+
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)
9086+
})
9087+
}
9088+
9089+
fn pay_for_offer_intern<CPP: FnOnce() -> Result<(), Bolt12SemanticError>>(
9090+
&self, offer: &Offer, quantity: Option<u64>, amount_msats: Option<u64>,
9091+
payer_note: Option<String>, payment_id: PaymentId,
9092+
human_readable_name: Option<HumanReadableName>, create_pending_payment: CPP,
90629093
) -> Result<(), Bolt12SemanticError> {
90639094
let expanded_key = &self.inbound_payment_key;
90649095
let entropy = &*self.entropy_source;
@@ -9082,6 +9113,10 @@ where
90829113
None => builder,
90839114
Some(payer_note) => builder.payer_note(payer_note),
90849115
};
9116+
let builder = match human_readable_name {
9117+
None => builder,
9118+
Some(hrn) => builder.sourced_from_human_readable_name(hrn),
9119+
};
90859120
let invoice_request = builder.build_and_sign()?;
90869121

90879122
let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key);
@@ -9091,12 +9126,7 @@ where
90919126

90929127
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
90939128

9094-
let expiration = StaleExpiration::TimerTicks(1);
9095-
self.pending_outbound_payments
9096-
.add_new_awaiting_invoice(
9097-
payment_id, expiration, retry_strategy, max_total_routing_fee_msat
9098-
)
9099-
.map_err(|_| Bolt12SemanticError::DuplicatePaymentId)?;
9129+
create_pending_payment()?;
91009130

91019131
let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap();
91029132
if !offer.paths().is_empty() {
@@ -9227,6 +9257,77 @@ where
92279257
}
92289258
}
92299259

9260+
/// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS
9261+
/// resolver which resolves names according to bLIP 32 at the `dns_resolver` [`Destination`].
9262+
///
9263+
/// If the wallet supports paying on-chain schemes, you should instead use
9264+
/// [`OMNameResolver::resolve_name`] and [`OMNameResolver::handle_dnssec_proof_for_uri`] (by
9265+
/// implementing [`DNSResolverMessageHandler`]) directly to look up a URI and then delegate to
9266+
/// your normal URI handling.
9267+
///
9268+
/// If `max_total_routing_fee_msat` is not specified, The default from
9269+
/// [`RouteParameters::from_payment_params_and_value`] is applied.
9270+
///
9271+
/// # Payment
9272+
///
9273+
/// The provided `payment_id` is used to ensure that only one invoice is paid for the request
9274+
/// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has
9275+
/// been sent.
9276+
///
9277+
/// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the
9278+
/// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the
9279+
/// payment will fail with an [`Event::InvoiceRequestFailed`].
9280+
///
9281+
/// # Privacy
9282+
///
9283+
/// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`]
9284+
/// to construct a [`BlindedPath`] for the reply path. For further privacy implications, see the
9285+
/// docs of the parameterized [`Router`], which implements [`MessageRouter`].
9286+
///
9287+
/// # Limitations
9288+
///
9289+
/// Requires a direct connection to the given [`Destination`] as well as an introduction node in
9290+
/// [`Offer::paths`] or to [`Offer::signing_pubkey`], if empty. A similar restriction applies to
9291+
/// the responding [`Bolt12Invoice::payment_paths`].
9292+
///
9293+
/// # Errors
9294+
///
9295+
/// Errors if:
9296+
/// - a duplicate `payment_id` is provided given the caveats in the aforementioned link,
9297+
///
9298+
/// [`Bolt12Invoice::payment_paths`]: crate::offers::invoice::Bolt12Invoice::payment_paths
9299+
/// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments
9300+
#[cfg(feature = "dnssec")]
9301+
pub fn pay_for_offer_from_human_readable_name(
9302+
&self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId,
9303+
retry_strategy: Retry, max_total_routing_fee_msat: Option<u64>,
9304+
dns_resolvers: Vec<Destination>,
9305+
) -> Result<(), ()> {
9306+
let mut context = DNSResolverContext {
9307+
nonce: [0; 16],
9308+
};
9309+
context.nonce[..].copy_from_slice(&self.entropy_source.get_secure_random_bytes()[..16]);
9310+
9311+
let onion_message = self.dns_resolver.resolve_name(payment_id, name.clone(), context.clone())?;
9312+
let reply_paths = self.create_blinded_paths(MessageContext::DNSResolver(context))?;
9313+
let expiration = StaleExpiration::TimerTicks(2);
9314+
self.pending_outbound_payments.add_new_awaiting_offer(payment_id, expiration, retry_strategy, max_total_routing_fee_msat, amount_msats, name)?;
9315+
let message_params = dns_resolvers
9316+
.iter()
9317+
.flat_map(|destination| reply_paths.iter().map(move |path| (path, destination)))
9318+
.take(OFFERS_MESSAGE_REQUEST_LIMIT);
9319+
for (reply_path, destination) in message_params {
9320+
self.pending_dns_onion_messages.lock().unwrap().push((
9321+
DNSResolverMessage::DNSSECQuery(onion_message.clone()),
9322+
MessageSendInstructions::WithSpecifiedReplyPath {
9323+
destination: destination.clone(),
9324+
reply_path: reply_path.clone(),
9325+
},
9326+
));
9327+
}
9328+
Ok(())
9329+
}
9330+
92309331
/// Gets a payment secret and payment hash for use in an invoice given to a third party wishing
92319332
/// to pay us.
92329333
///
@@ -9850,6 +9951,10 @@ where
98509951
payment_secrets.retain(|_, inbound_payment| {
98519952
inbound_payment.expiry_time > header.time as u64
98529953
});
9954+
#[cfg(feature = "dnssec")] {
9955+
let timestamp = self.highest_seen_timestamp.load(Ordering::Relaxed) as u32;
9956+
self.dns_resolver.new_best_block(height, timestamp);
9957+
}
98539958
}
98549959

98559960
fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option<BlockHash>)> {
@@ -11004,6 +11109,56 @@ where
1100411109
}
1100511110
}
1100611111

11112+
#[cfg(feature = "dnssec")]
11113+
impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
11114+
DNSResolverMessageHandler for ChannelManager<M, T, ES, NS, SP, F, R, L>
11115+
where
11116+
M::Target: chain::Watch<<SP::Target as SignerProvider>::EcdsaSigner>,
11117+
T::Target: BroadcasterInterface,
11118+
ES::Target: EntropySource,
11119+
NS::Target: NodeSigner,
11120+
SP::Target: SignerProvider,
11121+
F::Target: FeeEstimator,
11122+
R::Target: Router,
11123+
L::Target: Logger,
11124+
{
11125+
fn dnssec_query(
11126+
&self, _message: DNSSECQuery, _responder: Option<Responder>,
11127+
) -> Option<(DNSResolverMessage, ResponseInstruction)> {
11128+
None
11129+
}
11130+
11131+
fn dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) {
11132+
let offer_opt = self.dns_resolver.handle_dnssec_proof_for_offer(message, context);
11133+
if let Some((completed_requests, offer)) = offer_opt {
11134+
for (name, payment_id) in completed_requests {
11135+
if let Ok((amt_msats, _hrn)) = self.pending_outbound_payments.received_offer(payment_id) {
11136+
debug_assert_eq!(name, _hrn);
11137+
let offer_pay_res =
11138+
self.pay_for_offer_intern(&offer, None, Some(amt_msats), None, payment_id, Some(name), || {
11139+
// We already marked the payment as in-flight
11140+
Ok(())
11141+
});
11142+
if offer_pay_res.is_err() {
11143+
// The offer we tried to pay is the canonical current offer for the name we
11144+
// wanted to pay. If we can't pay it, there's no way to recover so fail the
11145+
// payment.
11146+
// Note that the PaymentFailureReason should be ignored for an
11147+
// AwaitingInvoice payment.
11148+
self.pending_outbound_payments.abandon_payment(
11149+
payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events,
11150+
);
11151+
}
11152+
}
11153+
}
11154+
}
11155+
}
11156+
11157+
fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> {
11158+
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
11159+
}
11160+
}
11161+
1100711162
impl<M: Deref, T: Deref, ES: Deref, NS: Deref, SP: Deref, F: Deref, R: Deref, L: Deref>
1100811163
NodeIdLookUp for ChannelManager<M, T, ES, NS, SP, F, R, L>
1100911164
where
@@ -12745,6 +12900,11 @@ where
1274512900

1274612901
logger: args.logger,
1274712902
default_configuration: args.default_config,
12903+
12904+
#[cfg(feature = "dnssec")]
12905+
dns_resolver: OMNameResolver::new(highest_seen_timestamp, best_block_height),
12906+
#[cfg(feature = "dnssec")]
12907+
pending_dns_onion_messages: Mutex::new(Vec::new()),
1274812908
};
1274912909

1275012910
for htlc_source in failed_htlcs.drain(..) {

lightning/src/ln/outbound_payment.rs

+54
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,60 @@ impl OutboundPayments {
14101410
(payment, onion_session_privs)
14111411
}
14121412

1413+
#[cfg(feature = "dnssec")]
1414+
pub(super) fn add_new_awaiting_offer(
1415+
&self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry,
1416+
max_total_routing_fee_msat: Option<u64>, amount_msats: u64,
1417+
resolving_hrn: HumanReadableName,
1418+
) -> Result<(), ()> {
1419+
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
1420+
match pending_outbounds.entry(payment_id) {
1421+
hash_map::Entry::Occupied(_) => Err(()),
1422+
hash_map::Entry::Vacant(entry) => {
1423+
entry.insert(PendingOutboundPayment::AwaitingOffer {
1424+
expiration,
1425+
retry_strategy,
1426+
max_total_routing_fee_msat,
1427+
amount_msats,
1428+
resolving_hrn,
1429+
});
1430+
1431+
Ok(())
1432+
},
1433+
}
1434+
}
1435+
1436+
#[cfg(feature = "dnssec")]
1437+
pub(super) fn received_offer(
1438+
&self, payment_id: PaymentId,
1439+
) -> Result<(u64, HumanReadableName), ()> {
1440+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
1441+
hash_map::Entry::Occupied(entry) => match entry.get() {
1442+
PendingOutboundPayment::AwaitingOffer {
1443+
expiration, retry_strategy, max_total_routing_fee_msat, ..
1444+
} => {
1445+
let mut new_val = PendingOutboundPayment::AwaitingInvoice {
1446+
expiration: *expiration,
1447+
retry_strategy: *retry_strategy,
1448+
max_total_routing_fee_msat: *max_total_routing_fee_msat,
1449+
};
1450+
core::mem::swap(&mut new_val, entry.into_mut());
1451+
1452+
if let PendingOutboundPayment::AwaitingOffer {
1453+
resolving_hrn, amount_msats, ..
1454+
} = new_val {
1455+
Ok((amount_msats, resolving_hrn))
1456+
} else {
1457+
debug_assert!(false, "We can't spontaneously change state");
1458+
Err(())
1459+
}
1460+
},
1461+
_ => Err(()),
1462+
},
1463+
hash_map::Entry::Vacant(_) => Err(()),
1464+
}
1465+
}
1466+
14131467
pub(super) fn add_new_awaiting_invoice(
14141468
&self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry,
14151469
max_total_routing_fee_msat: Option<u64>

0 commit comments

Comments
 (0)