Skip to content

Commit bd237b1

Browse files
committed
Handle DNSSECError messages when attempting DNSSEC resolution
lightning/blips#71 updated the DNSSEC resolution bLIP to include an explicit error message when DNS(SEC) resolution was attempted but failed, allowing for faster fallback to LN-Address (for clients that do) and faster payment failure. Here we add client-side support for the new message, accepting `DNSSECError` messages and marking payments as failed when all our queries have failed. Largely writen by an LLM
1 parent c37b0dc commit bd237b1

2 files changed

Lines changed: 199 additions & 54 deletions

File tree

lightning-dns-resolver/src/lib.rs

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,6 @@ mod test {
273273

274274
#[tokio::test]
275275
async fn resolution_test() {
276-
let secp_ctx = Secp256k1::new();
277-
278276
let (resolver_messenger, resolver_id) = create_resolver();
279277

280278
let resolver_dest = Destination::Node(resolver_id);
@@ -307,25 +305,11 @@ mod test {
307305
payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap();
308306
resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap();
309307

310-
let (msg, context) =
311-
payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap();
312-
let query_context = MessageContext::DNSResolver(context);
313-
let receive_key = payer_keys.get_receive_auth_key();
314-
let reply_path = BlindedMessagePath::one_hop(
315-
payer_id,
316-
receive_key,
317-
query_context,
318-
false,
319-
&*payer_keys,
320-
&secp_ctx,
321-
);
322-
payer.pending_messages.lock().unwrap().push((
323-
DNSResolverMessage::DNSSECQuery(msg),
324-
MessageSendInstructions::WithSpecifiedReplyPath {
325-
destination: resolver_dest,
326-
reply_path,
327-
},
328-
));
308+
let messages = payer
309+
.resolver
310+
.resolve_name(payment_id, name.clone(), vec![resolver_dest], &*payer_keys)
311+
.unwrap();
312+
payer.pending_messages.lock().unwrap().extend(messages);
329313

330314
let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap();
331315
resolver_messenger.get_om().handle_onion_message(payer_id, &query);
@@ -351,8 +335,6 @@ mod test {
351335
async fn failed_query_does_not_leak_pending_counter() {
352336
use std::sync::atomic::Ordering;
353337

354-
let secp_ctx = Secp256k1::new();
355-
356338
// Resolver points at a port that should refuse TCP, so build_txt_proof_async
357339
// returns Err quickly.
358340
let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43, true));
@@ -405,25 +387,11 @@ mod test {
405387
payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap();
406388
resolver_messenger.peer_connected(payer_id, &init_msg, false).unwrap();
407389

408-
let (msg, context) =
409-
payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap();
410-
let query_context = MessageContext::DNSResolver(context);
411-
let receive_key = payer_keys.get_receive_auth_key();
412-
let reply_path = BlindedMessagePath::one_hop(
413-
payer_id,
414-
receive_key,
415-
query_context,
416-
false,
417-
&*payer_keys,
418-
&secp_ctx,
419-
);
420-
payer.pending_messages.lock().unwrap().push((
421-
DNSResolverMessage::DNSSECQuery(msg),
422-
MessageSendInstructions::WithSpecifiedReplyPath {
423-
destination: resolver_dest,
424-
reply_path,
425-
},
426-
));
390+
let messages = payer
391+
.resolver
392+
.resolve_name(payment_id, name.clone(), vec![resolver_dest], &*payer_keys)
393+
.unwrap();
394+
payer.pending_messages.lock().unwrap().extend(messages);
427395

428396
let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap();
429397
resolver_messenger.handle_onion_message(payer_id, &query);

lightning/src/onion_message/dns_resolution.rs

Lines changed: 189 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,16 @@ use core::fmt;
4040
use core::ops::Deref;
4141

4242
use crate::blinded_path::message::DNSResolverContext;
43+
#[cfg(feature = "dnssec")]
44+
use crate::blinded_path::message::MessageContext;
4345
use crate::io;
4446
#[cfg(feature = "dnssec")]
4547
use crate::ln::channelmanager::PaymentId;
4648
use crate::ln::msgs::DecodeError;
4749
#[cfg(feature = "dnssec")]
4850
use crate::offers::offer::Offer;
51+
#[cfg(feature = "dnssec")]
52+
use crate::onion_message::messenger::Destination;
4953
use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction};
5054
use crate::onion_message::packet::OnionMessageContents;
5155
use crate::prelude::*;
@@ -375,6 +379,7 @@ struct PendingResolution {
375379
context: DNSResolverContext,
376380
name: HumanReadableName,
377381
payment_id: PaymentId,
382+
pending_queries: usize,
378383
}
379384

380385
/// A stateful resolver which maps BIP 353 Human Readable Names to URIs and BOLT12 [`Offer`]s.
@@ -462,11 +467,18 @@ impl OMNameResolver {
462467

463468
/// Begins the process of resolving a BIP 353 Human Readable Name.
464469
///
465-
/// Returns a [`DNSSECQuery`] onion message and a [`DNSResolverContext`] which should be sent
466-
/// to a resolver (with the context used to generate the blinded response path) on success.
470+
/// Returns a list of [`DNSSECQuery`] onion messages and the [`MessageSendInstructions`] over
471+
/// which each should be sent - one entry per provided `destination`. The query may be sent to
472+
/// multiple resolvers (i.e. multiple `destinations`) to improve the chance of a timely
473+
/// response; the resolution is only considered failed once a [`DNSSECError`] has been received
474+
/// for every one of them (see [`Self::handle_dnssec_error`]).
475+
///
476+
/// Each [`MessageSendInstructions`] includes a reply path built from the same
477+
/// [`DNSResolverContext`], so that responses can be matched back to this resolution.
467478
pub fn resolve_name<ES: EntropySource + ?Sized>(
468-
&self, payment_id: PaymentId, name: HumanReadableName, entropy_source: &ES,
469-
) -> Result<(DNSSECQuery, DNSResolverContext), ()> {
479+
&self, payment_id: PaymentId, name: HumanReadableName, destinations: Vec<Destination>,
480+
entropy_source: &ES,
481+
) -> Result<Vec<(DNSResolverMessage, MessageSendInstructions)>, ()> {
470482
let dns_name =
471483
Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user(), name.domain()));
472484
debug_assert!(
@@ -477,11 +489,23 @@ impl OMNameResolver {
477489
context.nonce.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]);
478490
if let Ok(dns_name) = dns_name {
479491
let start_height = self.latest_block_height.load(Ordering::Acquire) as u32;
492+
let pending_queries = destinations.len();
493+
let query = DNSResolverMessage::DNSSECQuery(DNSSECQuery(dns_name.clone()));
494+
let messages = destinations
495+
.into_iter()
496+
.map(|destination| {
497+
let instructions = MessageSendInstructions::WithReplyPath {
498+
destination,
499+
context: MessageContext::DNSResolver(context.clone()),
500+
};
501+
(query.clone(), instructions)
502+
})
503+
.collect();
480504
let mut pending_resolves = self.pending_resolves.lock().unwrap();
481-
let context_ret = context.clone();
482-
let resolution = PendingResolution { start_height, context, name, payment_id };
483-
pending_resolves.entry(dns_name.clone()).or_insert_with(Vec::new).push(resolution);
484-
Ok((DNSSECQuery(dns_name), context_ret))
505+
let resolution =
506+
PendingResolution { start_height, context, name, payment_id, pending_queries };
507+
pending_resolves.entry(dns_name).or_insert_with(Vec::new).push(resolution);
508+
Ok(messages)
485509
} else {
486510
Err(())
487511
}
@@ -607,12 +631,86 @@ impl OMNameResolver {
607631
}
608632
None
609633
}
634+
635+
/// Handles a [`DNSSECError`] message, indicating that one of the resolvers we sent a
636+
/// [`DNSSECQuery`] to was unable to provide a [`DNSSECProof`] for the requested name.
637+
///
638+
/// Each error counts as a failure of one of the queries we made for the matching resolution
639+
/// (see the `num_queries` argument to [`Self::resolve_name`]). A resolution is only considered
640+
/// failed once we have received a [`DNSSECError`] for *all* the queries we made for it, as a
641+
/// [`DNSSECProof`] may still arrive from one of the other resolvers we queried. When a
642+
/// resolution does fail, it is removed and its [`HumanReadableName`] and [`PaymentId`] (as
643+
/// passed to [`Self::resolve_name`]) are included in the returned list. The caller should
644+
/// consider those resolutions failed.
645+
///
646+
/// Note that [`DNSSECError::definitely_unresolvable`] is intentionally ignored - a single
647+
/// resolver claiming a name is unresolvable does not preclude another resolver we queried from
648+
/// successfully resolving it, so we always wait for all queries to fail.
649+
///
650+
/// As with [`Self::handle_dnssec_proof_for_uri`], the [`DNSResolverContext`] is checked against
651+
/// the contexts of any pending resolutions for the name to ensure the error was received over a
652+
/// blinded path we created when making the relevant [`DNSSECQuery`].
653+
pub fn handle_dnssec_error(
654+
&self, msg: DNSSECError, context: DNSResolverContext,
655+
) -> Vec<(HumanReadableName, PaymentId)> {
656+
let DNSSECError { name, .. } = msg;
657+
let mut failed_resolutions = Vec::new();
658+
let mut pending_resolves = self.pending_resolves.lock().unwrap();
659+
if let hash_map::Entry::Occupied(mut entry) = pending_resolves.entry(name) {
660+
entry.get_mut().retain_mut(|resolution| {
661+
if resolution.context != context {
662+
// This error was not received over the blinded path we created for this
663+
// resolution (the contexts differ), so it does not pertain to this resolution.
664+
return true;
665+
}
666+
// One of the queries we made for this resolution failed. Only fail (and remove) the
667+
// resolution once all the queries we made for it have failed.
668+
resolution.pending_queries = resolution.pending_queries.saturating_sub(1);
669+
if resolution.pending_queries == 0 {
670+
failed_resolutions.push((resolution.name, resolution.payment_id));
671+
false
672+
} else {
673+
true
674+
}
675+
});
676+
if entry.get().is_empty() {
677+
entry.remove();
678+
}
679+
}
680+
failed_resolutions
681+
}
610682
}
611683

612684
#[cfg(test)]
613685
mod tests {
614686
use super::*;
615687

688+
#[cfg(feature = "dnssec")]
689+
use crate::util::test_utils::pubkey;
690+
691+
#[cfg(feature = "dnssec")]
692+
fn dest(b: u8) -> Destination {
693+
Destination::Node(pubkey(b))
694+
}
695+
696+
/// Extracts the DNS [`Name`] and [`DNSResolverContext`] from the messages returned by
697+
/// [`OMNameResolver::resolve_name`].
698+
#[cfg(feature = "dnssec")]
699+
fn dns_name_and_context(
700+
messages: &[(DNSResolverMessage, MessageSendInstructions)],
701+
) -> (Name, DNSResolverContext) {
702+
match &messages[0] {
703+
(
704+
DNSResolverMessage::DNSSECQuery(DNSSECQuery(name)),
705+
MessageSendInstructions::WithReplyPath {
706+
context: MessageContext::DNSResolver(context),
707+
..
708+
},
709+
) => (name.clone(), context.clone()),
710+
_ => panic!("Unexpected resolve_name output"),
711+
}
712+
}
713+
616714
#[test]
617715
fn test_hrn_display_format() {
618716
let user = "user";
@@ -651,6 +749,21 @@ mod tests {
651749
assert!(HumanReadableName::new("user", &huge_domain).is_err());
652750
}
653751

752+
#[test]
753+
fn test_dnssec_error_roundtrip() {
754+
let name = Name::try_from("test.user._bitcoin-payment.example.com.".to_owned()).unwrap();
755+
for definitely_unresolvable in [false, true] {
756+
let msg = DNSResolverMessage::DNSSECError(DNSSECError {
757+
name: name.clone(),
758+
definitely_unresolvable,
759+
});
760+
let mut buf = Vec::new();
761+
msg.write(&mut buf).unwrap();
762+
let read = DNSResolverMessage::read(&mut &buf[..], DNSSEC_ERROR_TYPE).unwrap();
763+
assert_eq!(msg, read);
764+
}
765+
}
766+
654767
#[test]
655768
#[cfg(feature = "dnssec")]
656769
fn test_expiry() {
@@ -659,22 +772,22 @@ mod tests {
659772
let name = HumanReadableName::new("user", "example.com").unwrap();
660773

661774
// Queue up a resolution
662-
resolver.resolve_name(PaymentId([0; 32]), name.clone(), &keys).unwrap();
775+
resolver.resolve_name(PaymentId([0; 32]), name.clone(), vec![dest(42)], &keys).unwrap();
663776
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
664777
// and check that it expires after two blocks
665778
resolver.new_best_block(44, 42);
666779
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0);
667780

668781
// Queue up another resolution
669-
resolver.resolve_name(PaymentId([1; 32]), name.clone(), &keys).unwrap();
782+
resolver.resolve_name(PaymentId([1; 32]), name.clone(), vec![dest(42)], &keys).unwrap();
670783
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
671784
// it won't expire after one block
672785
resolver.new_best_block(45, 42);
673786
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
674787
assert_eq!(resolver.pending_resolves.lock().unwrap().iter().next().unwrap().1.len(), 1);
675788
// and queue up a second and third resolution of the same name
676-
resolver.resolve_name(PaymentId([2; 32]), name.clone(), &keys).unwrap();
677-
resolver.resolve_name(PaymentId([3; 32]), name.clone(), &keys).unwrap();
789+
resolver.resolve_name(PaymentId([2; 32]), name.clone(), vec![dest(42)], &keys).unwrap();
790+
resolver.resolve_name(PaymentId([3; 32]), name.clone(), vec![dest(42)], &keys).unwrap();
678791
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
679792
assert_eq!(resolver.pending_resolves.lock().unwrap().iter().next().unwrap().1.len(), 3);
680793
// after another block the first will expire, but the second and third won't
@@ -689,4 +802,68 @@ mod tests {
689802
resolver.new_best_block(47, 42);
690803
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0);
691804
}
805+
806+
#[test]
807+
#[cfg(feature = "dnssec")]
808+
fn test_dnssec_error() {
809+
let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0, true);
810+
let resolver = OMNameResolver::new(42, 42);
811+
let name = HumanReadableName::new("user", "example.com").unwrap();
812+
813+
// Resolve a name, sending the query to two resolvers.
814+
let messages = resolver
815+
.resolve_name(PaymentId([0; 32]), name.clone(), vec![dest(1), dest(2)], &keys)
816+
.unwrap();
817+
assert_eq!(messages.len(), 2);
818+
let (dns_name, context) = dns_name_and_context(&messages);
819+
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
820+
821+
// An error whose context doesn't match any pending resolution is ignored entirely (and does
822+
// not count against the resolution's queries), even if it claims the name is unresolvable.
823+
let mut wrong_context = context.clone();
824+
wrong_context.nonce[0] ^= 0x01;
825+
let wrong = DNSSECError { name: dns_name.clone(), definitely_unresolvable: true };
826+
assert!(resolver.handle_dnssec_error(wrong, wrong_context).is_empty());
827+
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
828+
829+
// The first matching error fails one of the two queries but not the resolution itself - even
830+
// though `definitely_unresolvable` is set - as a proof may yet arrive from the other query.
831+
let err = DNSSECError { name: dns_name.clone(), definitely_unresolvable: true };
832+
assert!(resolver.handle_dnssec_error(err, context.clone()).is_empty());
833+
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1);
834+
835+
// The second matching error fails the last query, so the resolution now fails - even though
836+
// this error only indicates a transient failure.
837+
let err = DNSSECError { name: dns_name, definitely_unresolvable: false };
838+
let failed = resolver.handle_dnssec_error(err, context);
839+
assert_eq!(failed, vec![(name, PaymentId([0; 32]))]);
840+
assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0);
841+
}
842+
843+
#[test]
844+
#[cfg(feature = "dnssec")]
845+
fn test_dnssec_error_only_fails_matching_resolution() {
846+
// An error only counts against the resolution whose blinded path (context) it was received
847+
// over; other resolutions for the same name (queued with a different `PaymentId`, and thus a
848+
// different context) are left untouched.
849+
let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0, true);
850+
let resolver = OMNameResolver::new(42, 42);
851+
let name = HumanReadableName::new("user", "example.com").unwrap();
852+
853+
let messages =
854+
resolver.resolve_name(PaymentId([0; 32]), name.clone(), vec![dest(1)], &keys).unwrap();
855+
let (dns_name, context_a) = dns_name_and_context(&messages);
856+
resolver.resolve_name(PaymentId([1; 32]), name.clone(), vec![dest(2)], &keys).unwrap();
857+
assert_eq!(resolver.pending_resolves.lock().unwrap().iter().next().unwrap().1.len(), 2);
858+
859+
// A single error over `context_a` fails only payment 0's (single-query) resolution.
860+
let err = DNSSECError { name: dns_name, definitely_unresolvable: false };
861+
let failed = resolver.handle_dnssec_error(err, context_a);
862+
assert_eq!(failed, vec![(name, PaymentId([0; 32]))]);
863+
864+
// Payment 1's resolution is still pending.
865+
let pending = resolver.pending_resolves.lock().unwrap();
866+
assert_eq!(pending.iter().next().unwrap().1.len(), 1);
867+
assert_eq!(pending.iter().next().unwrap().1[0].payment_id, PaymentId([1; 32]));
868+
}
692869
}

0 commit comments

Comments
 (0)