-
Notifications
You must be signed in to change notification settings - Fork 116
Add support for resolving BIP 353 Human-Readable Names #630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add support for resolving BIP 353 Human-Readable Names #630
Conversation
|
👋 Thanks for assigning @tnull as a reviewer! |
|
This PR builds upon #607. It is currently opened in draft because the end-to-end test I added runs indefinitely without resolving or timing out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First look at the additional changes here, had to cut the review round short though.
Cargo.toml
Outdated
| vss-client = "0.3" | ||
| prost = { version = "0.11.6", default-features = false} | ||
| bitcoin-payment-instructions = { version = "0.5" } | ||
| lightning-dns-resolver = "0.2.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add this above to the group(s) of lightning-* imports (also the commented-out ones) and follow the scheme there.
bindings/ldk_node.udl
Outdated
| u64 probing_liquidity_limit_multiplier; | ||
| AnchorChannelsConfig? anchor_channels_config; | ||
| SendingParameters? sending_parameters; | ||
| boolean is_hrn_resolver; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think over at the first PR we discussed to introduce a dedicated config for HRN-related things, also to specify default resolvers, for example? Probably this should be part of such a config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we did - I'll re-introduce the dedicated config for HRN-related things.
src/builder.rs
Outdated
| let hrn_resolver = Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(&network_graph))); | ||
| let resolver = if config.is_hrn_resolver { | ||
| Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( | ||
| "8.8.8.8:53".parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably want to make the DNS server used configurable via the config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that makes sense - will do.
src/payment/unified.rs
Outdated
| Error::HrnResolverNotConfigured | ||
| })?; | ||
|
|
||
| println!("Parsing instructions..."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, please remove any printlns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah - will do, just left this here because I am still debugging the issue with the end-to-end test not resolving HRNs, erroring out or timing out.
| peer_manager_clone.process_events(); | ||
| })); | ||
| let hrn_resolver = match resolver { | ||
| Resolver::DNS(_) => None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, so if we're resolving for others, we won't be able to send to HRNs ourselves?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my understanding, it seems this is the case. The onion messenger can take only one type of dns_resolver at a time - either the one sending out the query or the one handling the query and responding with the proof. If our node is configured to resolve for others, we'd have to pass the appropriate resolver to the onion messenger, preventing us from sending to HRNs ourselves.
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was able to reproduce the behavior you were running into (see below).
tests/integration_tests_rust.rs
Outdated
| // Sleep one more sec to make sure the node announcement propagates. | ||
| std::thread::sleep(std::time::Duration::from_secs(1)); | ||
|
|
||
| let hrn = "[email protected]"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was now able to reproduce the infinite loop you were seeing, and it seems that this HRN doesn't have a valid DNSSEC proof attached (i.e., we hit the else clause of this case in the DNS resolver: https://github.com/lightningdevkit/rust-lightning/blob/674861d06fb333222cff9c2caaeb3783728672a1/lightning-dns-resolver/src/lib.rs#L134). If I plug in [email protected] the resolution actually succeeds, however, it of course doesn't have any suitable regtest payment instructions attached. I'm not fully sure if there are any plans for local-testing end-to-end lightning-dns-resolver (cc @TheBlueMatt ?), but we may indeed need to spin up our own DNS server (instead of using 8.8.8.8) and publish a correct DNSSEC proof to make the test fully work end-to-end in regtest.
Also opened an issue here: rust-bitcoin/bitcoin-payment-instructions#11
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do have end-to-end tests in lightning-dns-resolver tho they are a bit awkward. The hook ChanelManager::testing_dnssec_proof_offer_resolution_override but then do a totally normal resolution (which must pass DNSSEC validation) and then swap out the returned offer, see https://github.com/lightningdevkit/rust-lightning/blob/main/lightning-dns-resolver/src/lib.rs#L387
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @tnull and @TheBlueMatt - I'll look into ChannelManager::testing_dnssec_proof_offer_resolution_override and also explore spinning up our own DNS server to publish a correct DNSSEC proof.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency, probably good to just use [email protected] as the test string, let it do DNS resolution, then have a similar override hook in ldk-node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do have end-to-end tests in
lightning-dns-resolvertho they are a bit awkward. The hookChanelManager::testing_dnssec_proof_offer_resolution_overridebut then do a totally normal resolution (which must pass DNSSEC validation) and then swap out the returned offer, see https://github.com/lightningdevkit/rust-lightning/blob/main/lightning-dns-resolver/src/lib.rs#L387
It seems that even with a similar testing hook (as added here: d5c46a3), we're hitting a ParseError::WrongNetwork error because the resolved payment instruction isn't for regtest. This is happening at this line: https://github.com/rust-bitcoin/bitcoin-payment-instructions/blob/74003b498b9bf90497457b1aab03bac48576892c/src/lib.rs#L520C3-L520C40 when testing without a regtest instruction on the DNS server.
This confirms the issue is around network mismatch, as the [email protected] instruction is for mainnet but not regtest.
The two main options seem to be:
-
Add
regtestinstructions to the DNS server where instructions for[email protected]currently reside. -
Spin up our own DNS server specifically for
regtesttesting purposes.
Given the need for a stable and predictable environment for end-to-end testing, spinning up a dedicated DNS server seems like the more robust solution, as it gives us full control over the DNSSEC proofs and the regtest payment instructions.
However, if there are existing plans or infrastructure to easily add regtest instructions to the current server, that would be simpler.
What do you think is the preferred path forward? (cc @TheBlueMatt)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to resolve this by swapping the resolved offer with a test offer as previously suggested and also switching the network to bitcoin mainnet when parsing a hrn in a test, in order to match the network of the payment instruction mapped to [email protected] and avoid hitting the WrongNetwork error.
src/payment/unified.rs
Outdated
| println!("Parsing instructions..."); | ||
|
|
||
| let instructions = | ||
| PaymentInstructions::parse(uri_str, self.config.network, resolver.as_ref(), false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We def. need to wrap this in a tokio::timeout to abandon the future if it never returns.
0517dbc to
0b0430c
Compare
4b5a6cd to
8c4ca25
Compare
4f1beac to
49f4013
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool! Lets move all the testing-related changes behind a cfg(hrn_tests) flag.
Now that we have a working end-to-end test I think we'll wait until 0.7 ships (likely ~next week) before moving forward with #666 and then this PR after it lands. This way, all the necessary changes would land for v0.8, including the payment metadata store which we'll probably still want to use to store the HRN post-resolution.
This unfortunately also needs a rebase now, sorry!
| } | ||
|
|
||
| /// Testing utils for DNSSEC proof resolution of offers associated with the given HRN. | ||
| pub mod dnssec_testing_utils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's introduce a new cfg(hrn_tests) gate and move all the testing-related changes behind it, to make sure we don't ever run it in production code.
Camillarhi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this! I left some comments.
| })?; | ||
| } | ||
|
|
||
| let resolver = if let Some(hrn_config) = &config.hrn_config { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the HRN resolver config, do we need the is_hrn_resolver boolean?
Right now we check both if the config exists andthe boolean value, but we end up with the same HRN resolver when the config exists with is_hrn_resolver = false or when there's no config at all.
Seems like we could simplify this by just using the presence of hrn_config to decide which resolver to use, and drop the boolean field entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! I think is_hrn_resolver is actually the core of this PR as it should be set explicitly by the user - a node might set hrn_config but choose not to resolve for others but just to enable sending (#666). Instead of dropping is_hrn_resolver, I think we should instead have a new Resolver variant for IgnoringMessageHandler which should be returned whenever no hrn_config is provided - I think that's more idiomatic because if hrn_config isn't set at all, we shouldn't be doing any HRN-related operations anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point about is_hrn_resolver being explicit, but right now, when is_hrn_resolver is false, we return the same default resolver as when the hrn_config is None. Unless there are plans to handle the is_hrn_resolver = false case differently in the future, having the config present seems sufficient to determine which resolver to use.
Instead of dropping
is_hrn_resolver, I think we should instead have a newResolvervariant for
IgnoringMessageHandlerwhich should be returned whenever nohrn_configis provided - I think that's more
idiomatic because ifhrn_configisn't set at all, we shouldn't be doing any HRN-related operations anyway.
Just to clarify, do you mean that if hrn_config is not set at all, the resolver would be None?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point about
is_hrn_resolverbeing explicit, but right now, whenis_hrn_resolveris false, we return the same default resolver as when thehrn_configis None. Unless there are plans to handle theis_hrn_resolver = falsecase differently in the future, having the config present seems sufficient to determine which resolver to use.
Here is what I am talking about -->
enum Resolver {
HRN(Arc<HRNResolver>),
DNS(Arc<DomainResolver>),
Ignore(Arc<IgnoringMessageHandler>),
}
let resolver = if let Some(hrn_config) = &config.hrn_config {
if hrn_config.is_hrn_resolver {
let dns_addr = hrn_config.dns_server_address.as_str();
Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs(
dns_addr.parse().map_err(|_| BuildError::DNSResolverSetupFailed)?,
)))
} else {
Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone(
&network_graph,
))))
}
} else {
// hrn_config is None, default to the IgnoringMessaageHandler.
Resolver::Ignore(Arc::new(IgnoringMessageHandler { }))
};
Just to clarify, do you mean that if
hrn_configis not set at all, the resolver would be None?
if hrn_config is not set at all, a no-op Resolver variant would be returned as highlighted above.
Already implemented this locally, will push updates asap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note that in this implementation, enabling DNS resolution for other nodes would prevent us from being able to send to HRNs ourselves (#630 (comment))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point about
is_hrn_resolverbeing explicit, but right now, whenis_hrn_resolveris false, we return the same default resolver as when thehrn_configis None. Unless there are plans to handle theis_hrn_resolver = falsecase differently in the future, having the config present seems sufficient to determine which resolver to use.Here is what I am talking about -->
enum Resolver { HRN(Arc<HRNResolver>), DNS(Arc<DomainResolver>), Ignore(Arc<IgnoringMessageHandler>), }let resolver = if let Some(hrn_config) = &config.hrn_config { if hrn_config.is_hrn_resolver { let dns_addr = hrn_config.dns_server_address.as_str(); Resolver::DNS(Arc::new(OMDomainResolver::ignoring_incoming_proofs( dns_addr.parse().map_err(|_| BuildError::DNSResolverSetupFailed)?, ))) } else { Resolver::HRN(Arc::new(LDKOnionMessageDNSSECHrnResolver::new(Arc::clone( &network_graph, )))) } } else { // hrn_config is None, default to the IgnoringMessaageHandler. Resolver::Ignore(Arc::new(IgnoringMessageHandler { })) };Just to clarify, do you mean that if
hrn_configis not set at all, the resolver would be None?if
hrn_configis not set at all, a no-opResolvervariant would be returned as highlighted above.Already implemented this locally, will push updates asap.
Yes this looks like a valid approach. false and None returns different values. Thanks
src/payment/bolt12.rs
Outdated
| let mut current_offer = offer.clone(); | ||
|
|
||
| if let Some(hrn_ref) = hrn.as_ref() { | ||
| current_offer = match crate::dnssec_testing_utils::get_testing_offer_override(Some( | ||
| hrn_ref.clone(), | ||
| )) { | ||
| Some(offer) => { | ||
| log_info!(self.logger, "Using test-specific Offer override."); | ||
| offer | ||
| }, | ||
| _ => offer.clone(), | ||
| }; | ||
| } | ||
|
|
||
| let offer = maybe_deref(¤t_offer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be simplified a bit. The current_offer mutable variable and cloning feels unnecessary.
We could do something like:
let final_offer = if let Some(hrn_ref) = &hrn {
get_testing_offer_override(Some(hrn_ref.clone()))
.map(|override_offer| {
log_info!(self.logger, "Using test-specific Offer override.");
override_offer
})
.unwrap_or_else(|| offer.clone())
} else {
offer.clone()
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I'll replace that with -->
let offer = if let Some(hrn_ref) = &hrn {
crate::dnssec_testing_utils::get_testing_offer_override(Some(hrn_ref.clone()))
.map(|override_offer| {
log_info!(self.logger, "Using test-specific Offer override.");
override_offer
})
.unwrap_or_else(|| offer.clone())
} else {
offer.clone()
};
let offer = maybe_deref(&offer);
tnull
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a rebase now that #666 landed.
49f4013 to
cdb1290
Compare
Introduce new configuration parameters to manage Human-Readable Name (HRN) resolution and DNSSEC validation behavior. These settings allow users to define custom resolution preferences for BOLT12 offer lookups. Moving these parameters into the central configuration struct ensures that node behavior is customizable at runtime and consistent across different network environments. This abstraction is necessary to support diverse DNSSEC requirements without hard-coding resolution logic.
Inject specialized resolution capabilities into OnionMessenger to support outbound payments and third-party resolution services. This change refines the previous resolution logic by allowing the node to act as a robust BIP 353 participant. If configured as a service provider, the node utilizes a Domain Resolver to handle requests for other participants. Otherwise, it uses an HRN Resolver specifically for initiating its own outbound payments. Providing these as optional parameters in the Node constructor ensures the logic matches the node's designated role in the ecosystem.
d5bcd8a to
73dc622
Compare
Introduce a comprehensive test case to verify the full lifecycle of a payment initiated via a Human Readable Name (HRN). This test ensures that the integration between HRN parsing, BIP 353 resolution, and BOLT12 offer execution is functioning correctly within the node. By asserting that an encoded URI can be successfully resolved to a valid offer and subsequently paid, we validate the reliability of the resolution pipeline and ensure that recent architectural changes to the OnionMessenger and Node configuration work in unison.
Update the GitHub Actions workflow to include coverage for the new hrn_tests feature across multiple build configurations. This ensures that the DNSSEC override logic is validated in both standard Rust and UniFFI-enabled environments. Including these flags in CI prevents regressions where testing-specific code might break the primary build or fail to compile due to type mismatches between the LDK and FFI wrappers. Testing both feature combinations (with and without UniFFI) guarantees that the abstraction for HumanReadableName remains consistent across all supported platforms and integration layers.
73dc622 to
9578e91
Compare
| let resolver = self.hrn_resolver.as_ref().clone().ok_or_else(|| { | ||
| log_error!(self.logger, "No HRN resolver configured. Cannot resolve HRNs."); | ||
| Error::HrnResolverNotConfigured | ||
| })?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this Error should be returned only when a HRN is passed in as the uri_str but no hrn_resolver was provided.
This PR makes it possible to configure LDK-Node as a HRN resolver for other nodes.
Changes
boolflag namedis_hrn_resolvertoConfig.is_hrn_resolver, a new node either has the ability to send payments to HRNs or do DNS resolution for other nodes.setup_two_nodesin the integration test suite to allow configuring a node to act as a HRN resolver for other nodes.This PR fixes #436