Skip to content

Commit a602cb2

Browse files
committed
Privatise CandidateRouteHop::short_channel_id as its a footgun
Short channel "ID"s are not globally unique when they come from a BOLT 11 route hint or a first hop (which can be an outbound SCID alias). In those cases, its rather confusing that we have a `short_channel_id` method which mixes them all together, and even more confusing that we have a `CandidateHopId` which is not, in fact returning a unique identifier. In our routing logic this is mostly fine - the cost of a collision isn't super high and we should still do just fine finding a route, however the same can't be true for downstream users, as they may or may not rely on the apparent guarantees. Thus, here, we privatise the SCID and id accessors.
1 parent f73ee6f commit a602cb2

File tree

3 files changed

+58
-27
lines changed

3 files changed

+58
-27
lines changed

lightning/src/routing/router.rs

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,8 +1054,8 @@ pub enum CandidateRouteHop<'a> {
10541054
hint: &'a (BlindedPayInfo, BlindedPath),
10551055
/// Index of the hint in the original list of blinded hints.
10561056
///
1057-
/// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
1058-
/// even though we don't have a short channel ID for this hop.
1057+
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
1058+
/// a short channel ID for this hop.
10591059
hint_idx: usize,
10601060
},
10611061
/// Similar to [`Self::Blinded`], but the path here only has one hop.
@@ -1077,18 +1077,29 @@ pub enum CandidateRouteHop<'a> {
10771077
hint: &'a (BlindedPayInfo, BlindedPath),
10781078
/// Index of the hint in the original list of blinded hints.
10791079
///
1080-
/// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
1081-
/// even though we don't have a short channel ID for this hop.
1080+
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
1081+
/// a short channel ID for this hop.
10821082
hint_idx: usize,
10831083
},
10841084
}
10851085

