Skip to content

Commit c4c78b2

Browse files
Limit phantom invoice hints to 3
1 parent 34af072 commit c4c78b2

File tree

1 file changed

+118
-7
lines changed

1 file changed

+118
-7
lines changed

lightning-invoice/src/utils.rs

+118-7
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ where
8686
/// participating node
8787
/// * It is fine to cache `phantom_route_hints` and reuse it across invoices, as long as the data is
8888
/// updated when a channel becomes disabled or closes
89-
/// * Note that if too many channels are included in [`PhantomRouteHints::channels`], the invoice
90-
/// may be too long for QR code scanning. To fix this, `PhantomRouteHints::channels` may be pared
91-
/// down
89+
/// * Note that the route hints generated from `phantom_route_hints` will be limited to a maximum
90+
/// of 3 hints to ensure that the invoice can be scanned in a QR code. These hints are selected
91+
/// in the order that the nodes in `PhantomRouteHints` are specified, selecting one hint per node
92+
/// until the maximum is hit. Callers may provide as many `PhantomRouteHints::channels` as
93+
/// desired, but note that some nodes will be trimmed if more than 3 nodes are provided.
9294
///
9395
/// `description_hash` is a SHA-256 hash of the description text
9496
///
@@ -220,12 +222,16 @@ where
220222

221223
/// Utility to select route hints for phantom invoices.
222224
/// See [`PhantomKeysManager`] for more information on phantom node payments.
225+
///
226+
/// To ensure that the phantom invoice is still readable by QR code, we limit to 3 hints per invoice:
227+
/// * Select up to three channels per node.
228+
/// * Select one hint from each node, up to three hints or until we run out of hints.
223229
fn select_phantom_hints<L: Deref>(amt_msat: Option<u64>, phantom_route_hints: Vec<PhantomRouteHints>,
224230
logger: L) -> Vec<RouteHint>
225231
where
226232
L::Target: Logger,
227233
{
228-
let mut phantom_hints : Vec<RouteHint> = Vec::new();
234+
let mut phantom_hints : Vec<Vec<RouteHint>> = Vec::new();
229235

230236
for PhantomRouteHints { channels, phantom_scid, real_node_pubkey } in phantom_route_hints {
231237
log_trace!(logger, "Generating phantom route hints for node {}",
@@ -239,7 +245,7 @@ where
239245
if route_hints.is_empty() {
240246
route_hints.push(RouteHint(vec![]))
241247
}
242-
for mut route_hint in route_hints {
248+
for route_hint in &mut route_hints {
243249
route_hint.0.push(RouteHintHop {
244250
src_node_id: real_node_pubkey,
245251
short_channel_id: phantom_scid,
@@ -250,12 +256,36 @@ where
250256
cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA,
251257
htlc_minimum_msat: None,
252258
htlc_maximum_msat: None,});
259+
}
260+
261+
phantom_hints.push(route_hints);
262+
}
253263

254-
phantom_hints.push(route_hint.clone());
264+
// We have one vector per real node involved in creating the phantom invoice. To distribute
265+
// the hints across our real nodes we add one hint from each in turn until no node has any hints
266+
// left (if one node has more hints than any other, these will accumulate at the end of the
267+
// vector).
268+
let mut invoice_hints: Vec<&RouteHint> = Vec::new();
269+
let mut hint_idx = 0;
270+
271+
loop {
272+
let mut remaining_hints = false;
273+
274+
for hints in phantom_hints.iter() {
275+
if hint_idx < hints.len() {
276+
invoice_hints.push(&hints[hint_idx]);
277+
remaining_hints = true
278+
}
255279
}
280+
281+
if !remaining_hints {
282+
break
283+
}
284+
285+
hint_idx +=1;
256286
}
257287

258-
phantom_hints
288+
invoice_hints.into_iter().take(3).cloned().collect()
259289
}
260290

261291
#[cfg(feature = "std")]
@@ -1536,6 +1566,64 @@ mod test {
15361566
);
15371567
}
15381568

1569+
#[test]
1570+
fn test_multi_node_hints_limited_to_3(){
1571+
let mut chanmon_cfgs = create_chanmon_cfgs(5);
1572+
let seed_1 = [42 as u8; 32];
1573+
let seed_2 = [43 as u8; 32];
1574+
let seed_3 = [44 as u8; 32];
1575+
let cross_node_seed = [44 as u8; 32];
1576+
chanmon_cfgs[2].keys_manager.backing = PhantomKeysManager::new(&seed_1, 43, 44, &cross_node_seed);
1577+
chanmon_cfgs[3].keys_manager.backing = PhantomKeysManager::new(&seed_2, 43, 44, &cross_node_seed);
1578+
chanmon_cfgs[4].keys_manager.backing = PhantomKeysManager::new(&seed_3, 43, 44, &cross_node_seed);
1579+
let node_cfgs = create_node_cfgs(5, &chanmon_cfgs);
1580+
let node_chanmgrs = create_node_chanmgrs(5, &node_cfgs, &[None, None, None, None, None]);
1581+
let nodes = create_network(5, &node_cfgs, &node_chanmgrs);
1582+
1583+
// Setup each phantom node with two channels from distinct peers.
1584+
let _chan_0_2 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 10_000, 0);
1585+
let _chan_1_2 = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 20_000, 0);
1586+
let _chan_0_3 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 3, 20_000, 0);
1587+
let _chan_1_3 = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 10_000, 0);
1588+
let _chan_0_4 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 4, 20_000, 0);
1589+
let _chan_1_4 = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000, 0);
1590+
1591+
// Since the invoice amount is above all channels inbound, every one is eligible. Test that
1592+
// we include one channel from each of our three nodes.
1593+
match_multi_node_invoice_distinct_nodes(
1594+
Some(100_000_000),
1595+
&nodes[3],
1596+
vec![&nodes[2], &nodes[3], &nodes[4],],
1597+
3,
1598+
);
1599+
}
1600+
1601+
#[test]
1602+
fn test_multi_node_hints_at_least_3(){
1603+
let mut chanmon_cfgs = create_chanmon_cfgs(5);
1604+
let seed_1 = [42 as u8; 32];
1605+
let seed_2 = [43 as u8; 32];
1606+
let cross_node_seed = [44 as u8; 32];
1607+
chanmon_cfgs[1].keys_manager.backing = PhantomKeysManager::new(&seed_1, 43, 44, &cross_node_seed);
1608+
chanmon_cfgs[2].keys_manager.backing = PhantomKeysManager::new(&seed_2, 43, 44, &cross_node_seed);
1609+
let node_cfgs = create_node_cfgs(5, &chanmon_cfgs);
1610+
let node_chanmgrs = create_node_chanmgrs(5, &node_cfgs, &[None, None, None, None, None]);
1611+
let nodes = create_network(5, &node_cfgs, &node_chanmgrs);
1612+
1613+
let _chan_0_3 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 3, 10_000, 0);
1614+
let _chan_1_3 = create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 20_000, 0);
1615+
let _chan_2_3 = create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 30_000, 0);
1616+
let _chan_0_4 = create_unannounced_chan_between_nodes_with_value(&nodes, 0, 4, 10_000, 0);
1617+
1618+
// Since the invoice amount is above all channels inbound, all four are eligible. Test that
1619+
// we still include 3 hints from 2 distinct peers of our two phantom nodes.
1620+
match_multi_node_invoice_distinct_nodes(
1621+
Some(100_000_000),
1622+
&nodes[3],vec![&nodes[3], &nodes[4],],
1623+
2,
1624+
);
1625+
}
1626+
15391627
fn create_multi_node_invoice<'a, 'b: 'a, 'c: 'b>(
15401628
invoice_amt: Option<u64>,
15411629
invoice_node: &Node<'a, 'b, 'c>,
@@ -1556,6 +1644,29 @@ mod test {
15561644
(invoice, phantom_scids)
15571645
}
15581646

