Skip to content

Commit 68b62c0

Browse files
committed
[router] Make Dijkstra's path scoring non-decreasing + consistent
Currently, the "best source" for a given node tracked during Dijkstra's is updated with a different critera from the heap sorting, resulting in loops in calculated paths. This adds a test for the specific failure currently seen, utilizing the new path-htlc-minimum tracking in the heap entries in place of the per-hop htlc-minimum values which the MPP changeset partially used.
1 parent c024ea6 commit 68b62c0

File tree

1 file changed

+163
-17
lines changed

1 file changed

+163
-17
lines changed

lightning/src/routing/router.rs

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ struct RouteGraphNode {
151151

152152
impl cmp::Ord for RouteGraphNode {
153153
fn cmp(&self, other: &RouteGraphNode) -> cmp::Ordering {
154-
other.lowest_fee_to_peer_through_node.cmp(&self.lowest_fee_to_peer_through_node)
154+
cmp::max(other.lowest_fee_to_peer_through_node, other.path_htlc_minimum_msat)
155+
.cmp(&cmp::max(self.lowest_fee_to_peer_through_node, self.path_htlc_minimum_msat))
155156
.then_with(|| other.pubkey.serialize().cmp(&self.pubkey.serialize()))
156157
}
157158
}
@@ -196,6 +197,9 @@ struct PathBuildingHop {
196197
/// we don't fall below the minimum. Should not be updated manually and
197198
/// generally should not be accessed.
198199
htlc_minimum_msat: u64,
200+
/// A mirror of the same field in RouteGraphNode. Note that this is only used during the graph
201+
/// walk and may be invalid thereafter.
202+
path_htlc_minimum_msat: u64,
199203
}
200204

201205
// Instantiated with a list of hops with correct data in them collected during path finding,
@@ -590,6 +594,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
590594
hop_use_fee_msat: u64::max_value(),
591595
total_fee_msat: u64::max_value(),
592596
htlc_minimum_msat: $directional_info.htlc_minimum_msat,
597+
path_htlc_minimum_msat,
593598
}
594599
});
595600

@@ -645,20 +650,12 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
645650
// 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
646651
// by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
647652
// to this channel.
648-
// TODO: this scoring could be smarter (e.g. 0.5*htlc_minimum_msat here).
649-
let mut old_cost = old_entry.total_fee_msat;
650-
if let Some(increased_old_cost) = old_cost.checked_add(old_entry.htlc_minimum_msat) {
651-
old_cost = increased_old_cost;
652-
} else {
653-
old_cost = u64::max_value();
654-
}
655-
656-
let mut new_cost = total_fee_msat;
657-
if let Some(increased_new_cost) = new_cost.checked_add($directional_info.htlc_minimum_msat) {
658-
new_cost = increased_new_cost;
659-
} else {
660-
new_cost = u64::max_value();
661-
}
653+
// Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
654+
// but it may require additional tracking - we don't want to double-count
655+
// the fees included in $incl_fee_next_hops_htlc_minimum_msat, but also
656+
// can't use something that may decrease on future hops.
657+
let old_cost = cmp::max(old_entry.total_fee_msat, old_entry.path_htlc_minimum_msat);
658+
let new_cost = cmp::max(total_fee_msat, $incl_fee_next_hops_htlc_minimum_msat);
662659

663660
if new_cost < old_cost {
664661
targets.push(new_graph_node);
@@ -674,9 +671,8 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
674671
cltv_expiry_delta: $directional_info.cltv_expiry_delta as u32,
675672
};
676673
old_entry.channel_fees = $directional_info.fees;
677-
// It's probably fine to replace the old entry, because the new one
678-
// passed the htlc_minimum-related checks above.
679674
old_entry.htlc_minimum_msat = $directional_info.htlc_minimum_msat;
675+
old_entry.path_htlc_minimum_msat = $incl_fee_next_hops_htlc_minimum_msat;
680676
}
681677
}
682678
}
@@ -3400,6 +3396,156 @@ mod tests {
34003396
}
34013397
}
34023398

