mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-01-18 13:24:36 +01:00
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:
commit
f152689d0a
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
@ -15,6 +15,7 @@ members = [
|
||||
"lightning-custom-message",
|
||||
"lightning-transaction-sync",
|
||||
"lightning-macros",
|
||||
"lightning-dns-resolver",
|
||||
"possiblyrandom",
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
19
lightning-dns-resolver/Cargo.toml
Normal file
19
lightning-dns-resolver/Cargo.toml
Normal 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"] }
|
462
lightning-dns-resolver/src/lib.rs
Normal file
462
lightning-dns-resolver/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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 {},
|
||||
|
@ -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);
|
||||
|
@ -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)]
|
||||
|
@ -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()))),
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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(());
|
||||
|
@ -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
|
||||
>;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user