Skip to content

Commit 10391b7

Browse files
committed
Add methods to fetch an OfferBuilder for "phantom" node configs
In the BOLT 11 world, we have specific support for what we call "phantom nodes" - creating invoices which can be paid to any one of a number of nodes by adding route-hints which represent nodes that do not exist. In BOLT 12, blinded paths make a similar feature much simpler - we can simply add blinded paths which terminate at different nodes. The blinding means that the sender is none the wiser. Here we add logic to fetch an `OfferBuilder` which can generate an offer payable to any one of a set of nodes. We retain the "phantom" terminology even though there are no longer any "phantom" nodes. Note that the current logic only supports the `invoice_request` message going to any of the participating nodes, it then replies with a `Bolt12Invoice` which can only be paid to the responding node. Future work may relax this restriction.
1 parent c10a0af commit 10391b7

File tree

6 files changed

+278
-19
lines changed

6 files changed

+278
-19
lines changed

ext-functional-test-demo/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod tests {
1717
impl TestSignerFactory for BrokenSignerFactory {
1818
fn make_signer(
1919
&self, _seed: &[u8; 32], _now: Duration, _v2_remote_key_derivation: bool,
20+
_phantom_seed: Option<&[u8; 32]>,
2021
) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
2122
panic!()
2223
}

lightning/src/ln/channelmanager.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13402,6 +13402,47 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => {
1340213402

1340313403
Ok(builder.into())
1340413404
}
13405+
13406+
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by any
13407+
/// [`ChannelManager`] (or [`OffersMessageFlow`]) using the same [`ExpandedKey`] (as returned
13408+
/// from [`NodeSigner::get_expanded_key`]). This allows any nodes participating in a BOLT 11
13409+
/// "phantom node" cluster to also receive BOLT 12 payments.
13410+
///
13411+
/// Note that, unlike with BOLT 11 invoices, BOLT 12 "phantom" offers do not in fact have any
13412+
/// "phantom node" appended to receiving paths. Instead, multiple blinded paths are simply
13413+
/// included which terminate at different final nodes.
13414+
///
13415+
/// `other_nodes_channels` must be set to a list of each participating node's `node_id` (from
13416+
/// [`NodeSigner::get_node_id`] with a [`Recipient::Node`]) and its channels.
13417+
///
13418+
/// `path_count_limit` is used to limit the number of blinded paths included in the resulting
13419+
/// [`Offer`]. Note that if this is less than the number of participating nodes (i.e.
13420+
/// `other_nodes_channels.len() + 1`) not all nodes will participate in receiving funds.
13421+
/// Because the parameterized [`MessageRouter`] will only get a chance to limit the number of
13422+
/// paths *per-node*, it is important to set this for offers that will be included in a QR
13423+
/// code.
13424+
///
13425+
/// See [`Self::create_offer_builder`] for more details on the blinded path construction.
13426+
///
13427+
/// [`ExpandedKey`]: inbound_payment::ExpandedKey
13428+
pub fn create_phantom_offer_builder(
13429+
&$self, other_nodes_channels: Vec<(PublicKey, Vec<ChannelDetails>)>,
13430+
path_count_limit: usize,
13431+
) -> Result<$builder, Bolt12SemanticError> {
13432+
let mut peers = Vec::with_capacity(other_nodes_channels.len() + 1);
13433+
if !other_nodes_channels.iter().any(|(node_id, _)| *node_id == $self.get_our_node_id()) {
13434+
peers.push(($self.get_our_node_id(), $self.get_peers_for_blinded_path()));
13435+
}
13436+
for (node_id, peer_chans) in other_nodes_channels {
13437+
peers.push((node_id, Self::channel_details_to_forward_nodes(peer_chans)));
13438+
}
13439+
13440+
let builder = $self.flow.create_phantom_offer_builder(
13441+
&$self.entropy_source, peers, path_count_limit
13442+
)?;
13443+
13444+
Ok(builder.into())
13445+
}
1340513446
} }
1340613447

