Skip to content

Commit c80a8a3

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 3325268 commit c80a8a3

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,
@@ -609,6 +613,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
609613
hop_use_fee_msat: u64::max_value(),
610614
total_fee_msat: u64::max_value(),
611615
htlc_minimum_msat: $directional_info.htlc_minimum_msat,
616+
path_htlc_minimum_msat,
612617
}
613618
});
614619

@@ -664,20 +669,12 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
664669
// 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
665670
// by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
666671
// to this channel.
667-
// TODO: this scoring could be smarter (e.g. 0.5*htlc_minimum_msat here).
668-
let mut old_cost = old_entry.total_fee_msat;
669-
if let Some(increased_old_cost) = old_cost.checked_add(old_entry.htlc_minimum_msat) {
670-
old_cost = increased_old_cost;
671-
} else {
672-
old_cost = u64::max_value();
673-
}
674-
675-
let mut new_cost = total_fee_msat;
676-
if let Some(increased_new_cost) = new_cost.checked_add($directional_info.htlc_minimum_msat) {
677-
new_cost = increased_new_cost;
678-
} else {
679-
new_cost = u64::max_value();
680-
}
672+
// Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
673+
// but it may require additional tracking - we don't want to double-count
674+
// the fees included in $incl_fee_next_hops_htlc_minimum_msat, but also
675+
// can't use something that may decrease on future hops.
676+
let old_cost = cmp::max(old_entry.total_fee_msat, old_entry.path_htlc_minimum_msat);
677+
let new_cost = cmp::max(total_fee_msat, path_htlc_minimum_msat);
681678

682679
if new_cost < old_cost {
683680
targets.push(new_graph_node);
@@ -693,9 +690,8 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
693690
cltv_expiry_delta: $directional_info.cltv_expiry_delta as u32,
694691
};
695692
old_entry.channel_fees = $directional_info.fees;
696-
// It's probably fine to replace the old entry, because the new one
697-
// passed the htlc_minimum-related checks above.
698693
old_entry.htlc_minimum_msat = $directional_info.htlc_minimum_msat;
694+
old_entry.path_htlc_minimum_msat = path_htlc_minimum_msat;
699695
}
700696
}
701697
}
@@ -3480,6 +3476,156 @@ mod tests {
34803476
}
34813477
}
34823478