1647+
fn match_multi_node_invoice_distinct_nodes<'a, 'b: 'a, 'c: 'b>(
1648+
invoice_amt: Option<u64>,
1649+
invoice_node: &Node<'a, 'b, 'c>,
1650+
network_multi_nodes: Vec<&Node<'a, 'b, 'c>>,
1651+
distinct_nodes: usize,
1652+
){
1653+
let (invoice, phantom_scids) = create_multi_node_invoice(invoice_amt, invoice_node, network_multi_nodes);
1654+
1655+
let mut hint_nodes = crate::HashMap::new();
1656+
for hint in invoice.private_routes() {
1657+
let hints = &(hint.0).0;
1658+
match hints.len() {
1659+
2 => {
1660+
hint_nodes.insert(hints[1].src_node_id, true);
1661+
let phantom_scid = hints[1].short_channel_id;
1662+
assert!(phantom_scids.contains(&phantom_scid));
1663+
},
1664+
_ => panic!("Incorrect hint length generated")
1665+
}
1666+
}
1667+
assert_eq!(hint_nodes.len(), distinct_nodes, "Unmatched number of distinct phantom nodes for hints.");
1668+
}
1669+
15591670
fn match_multi_node_invoice_routes<'a, 'b: 'a, 'c: 'b>(
15601671
invoice_amt: Option<u64>,
15611672
invoice_node: &Node<'a, 'b, 'c>,

0 commit comments

Comments
 (0)