1340713448
macro_rules! create_refund_builder { ($self: ident, $builder: ty) => {
@@ -14018,6 +14059,41 @@ impl<
1401814059
now
1401914060
}
1402014061

14062+
/// Converts a list of channels to a list of peers which may be suitable to receive onion
14063+
/// messages through.
14064+
fn channel_details_to_forward_nodes(
14065+
mut channel_list: Vec<ChannelDetails>,
14066+
) -> Vec<MessageForwardNode> {
14067+
channel_list.sort_unstable_by_key(|chan| chan.counterparty.node_id);
14068+
let mut res = Vec::new();
14069+
// TODO: When MSRV reaches 1.77 use chunk_by
14070+
let mut start = 0;
14071+
while start < channel_list.len() {
14072+
let counterparty_node_id = channel_list[start].counterparty.node_id;
14073+
let end = channel_list[start..]
14074+
.iter()
14075+
.position(|chan| chan.counterparty.node_id != counterparty_node_id)
14076+
.map(|pos| start + pos)
14077+
.unwrap_or(channel_list.len());
14078+
14079+
let peer_chans = &channel_list[start..end];
14080+
if peer_chans.iter().any(|chan| chan.is_usable)
14081+
&& peer_chans.iter().any(|c| c.counterparty.features.supports_onion_messages())
14082+
{
14083+
res.push(MessageForwardNode {
14084+
node_id: peer_chans[0].counterparty.node_id,
14085+
short_channel_id: peer_chans
14086+
.iter()
14087+
.filter(|chan| chan.is_usable)
14088+
.min_by_key(|chan| chan.short_channel_id)
14089+
.and_then(|chan| chan.get_inbound_payment_scid()),
14090+
})
14091+
}
14092+
start = end;
14093+
}
14094+
res
14095+
}
14096+
1402114097
fn get_peers_for_blinded_path(&self) -> Vec<MessageForwardNode> {
1402214098
let per_peer_state = self.per_peer_state.read().unwrap();
1402314099
per_peer_state

lightning/src/ln/functional_test_utils.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4405,21 +4405,41 @@ pub fn create_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> {
44054405

44064406
pub fn create_chanmon_cfgs_with_legacy_keys(
44074407
node_count: usize, predefined_keys_ids: Option<Vec<[u8; 32]>>,
4408+
) -> Vec<TestChanMonCfg> {
4409+
create_chanmon_cfgs_internal(node_count, predefined_keys_ids, false)
4410+
}
4411+
4412+
pub fn create_phantom_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> {
4413+
create_chanmon_cfgs_internal(node_count, None, true)
4414+
}
4415+
4416+
pub fn create_chanmon_cfgs_internal(
4417+
node_count: usize, predefined_keys_ids: Option<Vec<[u8; 32]>>, phantom: bool,
44084418
) -> Vec<TestChanMonCfg> {
44094419
let mut chan_mon_cfgs = Vec::new();
4420+
let phantom_seed = if phantom { Some(&[42; 32]) } else { None };
44104421
for i in 0..node_count {
44114422
let tx_broadcaster = test_utils::TestBroadcaster::new(Network::Testnet);
44124423
let fee_estimator = test_utils::TestFeeEstimator::new(253);
44134424
let chain_source = test_utils::TestChainSource::new(Network::Testnet);
44144425
let logger = test_utils::TestLogger::with_id(format!("node {}", i));
44154426
let persister = test_utils::TestPersister::new();
4416-
let seed = [i as u8; 32];
4417-
let keys_manager = if predefined_keys_ids.is_some() {
4427+
let mut seed = [i as u8; 32];
4428+
if phantom {
4429+
// We would ideally randomize keys on every test run, but some tests fail in that case.
4430+
// Instead, we only randomize in the phantom case.
4431+
use core::hash::{BuildHasher, Hasher};
4432+
// Get a random value using the only std API to do so - the DefaultHasher
4433+
let rand_val = std::collections::hash_map::RandomState::new().build_hasher().finish();
4434+
seed[..8].copy_from_slice(&rand_val.to_ne_bytes());
4435+
}
4436+
let keys_manager = test_utils::TestKeysInterface::with_settings(
4437+
&seed,
4438+
Network::Testnet,
44184439
// Use legacy (V1) remote_key derivation for tests using legacy key sets.
4419-
test_utils::TestKeysInterface::with_v1_remote_key_derivation(&seed, Network::Testnet)
4420-
} else {
4421-
test_utils::TestKeysInterface::new(&seed, Network::Testnet)
4422-
};
4440+
predefined_keys_ids.is_some(),
4441+
phantom_seed,
4442+
);
44234443
let scorer = RwLock::new(test_utils::TestScorer::new());
44244444

44254445
// Set predefined keys_id if provided

lightning/src/ln/offers_tests.rs

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,21 @@ const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 *
7575
use crate::prelude::*;
7676

7777
macro_rules! expect_recent_payment {
78-
($node: expr, $payment_state: path, $payment_id: expr) => {
79-
match $node.node.list_recent_payments().first() {
80-
Some(&$payment_state { payment_id: actual_payment_id, .. }) => {
81-
assert_eq!($payment_id, actual_payment_id);
82-
},
83-
Some(_) => panic!("Unexpected recent payment state"),
84-
None => panic!("No recent payments"),
78+
($node: expr, $payment_state: path, $payment_id: expr) => {{
79+
let mut found_payment = false;
80+
for payment in $node.node.list_recent_payments().iter() {
81+
match payment {
82+
$payment_state { payment_id: actual_payment_id, .. } => {
83+
if $payment_id == *actual_payment_id {
84+
found_payment = true;
85+
break;
86+
}
87+
},
88+
_ => {},
89+
}
8590
}
86-
}
91+
assert!(found_payment);
92+
}}
8793
}
8894

8995
fn connect_peers<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>) {
@@ -2572,3 +2578,92 @@ fn no_double_pay_with_stale_channelmanager() {
25722578
// generated in response to the duplicate invoice.
25732579
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
25742580
}
2581+
2582+
#[test]
2583+
fn creates_and_pays_for_phantom_offer() {
2584+
// Tests that we can pay a "phantom offer" to any participating node.
2585+
let mut chanmon_cfgs = create_chanmon_cfgs(1);
2586+
chanmon_cfgs.append(&mut create_phantom_chanmon_cfgs(2));
2587+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
2588+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
2589+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
2590+
2591+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
2592+
create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 10_000_000, 1_000_000_000);
2593+
2594+
let node_a_id = nodes[0].node.get_our_node_id();
2595+
let node_b_id = nodes[1].node.get_our_node_id();
2596+
let node_c_id = nodes[2].node.get_our_node_id();
2597+
2598+
let offer = nodes[1].node
2599+
.create_phantom_offer_builder(vec![(node_c_id, nodes[2].node.list_channels())], 2)
2600+
.unwrap()
2601+
.amount_msats(10_000_000)
2602+
.build().unwrap();
2603+
2604+
// The offer should be resolvable by either of node B or C but signed by a derived key
2605+
assert!(offer.issuer_signing_pubkey().is_some());
2606+
assert_ne!(offer.issuer_signing_pubkey(), Some(node_b_id));
2607+
assert_ne!(offer.issuer_signing_pubkey(), Some(node_c_id));
2608+
assert_eq!(offer.paths().len(), 2);
2609+
let mut b_path_count = 0;
2610+
let mut c_path_count = 0;
2611+
for path in offer.paths() {
2612+
if check_compact_path_introduction_node(&path, &nodes[0], node_b_id) {
2613+
b_path_count += 1;
2614+
}
2615+
if check_compact_path_introduction_node(&path, &nodes[0], node_c_id) {
2616+
c_path_count += 1;
2617+
}
2618+
}
2619+
assert_eq!(b_path_count, 1);
2620+
assert_eq!(c_path_count, 1);
2621+
2622+
// Pay twice, first via node B (the node that actually built the offer) then pay via node C
2623+
// (which won't have seen the offer until it receives the invoice_request).
2624+
for (payment_id, recipient) in [([1; 32], &nodes[1]), ([2; 32], &nodes[2])] {
2625+
let payment_id = PaymentId(payment_id);
2626+
nodes[0].node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap();
2627+
expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id);
2628+
2629+
let recipient_id = recipient.node.get_our_node_id();
2630+
let non_recipient_id = if node_b_id == recipient_id {
2631+
node_c_id
2632+
} else {
2633+
node_b_id
2634+
};
2635+
2636+
let onion_message =
2637+
nodes[0].onion_messenger.next_onion_message_for_peer(recipient_id).unwrap();
2638+
let _discard =
2639+
nodes[0].onion_messenger.next_onion_message_for_peer(non_recipient_id).unwrap();
2640+
recipient.onion_messenger.handle_onion_message(node_a_id, &onion_message);
2641+
2642+
let (invoice_request, _) = extract_invoice_request(&recipient, &onion_message);
2643+
let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
2644+
offer_id: offer.id(),
2645+
invoice_request: InvoiceRequestFields {
2646+
payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
2647+
quantity: None,
2648+
payer_note_truncated: None,
2649+
human_readable_name: None,
2650+
},
2651+
});
2652+
2653+
let onion_message =
2654+
recipient.onion_messenger.next_onion_message_for_peer(node_a_id).unwrap();
2655+
nodes[0].onion_messenger.handle_onion_message(recipient_id, &onion_message);
2656+
2657+
let (invoice, _) = extract_invoice(&nodes[0], &onion_message);
2658+
assert_eq!(invoice.amount_msats(), 10_000_000);
2659+
2660+
route_bolt12_payment(&nodes[0], &[recipient], &invoice);
2661+
expect_recent_payment!(&nodes[0], RecentPaymentDetails::Pending, payment_id);
2662+
2663+
claim_bolt12_payment(&nodes[0], &[recipient], payment_context, &invoice);
2664+
expect_recent_payment!(&nodes[0], RecentPaymentDetails::Fulfilled, payment_id);
2665+
2666+
assert!(nodes[0].onion_messenger.next_onion_message_for_peer(node_b_id).is_none());
2667+
assert!(nodes[0].onion_messenger.next_onion_message_for_peer(node_c_id).is_none());
2668+
}
2669+
}

lightning/src/offers/flow.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,39 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
286286
self.create_blinded_paths(peers, context)
287287
}
288288

289+
fn blinded_paths_for_phantom_offer(
290+
&self, per_node_peers: Vec<(PublicKey, Vec<MessageForwardNode>)>, path_count_limit: usize,
291+
context: MessageContext,
292+
) -> Result<Vec<BlindedMessagePath>, ()> {
293+
let receive_key = ReceiveAuthKey(self.inbound_payment_key.phantom_node_blinded_path_key);
294+
let secp_ctx = &self.secp_ctx;
295+
296+
let mut per_node_paths: Vec<_> = per_node_peers
297+
.into_iter()
298+
.filter_map(|(recipient, peers)| {
299+
self.message_router
300+
.create_blinded_paths(recipient, receive_key, context.clone(), peers, secp_ctx)
301+
.ok()
302+
})
303+
.collect();
304+
305+
let mut res = Vec::new();
306+
while res.len() < path_count_limit && !per_node_paths.is_empty() {
307+
for node_paths in per_node_paths.iter_mut() {
308+
if let Some(path) = node_paths.pop() {
309+
res.push(path);
310+
}
311+
}
312+
per_node_paths.retain(|node_paths| !node_paths.is_empty());
313+
}
314+
315+
if res.is_empty() {
316+
Err(())
317+
} else {
318+
Ok(res)
319+
}
320+
}
321+
289322
/// Creates a collection of blinded paths by delegating to
290323
/// [`MessageRouter::create_blinded_paths`].
291324
///
@@ -559,8 +592,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
559592

560593
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the
561594
/// [`OffersMessageFlow`], and any corresponding [`InvoiceRequest`] can be verified using
562-
/// [`Self::verify_invoice_request`]. The offer will expire at `absolute_expiry` if `Some`,
563-
/// or will not expire if `None`.
595+
/// [`Self::verify_invoice_request`].
564596
///
565597
/// # Privacy
566598
///
@@ -634,6 +666,25 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
634666
})
635667
}
636668