10861086
impl<'a> CandidateRouteHop<'a> {
1087-
/// Returns short_channel_id if known.
1088-
/// For `FirstHop` we assume [`ChannelDetails::get_outbound_payment_scid`] is always set, this assumption is checked in
1089-
/// [`find_route`] method.
1090-
/// For `Blinded` and `OneHopBlinded` we return `None` because next hop is not known.
1091-
pub fn short_channel_id(&self) -> Option<u64> {
1087+
/// Returns the short channel ID for this hop, if one is known.
1088+
///
1089+
/// This SCID could be an alias or a globally unique SCID, and thus is only expected to
1090+
/// uniquely identify this channel in conjunction with the [`CandidateRouteHop::source`].
1091+
///
1092+
/// Returns `Some` as long as the candidate is a [`CandidateRouteHop::PublicHop`], a
1093+
/// [`CandidateRouteHop::PrivateHop`] from a BOLT 11 route hint, or a
1094+
/// [`CandidateRouteHop::FirstHop`] with a known [`ChannelDetails::get_outbound_payment_scid`]
1095+
/// (which is always true for channels which are funded and ready for use).
1096+
///
1097+
/// In other words, this should always return `Some` as long as the candidate hop is not a
1098+
/// [`CandidateRouteHop::Blinded`] or a [`CandidateRouteHop::OneHopBlinded`].
1099+
///
1100+
/// Note that this is deliberately not public as it is somewhat of a footgun because it doesn't
1101+
/// define a global namespace.
1102+
fn short_channel_id(&self) -> Option<u64> {
10921103
match self {
10931104
CandidateRouteHop::FirstHop { details, .. } => details.get_outbound_payment_scid(),
10941105
CandidateRouteHop::PublicHop { short_channel_id, .. } => Some(*short_channel_id),
@@ -1098,6 +1109,21 @@ impl<'a> CandidateRouteHop<'a> {
10981109
}
10991110
}
11001111

1112+
/// Returns the globally unique short channel ID for this hop, if one is known.
1113+
///
1114+
/// This only returns `Some` if the channel is public (either our own, or one we've learned
1115+
/// from the public network graph), and thus the short channel ID we have for this channel is
1116+
/// globally unique and identifies this channel in a global namespace.
1117+
pub fn globally_unique_short_channel_id(&self) -> Option<u64> {
1118+
match self {
1119+
CandidateRouteHop::FirstHop { details, .. } => if details.is_public { details.short_channel_id } else { None },
1120+
CandidateRouteHop::PublicHop { short_channel_id, .. } => Some(*short_channel_id),
1121+
CandidateRouteHop::PrivateHop { .. } => None,
1122+
CandidateRouteHop::Blinded { .. } => None,
1123+
CandidateRouteHop::OneHopBlinded { .. } => None,
1124+
}
1125+
}
1126+
11011127
// NOTE: This may alloc memory so avoid calling it in a hot code path.
11021128
fn features(&self) -> ChannelFeatures {
11031129
match self {
@@ -1166,10 +1192,10 @@ impl<'a> CandidateRouteHop<'a> {
11661192
}
11671193
}
11681194

1169-
/// Returns the id of this hop.
1170-
/// For `Blinded` and `OneHopBlinded` we return `CandidateHopId::Blinded` with `hint_idx` because we don't know the channel id.
1171-
/// For any other option we return `CandidateHopId::Clear` because we know the channel id and the direction.
1172-
pub fn id(&self) -> CandidateHopId {
1195+
/// Returns an ID describing the given hop.
1196+
///
1197+
/// See the docs on [`CandidateHopId`] for when this is, or is not, unique.
1198+
fn id(&self) -> CandidateHopId {
11731199
match self {
11741200
CandidateRouteHop::Blinded { hint_idx, .. } => CandidateHopId::Blinded(*hint_idx),
11751201
CandidateRouteHop::OneHopBlinded { hint_idx, .. } => CandidateHopId::Blinded(*hint_idx),
@@ -1214,12 +1240,17 @@ impl<'a> CandidateRouteHop<'a> {
12141240
}
12151241
}
12161242

1217-
/// A wrapper around the various hop id representations.
1243+
/// A unique(ish) identifier for a specific [`CandidateRouteHop`].
1244+
///
1245+
/// For blinded paths, this ID is unique only within a given [`find_route`] call.
1246+
///
1247+
/// For other hops, because SCIDs between private channels and public channels can conflict, this
1248+
/// isn't guaranteed to be unique at all.
12181249
///
1219-
/// `CandidateHopId::Clear` is used to identify a hop with a known short channel id and direction.
1220-
/// `CandidateHopId::Blinded` is used to identify a blinded hop `hint_idx`.
1250+
/// For our uses, this is generally fine, but it is not public as it is otherwise a rather
1251+
/// difficult-to-use API.
12211252
#[derive(Clone, Copy, Eq, Hash, Ord, PartialOrd, PartialEq)]
1222-
pub enum CandidateHopId {
1253+
enum CandidateHopId {
12231254
/// Contains (scid, src_node_id < target_node_id)
12241255
Clear((u64, bool)),
12251256
/// Index of the blinded route hint in [`Payee::Blinded::route_hints`].
@@ -2445,8 +2476,10 @@ where L::Target: Logger {
24452476
let target = ordered_hops.last().unwrap().0.candidate.target().unwrap_or(maybe_dummy_payee_node_id);
24462477
if let Some(first_channels) = first_hop_targets.get(&target) {
24472478
for details in first_channels {
2448-
if let Some(scid) = ordered_hops.last().unwrap().0.candidate.short_channel_id() {
2449-
if details.get_outbound_payment_scid().unwrap() == scid {
2479+
if let CandidateRouteHop::FirstHop { details: last_hop_details, .. }
2480+
= ordered_hops.last().unwrap().0.candidate
2481+
{
2482+
if details.get_outbound_payment_scid() == last_hop_details.get_outbound_payment_scid() {
24502483
ordered_hops.last_mut().unwrap().1 = details.counterparty.features.to_context();
24512484
features_set = true;
24522485
break;

lightning/src/routing/scoring.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,13 +1344,11 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreLookUp for Prob
13441344
fn channel_penalty_msat(
13451345
&self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &ProbabilisticScoringFeeParameters
13461346
) -> u64 {
1347-
let scid = match candidate.short_channel_id() {
1348-
Some(scid) => scid,
1349-
None => return 0,
1350-
};
1351-
let target = match candidate.target() {
1352-
Some(target) => target,
1353-
None => return 0,
1347+
let (scid, target) = match candidate {
1348+
CandidateRouteHop::PublicHop { info, short_channel_id } => {
1349+
(short_channel_id, info.target())
1350+
},
1351+
_ => return 0,
13541352
};
13551353
let source = candidate.source();
13561354
if let Some(penalty) = score_params.manual_node_penalties.get(&target) {

lightning/src/util/test_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,7 @@ impl ScoreLookUp for TestScorer {
13241324
fn channel_penalty_msat(
13251325
&self, candidate: &CandidateRouteHop, usage: ChannelUsage, _score_params: &Self::ScoreParams
13261326
) -> u64 {
1327-
let short_channel_id = match candidate.short_channel_id() {
1327+
let short_channel_id = match candidate.globally_unique_short_channel_id() {
13281328
Some(scid) => scid,
13291329
None => return 0,
13301330
};

0 commit comments

Comments
 (0)