3479+
#[test]
3480+
fn min_criteria_consistency() {
3481+
// Test that we don't use an inconsistent metric between updating and walking nodes during
3482+
// our Dijkstra's pass. In the initial version of MPP, the "best source" for a given node
3483+
// was updated with a different critera from the heap sorting, resulting in loops in
3484+
// calculated paths. We test for that specific case here.
3485+
3486+
// We construct a network that looks like this:
3487+
//
3488+
// node2 -1(3)2- node3
3489+
// 2 2
3490+
// (2) (4)
3491+
// 1 1
3492+
// node1 -1(5)2- node4 -1(1)2- node6
3493+
// 2
3494+
// (6)
3495+
// 1
3496+
// our_node
3497+
//
3498+
// We create a loop on the side of our real path - our destination is node 6, with a
3499+
// previous hop of node 4. From 4, the cheapest previous path is channel 2 from node 2,
3500+
// followed by node 3 over channel 3. Thereafter, the cheapest next-hop is back to node 4
3501+
// (this time over channel 4). Channel 4 has 0 htlc_minimum_msat whereas channel 1 (the
3502+
// other channel with a previous-hop of node 4) has a high (but irrelevant to the overall
3503+
// payment) htlc_minimum_msat. In the original algorithm, this resulted in node4's
3504+
// "previous hop" being set to node 3, creating a loop in the path.
3505+
let secp_ctx = Secp256k1::new();
3506+
let logger = Arc::new(test_utils::TestLogger::new());
3507+
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
3508+
let (our_privkey, our_id, privkeys, nodes) = get_nodes(&secp_ctx);
3509+
3510+
add_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, &privkeys[1], ChannelFeatures::from_le_bytes(id_to_feature_flags(6)), 6);
3511+
update_channel(&net_graph_msg_handler, &secp_ctx, &our_privkey, UnsignedChannelUpdate {
3512+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3513+
short_channel_id: 6,
3514+
timestamp: 1,
3515+
flags: 0,
3516+
cltv_expiry_delta: (6 << 8) | 0,
3517+
htlc_minimum_msat: 0,
3518+
htlc_maximum_msat: OptionalField::Absent,
3519+
fee_base_msat: 0,
3520+
fee_proportional_millionths: 0,
3521+
excess_data: Vec::new()
3522+
});
3523+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[1], NodeFeatures::from_le_bytes(id_to_feature_flags(1)), 0);
3524+
3525+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], &privkeys[4], ChannelFeatures::from_le_bytes(id_to_feature_flags(5)), 5);
3526+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[1], UnsignedChannelUpdate {
3527+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3528+
short_channel_id: 5,
3529+
timestamp: 1,
3530+
flags: 0,
3531+
cltv_expiry_delta: (5 << 8) | 0,
3532+
htlc_minimum_msat: 0,
3533+
htlc_maximum_msat: OptionalField::Absent,
3534+
fee_base_msat: 100,
3535+
fee_proportional_millionths: 0,
3536+
excess_data: Vec::new()
3537+
});
3538+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[4], NodeFeatures::from_le_bytes(id_to_feature_flags(4)), 0);
3539+
3540+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], &privkeys[3], ChannelFeatures::from_le_bytes(id_to_feature_flags(4)), 4);
3541+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], UnsignedChannelUpdate {
3542+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3543+
short_channel_id: 4,
3544+
timestamp: 1,
3545+
flags: 0,
3546+
cltv_expiry_delta: (4 << 8) | 0,
3547+
htlc_minimum_msat: 0,
3548+
htlc_maximum_msat: OptionalField::Absent,
3549+
fee_base_msat: 0,
3550+
fee_proportional_millionths: 0,
3551+
excess_data: Vec::new()
3552+
});
3553+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[3], NodeFeatures::from_le_bytes(id_to_feature_flags(3)), 0);
3554+
3555+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[3], &privkeys[2], ChannelFeatures::from_le_bytes(id_to_feature_flags(3)), 3);
3556+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[3], UnsignedChannelUpdate {
3557+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3558+
short_channel_id: 3,
3559+
timestamp: 1,
3560+
flags: 0,
3561+
cltv_expiry_delta: (3 << 8) | 0,
3562+
htlc_minimum_msat: 0,
3563+
htlc_maximum_msat: OptionalField::Absent,
3564+
fee_base_msat: 0,
3565+
fee_proportional_millionths: 0,
3566+
excess_data: Vec::new()
3567+
});
3568+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[2], NodeFeatures::from_le_bytes(id_to_feature_flags(2)), 0);
3569+
3570+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], &privkeys[4], ChannelFeatures::from_le_bytes(id_to_feature_flags(2)), 2);
3571+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[2], UnsignedChannelUpdate {
3572+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3573+
short_channel_id: 2,
3574+
timestamp: 1,
3575+
flags: 0,
3576+
cltv_expiry_delta: (2 << 8) | 0,
3577+
htlc_minimum_msat: 0,
3578+
htlc_maximum_msat: OptionalField::Absent,
3579+
fee_base_msat: 0,
3580+
fee_proportional_millionths: 0,
3581+
excess_data: Vec::new()
3582+
});
3583+
3584+
add_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], &privkeys[6], ChannelFeatures::from_le_bytes(id_to_feature_flags(1)), 1);
3585+
update_channel(&net_graph_msg_handler, &secp_ctx, &privkeys[4], UnsignedChannelUpdate {
3586+
chain_hash: genesis_block(Network::Testnet).header.block_hash(),
3587+
short_channel_id: 1,
3588+
timestamp: 1,
3589+
flags: 0,
3590+
cltv_expiry_delta: (1 << 8) | 0,
3591+
htlc_minimum_msat: 100,
3592+
htlc_maximum_msat: OptionalField::Absent,
3593+
fee_base_msat: 0,
3594+
fee_proportional_millionths: 0,
3595+
excess_data: Vec::new()
3596+
});
3597+
add_or_update_node(&net_graph_msg_handler, &secp_ctx, &privkeys[6], NodeFeatures::from_le_bytes(id_to_feature_flags(6)), 0);
3598+
3599+
{
3600+
// Now ensure the route flows simply over nodes 1 and 4 to 6.
3601+
let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &Vec::new(), 10_000, 42, Arc::clone(&logger)).unwrap();
3602+
assert_eq!(route.paths.len(), 1);
3603+
assert_eq!(route.paths[0].len(), 3);
3604+
3605+
assert_eq!(route.paths[0][0].pubkey, nodes[1]);
3606+
assert_eq!(route.paths[0][0].short_channel_id, 6);
3607+
assert_eq!(route.paths[0][0].fee_msat, 100);
3608+
assert_eq!(route.paths[0][0].cltv_expiry_delta, (5 << 8) | 0);
3609+
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(1));
3610+
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(6));
3611+
3612+
assert_eq!(route.paths[0][1].pubkey, nodes[4]);
3613+
assert_eq!(route.paths[0][1].short_channel_id, 5);
3614+
assert_eq!(route.paths[0][1].fee_msat, 0);
3615+
assert_eq!(route.paths[0][1].cltv_expiry_delta, (1 << 8) | 0);
3616+
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(4));
3617+
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(5));
3618+
3619+
assert_eq!(route.paths[0][2].pubkey, nodes[6]);
3620+
assert_eq!(route.paths[0][2].short_channel_id, 1);
3621+
assert_eq!(route.paths[0][2].fee_msat, 10_000);
3622+
assert_eq!(route.paths[0][2].cltv_expiry_delta, 42);
3623+
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(6));
3624+
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(1));
3625+
}
3626+
}
3627+
3628+
34833629
#[test]
34843630
fn exact_fee_liquidity_limit() {
34853631
// Test that if, while walking the graph, we find a hop that has exactly enough liquidity

0 commit comments

Comments
 (0)