669+
/// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by any
670+
/// [`OffersMessageFlow`] using the same [`ExpandedKey`] (provided in the constructor as
671+
/// `inbound_payment_key`), and any corresponding [`InvoiceRequest`] can be verified using
672+
/// [`Self::verify_invoice_request`].
673+
///
674+
/// See [`Self::create_offer_builder`] for more details on privacy and limitations.
675+
///
676+
/// [`ExpandedKey`]: inbound_payment::ExpandedKey
677+
pub fn create_phantom_offer_builder<ES: EntropySource>(
678+
&self, entropy_source: ES, per_node_peers: Vec<(PublicKey, Vec<MessageForwardNode>)>,
679+
path_count_limit: usize,
680+
) -> Result<OfferBuilder<'_, DerivedMetadata, secp256k1::All>, Bolt12SemanticError> {
681+
self.create_offer_builder_intern(entropy_source, |_, context, _| {
682+
self.blinded_paths_for_phantom_offer(per_node_peers, path_count_limit, context)
683+
.map_err(|_| Bolt12SemanticError::MissingPaths)
684+
})
685+
.map(|(builder, _)| builder)
686+
}
687+
637688
fn create_refund_builder_intern<ES: EntropySource, PF, I>(
638689
&self, entropy_source: ES, make_paths: PF, amount_msats: u64, absolute_expiry: Duration,
639690
payment_id: PaymentId,

lightning/src/util/test_utils.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,7 @@ pub trait TestSignerFactory: Send + Sync {
19541954
/// Make a dynamic signer
19551955
fn make_signer(
19561956
&self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool,
1957+
phantom_seed: Option<&[u8; 32]>,
19571958
) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>>;
19581959
}
19591960

@@ -1963,12 +1964,13 @@ struct DefaultSignerFactory();
19631964
impl TestSignerFactory for DefaultSignerFactory {
19641965
fn make_signer(
19651966
&self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool,
1967+
phantom_seed: Option<&[u8; 32]>,
19661968
) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> {
19671969
let phantom = sign::PhantomKeysManager::new(
19681970
seed,
19691971
now.as_secs(),
19701972
now.subsec_nanos(),
1971-
seed,
1973+
if let Some(provided_seed) = phantom_seed { provided_seed } else { seed },
19721974
v2_remote_key_derivation,
19731975
);
19741976
let dphantom = DynPhantomKeysInterface::new(phantom);
@@ -2000,7 +2002,7 @@ impl TestKeysInterface {
20002002
let factory = DefaultSignerFactory();
20012003

20022004
let now = Duration::from_secs(genesis_block(network).header.time as u64);
2003-
let backing = factory.make_signer(seed, now, true);
2005+
let backing = factory.make_signer(seed, now, true, None);
20042006
Self::build(backing)
20052007
}
20062008

@@ -2012,7 +2014,21 @@ impl TestKeysInterface {
20122014
let factory = DefaultSignerFactory();
20132015

20142016
let now = Duration::from_secs(genesis_block(network).header.time as u64);
2015-
let backing = factory.make_signer(seed, now, false);
2017+
let backing = factory.make_signer(seed, now, false, None);
2018+
Self::build(backing)
2019+
}
2020+
2021+
pub fn with_settings(
2022+
seed: &[u8; 32], network: Network, v1_derivation: bool, phantom_seed: Option<&[u8; 32]>,
2023+
) -> Self {
2024+
#[cfg(feature = "std")]
2025+
let factory = SIGNER_FACTORY.get();
2026+
2027+
#[cfg(not(feature = "std"))]
2028+
let factory = DefaultSignerFactory();
2029+
2030+
let now = Duration::from_secs(genesis_block(network).header.time as u64);
2031+
let backing = factory.make_signer(seed, now, !v1_derivation, phantom_seed);
20162032
Self::build(backing)
20172033
}
20182034

0 commit comments

Comments
 (0)