Skip to content

Commit cc6a6b3

Browse files
committed
[router] Make Dijkstra's path scoring 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 cc6a6b3

File tree

1 file changed

+170
-13
lines changed

1 file changed

+170
-13
lines changed

lightning/src/routing/router.rs

Lines changed: 170 additions & 13 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+
(other.lowest_fee_to_peer_through_node + other.path_htlc_minimum_msat)
155+
.cmp(&(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

@@ -646,19 +651,17 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
646651
// by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
647652
// to this channel.
648653
// 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;
654+
let old_cost = if let Some(increased_old_cost) = old_entry.total_fee_msat.checked_add(old_entry.path_htlc_minimum_msat) {
655+
increased_old_cost
652656
} else {
653-
old_cost = u64::max_value();
654-
}
657+
u64::max_value()
658+
};
655659

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;
660+
let new_cost = if let Some(increased_new_cost) = total_fee_msat.checked_add(path_htlc_minimum_msat) {
661+
increased_new_cost
659662
} else {
660-
new_cost = u64::max_value();
661-
}
663+
u64::max_value()
664+
};
662665

663666
if new_cost < old_cost {
664667
targets.push(new_graph_node);
@@ -674,9 +677,8 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
674677
cltv_expiry_delta: $directional_info.cltv_expiry_delta as u32,
675678
};
676679
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.
679680
old_entry.htlc_minimum_msat = $directional_info.htlc_minimum_msat;
681+
old_entry.path_htlc_minimum_msat = path_htlc_minimum_msat;
680682
}
681683
}
682684
}
@@ -3400,6 +3402,161 @@ mod tests {
34003402
}
34013403
}
34023404

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

0 commit comments

Comments
 (0)