Skip to content

Commit 99e4a1f

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 2caccc5 commit 99e4a1f

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
@@ -1055,8 +1055,8 @@ pub enum CandidateRouteHop<'a> {
10551055
hint: &'a (BlindedPayInfo, BlindedPath),
10561056
/// Index of the hint in the original list of blinded hints.
10571057
///
1058-
/// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
1059-
/// even though we don't have a short channel ID for this hop.
1058+
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
1059+
/// a short channel ID for this hop.
10601060
hint_idx: usize,
10611061
},
10621062
/// Similar to [`Self::Blinded`], but the path here only has one hop.
@@ -1078,18 +1078,29 @@ pub enum CandidateRouteHop<'a> {
10781078
hint: &'a (BlindedPayInfo, BlindedPath),
10791079
/// Index of the hint in the original list of blinded hints.
10801080
///
1081-
/// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
1082-
/// even though we don't have a short channel ID for this hop.
1081+
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
1082+
/// a short channel ID for this hop.
10831083
hint_idx: usize,
10841084
},
10851085
}
10861086

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

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

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

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