Skip to content

Commit 4cd9241

Browse files
authored
Merge pull request #4567 from Alkamal01/prefer-outbound-scid-alias-for-routing
Prefer outbound SCID alias over real SCID when routing
2 parents 3a48e75 + 59fd10c commit 4cd9241

7 files changed

Lines changed: 73 additions & 37 deletions

File tree

lightning/src/ln/channel_state.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,10 @@ pub struct ChannelDetails {
312312
/// Note that if [`inbound_scid_alias`] is set, it must be used for invoices and inbound
313313
/// payments instead of this. See [`get_inbound_payment_scid`].
314314
///
315-
/// For channels with [`confirmations_required`] set to `Some(0)`, [`outbound_scid_alias`] may
316-
/// be used in place of this in outbound routes. See [`get_outbound_payment_scid`].
315+
/// For routing outbound payments, this value should not be used if [`outbound_scid_alias`] is
316+
/// set. [`outbound_scid_alias`] provides a stable routing identifier across splices, whereas
317+
/// this value will change when a splice confirms.
318+
/// Use [`get_outbound_payment_scid`] to pick the appropriate value.
317319
///
318320
/// When a channel is spliced, this continues to refer to the original pre-splice channel
319321
/// state until the splice transaction reaches sufficient confirmations to be locked (and we
@@ -323,21 +325,17 @@ pub struct ChannelDetails {
323325
/// [`outbound_scid_alias`]: Self::outbound_scid_alias
324326
/// [`get_inbound_payment_scid`]: Self::get_inbound_payment_scid
325327
/// [`get_outbound_payment_scid`]: Self::get_outbound_payment_scid
326-
/// [`confirmations_required`]: Self::confirmations_required
327328
pub short_channel_id: Option<u64>,
328329
/// An optional [`short_channel_id`] alias for this channel, randomly generated by us and
329-
/// usable in place of [`short_channel_id`] to reference the channel in outbound routes when
330-
/// the channel has not yet been confirmed (as long as [`confirmations_required`] is
331-
/// `Some(0)`).
330+
/// usable in place of [`short_channel_id`] to route outbound payments. Because this alias is
331+
/// assigned at channel open and remains stable across splices, it should be used for routing
332+
/// instead of the real [`short_channel_id`] (which changes each time a splice confirms).
333+
/// See [`get_outbound_payment_scid`].
332334
///
333335
/// This will be `None` as long as the channel is not available for routing outbound payments.
334336
///
335-
/// When a channel is spliced, this continues to refer to the original pre-splice channel
336-
/// state until the splice transaction reaches sufficient confirmations to be locked (and we
337-
/// exchange `splice_locked` messages with our peer).
338-
///
339337
/// [`short_channel_id`]: Self::short_channel_id
340-
/// [`confirmations_required`]: Self::confirmations_required
338+
/// [`get_outbound_payment_scid`]: Self::get_outbound_payment_scid
341339
pub outbound_scid_alias: Option<u64>,
342340
/// An optional [`short_channel_id`] alias for this channel, randomly generated by our
343341
/// counterparty and usable in place of [`short_channel_id`] in invoice route hints. Our
@@ -513,12 +511,16 @@ impl ChannelDetails {
513511
/// This should be used in [`Route`]s to describe the first hop or in other contexts where
514512
/// we're sending or forwarding a payment outbound over this channel.
515513
///
516-
/// This is either the [`ChannelDetails::short_channel_id`], if set, or the
517-
/// [`ChannelDetails::outbound_scid_alias`]. See those for more information.
514+
/// Returns [`outbound_scid_alias`] if set, otherwise [`short_channel_id`]. The alias is
515+
/// preferred because when a splice confirms the real SCID changes, whereas the alias assigned
516+
/// at channel open remains stable.
517+
///
518+
/// [`outbound_scid_alias`]: ChannelDetails::outbound_scid_alias
519+
/// [`short_channel_id`]: ChannelDetails::short_channel_id
518520
///
519521
/// [`Route`]: crate::routing::router::Route
520522
pub fn get_outbound_payment_scid(&self) -> Option<u64> {
521-
self.short_channel_id.or(self.outbound_scid_alias)
523+
self.outbound_scid_alias.or(self.short_channel_id)
522524
}
523525

524526
/// Gets the funding output for this channel, if available.

lightning/src/ln/onion_route_tests.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -815,13 +815,16 @@ fn test_onion_failure() {
815815
let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None);
816816

817817
// Our immediate peer sent UpdateFailMalformedHTLC because it couldn't understand the onion in
818-
// the UpdateAddHTLC that we sent.
818+
// the UpdateAddHTLC that we sent. These tests explicitly route via the real SCID (not the
819+
// alias) so the expected_short_channel_id assertions below match.
819820
let short_channel_id = channels[0].0.contents.short_channel_id;
821+
let mut route_via_real_scid = route.clone();
822+
route_via_real_scid.paths[0].hops[0].short_channel_id = short_channel_id;
820823
run_onion_failure_test(
821824
"invalid_onion_version",
822825
0,
823826
&nodes,
824-
&route,
827+
&route_via_real_scid,
825828
&payment_hash,
826829
&payment_secret,
827830
|msg| {
@@ -839,7 +842,7 @@ fn test_onion_failure() {
839842
"invalid_onion_hmac",
840843
0,
841844
&nodes,
842-
&route,
845+
&route_via_real_scid,
843846
&payment_hash,
844847
&payment_secret,
845848
|msg| {
@@ -857,7 +860,7 @@ fn test_onion_failure() {
857860
"invalid_onion_key",
858861
0,
859862
&nodes,
860-
&route,
863+
&route_via_real_scid,
861864
&payment_hash,
862865
&payment_secret,
863866
|msg| {

lightning/src/ln/payment_tests.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,16 @@ fn mpp_retry_overpay() {
250250
let (mut route, hash, payment_preimage, pay_secret) =
251251
get_route_and_payment_hash!(nodes[0], nodes[3], payment_params, amt_msat, max_fee);
252252

253-
// Check we overpay on the second path which we're about to fail.
253+
// Check we overpay on the second path which we're about to fail. Path ordering is not fixed,
254+
// so we identify paths by first-hop pubkey.
254255
assert_eq!(chan_1_update.contents.fee_proportional_millionths, 0);
255-
let overpaid_amount_1 = route.paths[0].fee_msat() as u32 - chan_1_update.contents.fee_base_msat;
256+
let path_via_b = route.paths.iter().find(|p| p.hops[0].pubkey == node_b_id).unwrap();
257+
let overpaid_amount_1 = path_via_b.fee_msat() as u32 - chan_1_update.contents.fee_base_msat;
256258
assert_eq!(overpaid_amount_1, 0);
257259

258260
assert_eq!(chan_2_update.contents.fee_proportional_millionths, 0);
259-
let overpaid_amount_2 = route.paths[1].fee_msat() as u32 - chan_2_update.contents.fee_base_msat;
261+
let path_via_c = route.paths.iter().find(|p| p.hops[0].pubkey == node_c_id).unwrap();
262+
let overpaid_amount_2 = path_via_c.fee_msat() as u32 - chan_2_update.contents.fee_base_msat;
260263

261264
let total_overpaid_amount = overpaid_amount_1 + overpaid_amount_2;
262265

@@ -304,11 +307,13 @@ fn mpp_retry_overpay() {
304307
// Rebalance the channel so the second half of the payment can succeed.
305308
send_payment(&nodes[3], &[&nodes[2]], 38_000_000);
306309

307-
// Retry the second half of the payment and make sure it succeeds.
308-
let first_path_value = route.paths[0].final_value_msat();
310+
// Retry the second half of the payment and make sure it succeeds. Identify the successful
311+
// path (through nodes[1]) by first-hop pubkey, since path ordering is not stable.
312+
let path_via_b_idx = route.paths.iter().position(|p| p.hops[0].pubkey == node_b_id).unwrap();
313+
let first_path_value = route.paths[path_via_b_idx].final_value_msat();
309314
assert_eq!(first_path_value, 36_000_000);
310315

311-
route.paths.remove(0);
316+
route.paths.remove(path_via_b_idx);
312317
route_params.final_value_msat -= first_path_value;
313318
let chan_4_scid = chan_4_update.contents.short_channel_id;
314319
route_params.payment_params.previously_failed_channels.push(chan_4_scid);
@@ -2023,8 +2028,18 @@ fn preflight_probes_yield_event() {
20232028
let route_params = RouteParameters::from_payment_params_and_value(payment_params, recv_value);
20242029
let res = nodes[0].node.send_preflight_probes(route_params, None).unwrap();
20252030

2031+
// Path ordering depends on outbound SCID selection. Determine which res entry corresponds
2032+
// to which path by comparing the alias SCIDs of the two channels.
2033+
let node_b_id = nodes[1].node.get_our_node_id();
2034+
let node_c_id = nodes[2].node.get_our_node_id();
2035+
let chans = nodes[0].node.list_usable_channels();
2036+
let chan_to_b = chans.iter().find(|c| c.counterparty.node_id == node_b_id).unwrap();
2037+
let chan_to_c = chans.iter().find(|c| c.counterparty.node_id == node_c_id).unwrap();
2038+
let b_first = chan_to_b.get_outbound_payment_scid() < chan_to_c.get_outbound_payment_scid();
2039+
let (hash_b, hash_c) = if b_first { (res[0].0, res[1].0) } else { (res[1].0, res[0].0) };
2040+
20262041
let expected_route: &[(&[&Node], PaymentHash)] =
2027-
&[(&[&nodes[1], &nodes[3]], res[0].0), (&[&nodes[2], &nodes[3]], res[1].0)];
2042+
&[(&[&nodes[1], &nodes[3]], hash_b), (&[&nodes[2], &nodes[3]], hash_c)];
20282043

20292044
assert_eq!(res.len(), expected_route.len());
20302045

@@ -2319,7 +2334,7 @@ fn test_trivial_inflight_htlc_tracking() {
23192334
let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat(
23202335
&NodeId::from_pubkey(&node_a_id),
23212336
&NodeId::from_pubkey(&node_b_id),
2322-
channel_1.funding().get_short_channel_id().unwrap(),
2337+
channel_1.context().outbound_scid_alias(),
23232338
);
23242339
// First hop accounts for expected 1000 msat fee
23252340
assert_eq!(chan_1_used_liquidity, Some(501000));
@@ -2429,7 +2444,7 @@ fn test_holding_cell_inflight_htlcs() {
24292444
let used_liquidity = inflight_htlcs.used_liquidity_msat(
24302445
&NodeId::from_pubkey(&node_a_id),
24312446
&NodeId::from_pubkey(&node_b_id),
2432-
channel.funding().get_short_channel_id().unwrap(),
2447+
channel.context().outbound_scid_alias(),
24332448
);
24342449

24352450
assert_eq!(used_liquidity, Some(2000000));

lightning/src/ln/priv_short_conf_tests.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,16 +1108,16 @@ fn test_0conf_channel_reorg() {
11081108
mine_transaction(&nodes[1], &tx);
11091109
mine_transaction(&nodes[2], &tx);
11101110

1111-
// Send a payment using the channel's real SCID, which will be public in a few blocks once we
1112-
// can generate a channel_announcement.
1111+
// Send a payment using the channel's alias SCID. The channel itself will be public in a few
1112+
// blocks once we can generate a channel_announcement.
11131113
let bs_chans = nodes[1].node.list_usable_channels();
11141114
let bs_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap();
11151115
let original_scid = bs_chan.short_channel_id.unwrap();
11161116
assert_eq!(nodes[2].node.list_usable_channels()[0].short_channel_id.unwrap(), original_scid);
11171117

11181118
let (mut route, payment_hash, payment_preimage, payment_secret) =
11191119
get_route_and_payment_hash!(nodes[1], nodes[2], 10_000);
1120-
assert_eq!(route.paths[0].hops[0].short_channel_id, original_scid);
1120+
assert_eq!(route.paths[0].hops[0].short_channel_id, bs_chan.outbound_scid_alias.unwrap());
11211121
send_along_route_with_secret(
11221122
&nodes[1],
11231123
route.clone(),
@@ -1188,7 +1188,7 @@ fn test_0conf_channel_reorg() {
11881188
assert_ne!(original_scid, new_scid);
11891189
assert_eq!(nodes[2].node.list_usable_channels()[0].short_channel_id.unwrap(), new_scid);
11901190

1191-
// At this point, the channel should happily forward or send payments with either the old SCID
1191+
// At this point, the channel should happily forward or send payments with either the alias SCID
11921192
// or the new SCID...
11931193
send_along_route_with_secret(
11941194
&nodes[1],
@@ -1286,12 +1286,22 @@ fn test_0conf_channel_reorg() {
12861286

12871287
let onion = RecipientOnionFields::secret_only(payment_secret, 10_000);
12881288
let id = PaymentId([0; 32]);
1289-
nodes[1].node.send_payment_with_route(route, payment_hash, onion.clone(), id).unwrap();
1289+
1290+
// The route uses the alias SCID, which is stable across reorgs. To verify the old real SCID
1291+
// is invalidated after propagation delay, we explicitly build a route using original_scid.
1292+
let mut old_scid_route = route.clone();
1293+
old_scid_route.paths[0].hops[0].short_channel_id = original_scid;
1294+
nodes[1].node.send_payment_with_route(old_scid_route, payment_hash, onion.clone(), id).unwrap();
12901295
let mut conditions = PaymentFailedConditions::new();
12911296
conditions.reason = Some(PaymentFailureReason::RouteNotFound);
12921297
expect_payment_failed_conditions(&nodes[1], payment_hash, false, conditions);
12931298

1294-
nodes[0].node.send_payment_with_route(forwarded_route, payment_hash, onion, id).unwrap();
1299+
let mut old_scid_forwarded_route = forwarded_route.clone();
1300+
old_scid_forwarded_route.paths[0].hops[1].short_channel_id = original_scid;
1301+
nodes[0]
1302+
.node
1303+
.send_payment_with_route(old_scid_forwarded_route, payment_hash, onion, id)
1304+
.unwrap();
12951305
check_added_monitors(&nodes[0], 1);
12961306
let mut ev = nodes[0].node.get_and_clear_pending_msg_events();
12971307
assert_eq!(ev.len(), 1);

lightning/src/ln/reload_tests.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,12 @@ fn test_mpp_claim_htlc_fulfills_unblocked_on_reload() {
956956
let chan_id_b = chan_b.2;
957957
let scid_a = chan_a.0.contents.short_channel_id;
958958
let scid_b = chan_b.0.contents.short_channel_id;
959+
// Routes to a directly-connected peer use the outbound SCID alias, so payment path success
960+
// events report the alias rather than the real SCID announced in gossip.
961+
let payment_scid_a = nodes[0].node.list_channels().iter()
962+
.find(|chan| chan.channel_id == chan_id_a).unwrap().get_outbound_payment_scid().unwrap();
963+
let payment_scid_b = nodes[0].node.list_channels().iter()
964+
.find(|chan| chan.channel_id == chan_id_b).unwrap().get_outbound_payment_scid().unwrap();
959965

960966
// Send an MPP payment to nodes[1]. `send_along_route_with_secret` leaves the payment
961967
// claimable but unclaimed, so nodes[1] still has both inbound HTLCs live when we start
@@ -1214,7 +1220,7 @@ fn test_mpp_claim_htlc_fulfills_unblocked_on_reload() {
12141220
}
12151221
}
12161222
assert!(saw_startup_payment_sent);
1217-
assert_eq!(startup_success_scids, vec![scid_a]);
1223+
assert_eq!(startup_success_scids, vec![payment_scid_a]);
12181224

12191225
// Handling the claim event runs the event-completion action that releases the remaining
12201226
// RAA-blocked monitor update. The startup unblock path already released channel A, so channel B
@@ -1270,7 +1276,7 @@ fn test_mpp_claim_htlc_fulfills_unblocked_on_reload() {
12701276
Event::PaymentPathSuccessful { payment_hash: Some(path_hash), path, .. } => {
12711277
assert_eq!(*path_hash, payment_hash);
12721278
assert_eq!(path.hops.len(), 1);
1273-
assert_eq!(path.hops[0].short_channel_id, scid_b);
1279+
assert_eq!(path.hops[0].short_channel_id, payment_scid_b);
12741280
},
12751281
_ => panic!("Unexpected final payment event: {:?}", final_payment_events[0]),
12761282
}

lightning/src/ln/splicing_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3880,7 +3880,7 @@ fn fail_splice_on_tx_complete_error() {
38803880

38813881
// Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence.
38823882
let (route, payment_hash, _payment_preimage, payment_secret) =
3883-
get_route_and_payment_hash!(initiator, acceptor, 1_000_000);
3883+
get_route_and_payment_hash!(acceptor, initiator, 1_000_000);
38843884
let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000);
38853885
let payment_id = PaymentId(payment_hash.0);
38863886
acceptor.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap();

lightning/src/routing/router.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9187,7 +9187,7 @@ mod tests {
91879187
assert_eq!(route.paths.len(), 1);
91889188
assert_eq!(route.get_total_amount(), amt_msat);
91899189
assert_eq!(route.paths[0].hops.len(), 2);
9190-
assert_eq!(route.paths[0].hops[0].short_channel_id, 1);
9190+
assert_eq!(route.paths[0].hops[0].short_channel_id, 44);
91919191
assert_eq!(route.paths[0].hops[1].short_channel_id, 45);
91929192
assert_eq!(route.get_total_fees(), 123);
91939193
}

0 commit comments

Comments
 (0)