3399+
#[test]
3400+
fn min_criteria_consistency() {
3401+
// Test that we don't use an inconsistent metric between updating and walking nodes during
3402+
// our Dijkstra's pass. In the initial version of MPP, the "best source" for a given node
3403+
// was updated with a different critera from the heap sorting, resulting in loops in
3404+
// calculated paths. We test for that specific case here.
3405+
3406+
// We construct a network that looks like this:
3407+
//
3408+
// node2 -1(3)2- node3
3409+
// 2 2
3410+
// (2) (4)
3411+
// 1 1
3412+
// node1 -1(5)2- node4 -1(1)2- node6
3413+
// 2
3414+
// (6)
3415+
// 1
3416+
// our_node
3417+
//
3418+
// We create a loop on the side of our real path - our destination is node 6, with a
3419+
// previous hop of node 4. From 4, the cheapest previous path is channel 2 from node 2,
3420+
// followed by node 3 over channel 3. Thereafter, the cheapest next-hop is back to node 4
3421+
// (this time over channel 4). Channel 4 has 0 htlc_minimum_msat whereas channel 1 (the
3422+
// other channel with a previous-hop of node 4) has a high (but irrelevant to the overall
3423+
// payment) htlc_minimum_msat. In the original algorithm, this resulted in node4's
3424+
// "previous hop" being set to node 3, creating a loop in the path.
3425+
let secp_ctx = Secp256k1::new();
3426+
let logger = Arc::new(test_utils::TestLogger::new());
3427+
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
3428+
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
3429+
3430+
add_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, &privkeys[1], ChannelFeatures::from_le_bytes(id_to_feature_flags(6)), 6);
3431+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
3432+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3433+
short_channel_id: 6,
3434+
timestamp: 1,
3435+
flags: 0,
3436+
cltv_expiry_delta: (6 << 8) | 0,
3437+
htlc_minimum_msat: 0,
3438+
htlc_maximum_msat: OptionalField::Absent,
3439+
fee_base_msat: 0,
3440+
fee_proportional_millionths: 0,
3441+
excess_data: Vec::new()
3442+
});
3443+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[1], NodeFeatures::from_le_bytes(id_to_feature_flags(1)), 0);
3444+
3445+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], &privkeys[4], ChannelFeatures::from_le_bytes(id_to_feature_flags(5)), 5);
3446+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
3447+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3448+
short_channel_id: 5,
3449+
timestamp: 1,
3450+
flags: 0,
3451+
cltv_expiry_delta: (5 << 8) | 0,
3452+
htlc_minimum_msat: 0,
3453+
htlc_maximum_msat: OptionalField::Absent,
3454+
fee_base_msat: 100,
3455+
fee_proportional_millionths: 0,
3456+
excess_data: Vec::new()
3457+
});
3458+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[4], NodeFeatures::from_le_bytes(id_to_feature_flags(4)), 0);
3459+
3460+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], &privkeys[3], ChannelFeatures::from_le_bytes(id_to_feature_flags(4)), 4);
3461+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], UnsignedChannelUpdate {
3462+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3463+
short_channel_id: 4,
3464+
timestamp: 1,
3465+
flags: 0,
3466+
cltv_expiry_delta: (4 << 8) | 0,
3467+
htlc_minimum_msat: 0,
3468+
htlc_maximum_msat: OptionalField::Absent,
3469+
fee_base_msat: 0,
3470+
fee_proportional_millionths: 0,
3471+
excess_data: Vec::new()
3472+
});
3473+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[3], NodeFeatures::from_le_bytes(id_to_feature_flags(3)), 0);
3474+
3475+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[3], &privkeys[2], ChannelFeatures::from_le_bytes(id_to_feature_flags(3)), 3);
3476+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[3], UnsignedChannelUpdate {
3477+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3478+
short_channel_id: 3,
3479+
timestamp: 1,
3480+
flags: 0,
3481+
cltv_expiry_delta: (3 << 8) | 0,
3482+
htlc_minimum_msat: 0,
3483+
htlc_maximum_msat: OptionalField::Absent,
3484+
fee_base_msat: 0,
3485+
fee_proportional_millionths: 0,
3486+
excess_data: Vec::new()
3487+
});
3488+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[2], NodeFeatures::from_le_bytes(id_to_feature_flags(2)), 0);
3489+
3490+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], &privkeys[4], ChannelFeatures::from_le_bytes(id_to_feature_flags(2)), 2);
3491+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
3492+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3493+
short_channel_id: 2,
3494+
timestamp: 1,
3495+
flags: 0,
3496+
cltv_expiry_delta: (2 << 8) | 0,
3497+
htlc_minimum_msat: 0,
3498+
htlc_maximum_msat: OptionalField::Absent,
3499+
fee_base_msat: 0,
3500+
fee_proportional_millionths: 0,
3501+
excess_data: Vec::new()
3502+
});
3503+
3504+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], &privkeys[6], ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 1);
3505+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], UnsignedChannelUpdate {
3506+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3507+
short_channel_id: 1,
3508+
timestamp: 1,
3509+
flags: 0,
3510+
cltv_expiry_delta: (1 << 8) | 0,
3511+
htlc_minimum_msat: 100,
3512+
htlc_maximum_msat: OptionalField::Absent,
3513+
fee_base_msat: 0,
3514+
fee_proportional_millionths: 0,
3515+
excess_data: Vec::new()
3516+
});
3517+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[6], NodeFeatures::from_le_bytes(id_to_feature_flags(6)), 0);
3518+
3519+
{
3520+
// Now ensure the route flows simply over nodes 1 and 4 to 6.
3521+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, &Vec::new(), 10_000, 42, Arc::clone(&logger)).unwrap();
3522+
assert_eq!(route.paths.len(), 1);
3523+
assert_eq!(route.paths[0].len(), 3);
3524+
3525+
assert_eq!(route.paths[0][0].pubkey, nodes[1]);
3526+
assert_eq!(route.paths[0][0].short_channel_id, 6);
3527+
assert_eq!(route.paths[0][0].fee_msat, 100);
3528+
assert_eq!(route.paths[0][0].cltv_expiry_delta, (5 << 8) | 0);
3529+
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(1));
3530+
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(6));
3531+
3532+
assert_eq!(route.paths[0][1].pubkey, nodes[4]);
3533+
assert_eq!(route.paths[0][1].short_channel_id, 5);
3534+
assert_eq!(route.paths[0][1].fee_msat, 0);
3535+
assert_eq!(route.paths[0][1].cltv_expiry_delta, (1 << 8) | 0);
3536+
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(4));
3537+
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(5));
3538+
3539+
assert_eq!(route.paths[0][2].pubkey, nodes[6]);
3540+
assert_eq!(route.paths[0][2].short_channel_id, 1);
3541+
assert_eq!(route.paths[0][2].fee_msat, 10_000);
3542+
assert_eq!(route.paths[0][2].cltv_expiry_delta, 42);
3543+
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(6));
3544+
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(1));
3545+
}
3546+
}
3547+
3548+
34033549
#[test]
34043550
fn exact_fee_liquidity_limit() {
34053551
// Test that if, while walking the graph, we find a hop that has exactly enough liquidity

0 commit comments

Comments
 (0)