Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions lightning/src/blinded_path/payment.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ use crate::ln::msgs::DecodeError;
use crate::offers::invoice::BlindedPayInfo;
use crate::offers::invoice_request::InvoiceRequestFields;
use crate::offers::offer::OfferId;
use crate::routing::gossip::DirectedChannelInfo;
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, Writeable, Writer};

#[allow(unused_imports)]
Expand DownExpand Up@@ -170,6 +171,19 @@ impl PaymentContext {
}
}

impl PaymentRelay {
fn normalize_cltv_expiry_delta(cltv_expiry_delta: u16) -> Result<u16, ()> {
// Avoid exposing esoteric CLTV expiry deltas, which could de-anonymize the path.
match cltv_expiry_delta {
0..=40 => Ok(40),
41..=80 => Ok(80),
81..=144 => Ok(144),
145..=216 => Ok(216),
_ => Err(()),
}
}
}

impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
type Error = ();

Expand All@@ -178,16 +192,25 @@ impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay {
fee_base_msat, fee_proportional_millionths, cltv_expiry_delta
} = info;

// Avoid exposing esoteric CLTV expiry deltas
let cltv_expiry_delta = match cltv_expiry_delta {
0..=40 => 40,
41..=80 => 80,
81..=144 => 144,
145..=216 => 216,
_ => return Err(()),
};
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(cltv_expiry_delta)?,
fee_proportional_millionths,
fee_base_msat
})
}
}

impl<'a> TryFrom<DirectedChannelInfo<'a>> for PaymentRelay {
type Error = ();

fn try_from(info: DirectedChannelInfo<'a>) -> Result<Self, ()> {
let direction = info.direction();

Ok(Self { cltv_expiry_delta, fee_proportional_millionths, fee_base_msat })
Ok(Self {
cltv_expiry_delta: Self::normalize_cltv_expiry_delta(direction.cltv_expiry_delta)?,
fee_proportional_millionths: direction.fees.proportional_millionths,
fee_base_msat: direction.fees.base_msat,
})
}
}

Expand Down
87 changes: 62 additions & 25 deletions lightning/src/onion_message/messenger.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -186,7 +186,7 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// &keys_manager, &keys_manager, logger, &node_id_lookup, message_router,
/// &offers_message_handler, &custom_message_handler
/// );

///
/// # #[derive(Debug)]
/// # struct YourCustomMessage {}
/// impl Writeable for YourCustomMessage {
Expand All@@ -195,13 +195,15 @@ for OnionMessenger<ES, NS, L, NL, MR, OMH, CMH> where
/// // Write your custom onion message to `w`
/// }
/// }
///
/// impl OnionMessageContents for YourCustomMessage {
/// fn tlv_type(&self) -> u64 {
/// # let your_custom_message_type = 42;
/// your_custom_message_type
/// }
/// fn msg_type(&self) -> &'static str { "YourCustomMessageType" }
/// }
///
/// // Send a custom onion message to a node id.
/// let destination = Destination::Node(destination_node_id);
/// let reply_path = None;
Expand DownExpand Up@@ -457,6 +459,9 @@ pub trait MessageRouter {

/// A [`MessageRouter`] that can only route to a directly connected [`Destination`].
///
/// When creating [`BlindedPath`]s, prefers three-hop paths over two-hops paths for the compact
/// representation. For the non-compact representation, three-hop paths are not considered.
///
/// # Privacy
///
/// Creating [`BlindedPath`]s may affect privacy since, if a suitable path cannot be found, it will
Expand DownExpand Up@@ -488,6 +493,9 @@ where
>(
&self, recipient: PublicKey, peers: I, secp_ctx: &Secp256k1<T>, compact_paths: bool
) -> Result<Vec<BlindedPath>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PATHS: usize = 3;

Expand All@@ -500,40 +508,69 @@ where
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));

let mut peer_info = peers
.map(|peer| (NodeId::from_pubkey(&peer.node_id), peer))
// Limit to peers with announced channels
.filter_map(|peer|
.filter_map(|(node_id, peer)|
network_graph
.node(&NodeId::from_pubkey(&peer.node_id))
.node(&node_id)
.filter(|info| info.channels.len() >= MIN_PEER_CHANNELS)
.map(|info| (peer, info.is_tor_only(), info.channels.len()))
.map(|info| (node_id, peer, info.is_tor_only(), &info.channels))
)
// Exclude Tor-only nodes when the recipient is announced.
.filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.filter(|(_, _, is_tor_only, _)| !(*is_tor_only && is_recipient_announced))
.collect::<Vec<_>>();

// Prefer using non-Tor nodes with the most channels as the introduction node.
peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
peer_info.sort_unstable_by(|(_, _, a_tor_only, a_channels), (_, _, b_tor_only, b_channels)| {
a_tor_only.cmp(b_tor_only).then(a_channels.len().cmp(&b_channels.len()).reverse())
});

let paths = peer_info.into_iter()
.map(|(peer, _, _)| {
BlindedPath::new_for_message(&[peer], recipient, &*self.entropy_source, secp_ctx)
})
.take(MAX_PATHS)
.collect::<Result<Vec<_>, _>>();

let mut paths = match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if is_recipient_announced {
BlindedPath::one_hop_for_message(recipient, &*self.entropy_source, secp_ctx)
.map(|path| vec![path])
} else {
Err(())
}
},
}?;
let three_hop_paths = peer_info.iter()
// Pair peers with their other peers
.flat_map(|(node_id, peer, _, channels)|
channels
.iter()
.filter_map(|scid| network_graph.channels().get(scid))
.filter_map(move |info| info
.as_directed_to(&node_id)
.map(|(_, source)| source)
)
.filter(|source| **source != recipient_node_id)
.filter(|source| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_onion_messages())
.unwrap_or(false)
)
.filter_map(|source| source.as_pubkey().ok())
.map(move |source_pubkey| (source_pubkey, peer.clone()))
)
.map(|(source_pubkey, peer)| BlindedPath::new_for_message(&[ForwardNode { node_id: source_pubkey, short_channel_id: None }, peer], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

let two_hop_paths = peer_info
.iter()
.map(|(_, peer, _, _)| BlindedPath::new_for_message(&[peer.clone()], recipient, entropy_source, secp_ctx))
.take(MAX_PATHS);

// Prefer three-hop paths over two-hop paths for compact paths. Fallback to a one-hop path
// if none were found and the recipient node is announced.
let mut paths = (!compact_paths).then(|| vec![])
.or_else(|| three_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| is_recipient_announced
.then(|| BlindedPath::one_hop_for_message(recipient, entropy_source, secp_ctx)
.map(|path| vec![path])
.unwrap_or(vec![])
)
)
.ok_or(())?;

if paths.is_empty() {
return Err(());
}

if compact_paths {
for path in &mut paths {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/routing/gossip.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1040,7 +1040,7 @@ impl<'a> DirectedChannelInfo<'a> {

/// Returns information for the direction.
#[inline]
pub(super) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }
pub(crate) fn direction(&self) -> &'a ChannelUpdateInfo { self.direction }

/// Returns the `node_id` of the source hop.
///
Expand Down
145 changes: 120 additions & 25 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,25 +93,37 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
&self, recipient: PublicKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs,
amount_msats: u64, secp_ctx: &Secp256k1<T>
) -> Result<Vec<(BlindedPayInfo, BlindedPath)>, ()> {
let entropy_source = &*self.entropy_source;
let recipient_node_id = NodeId::from_pubkey(&recipient);

// Limit the number of blinded paths that are computed.
const MAX_PAYMENT_PATHS: usize = 3;

// Ensure peers have at least three channels so that it is more difficult to infer the
// recipient's node_id.
const MIN_PEER_CHANNELS: usize = 3;

// The minimum channel balance certainty required for using a channel in a blinded path.
const MIN_CHANNEL_CERTAINTY: f64 = 0.5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having some arbitrary "certainty" constant in the router itself, should we not just let the scorer figure out what the right certainty threshold (or even certainty concept) is? Basically have it return Option<u64 (or f64)> and just use that to select the best path, don't apply any arbitrary limits here but make sure the docs are clear that the scorer should apply arbitrary limits itself.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... as currently written, the return value is used to compare against MIN_CHANNEL_CERTAINTY. If the scorer applies a limit itself, it really only needs to return a bool. Or are you suggesting we combine the two new methods into one returning Some success probability or None? Note that certainty is only used for filtering -- so it is not used to choose the best path -- while success probability is dependent on the amount and used for both filtering and sorting.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I'd imagine we probably return either an option to include the probability/hop cost or we use something like u64::MAX/f64::INF as a magic "dont use this channel" value (like we do now with the fee cost).


// The minimum success probability required for using a channel in a blinded path.
const MIN_SUCCESS_PROBABILITY: f64 = 0.25;

let network_graph = self.network_graph.deref().read_only();
let paths = first_hops.into_iter()
let counterparty_channels = first_hops.into_iter()
.filter(|details| details.counterparty.features.supports_route_blinding())
.filter(|details| amount_msats <= details.inbound_capacity_msat)
.filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0))
.filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX))
.filter(|details| network_graph
// Limit to counterparties with announced channels
.filter_map(|details|
network_graph
.node(&NodeId::from_pubkey(&details.counterparty.node_id))
.map(|node_info| node_info.channels.len() >= MIN_PEER_CHANNELS)
.unwrap_or(false)
.map(|info| &info.channels[..])
.and_then(|channels| (channels.len() >= MIN_PEER_CHANNELS).then(|| channels))
.map(|channels| (details, channels))
)
.filter_map(|details| {
.filter_map(|(details, counterparty_channels)| {
let short_channel_id = match details.get_inbound_payment_scid() {
Some(short_channel_id) => short_channel_id,
None => return None,
Expand All@@ -129,7 +141,7 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
max_cltv_expiry: tlvs.payment_constraints.max_cltv_expiry + cltv_expiry_delta,
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
};
Some(payment::ForwardNode {
let forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id,
payment_relay,
Expand All@@ -138,29 +150,112 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Deref, ES: Deref, S: Deref,
},
node_id: details.counterparty.node_id,
htlc_maximum_msat: details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX),
})
})
.map(|forward_node| {
};
Some((forward_node, counterparty_channels))
});

let scorer = self.scorer.read_lock();
let three_hop_paths = counterparty_channels.clone()
// Pair counterparties with their other channels
.flat_map(|(forward_node, counterparty_channels)|
counterparty_channels
.iter()
.filter_map(|scid| network_graph.channels().get_key_value(scid))
.filter_map(move |(scid, info)| info
.as_directed_to(&NodeId::from_pubkey(&forward_node.node_id))
.map(|(info, source)| (source, *scid, info))
)
.filter(|(source, _, _)| **source != recipient_node_id)
.filter(|(source, _, _)| network_graph
.node(source)
.and_then(|info| info.announcement_info.as_ref())
.map(|info| info.features().supports_route_blinding())
.unwrap_or(false)
)
.filter(|(_, _, info)| amount_msats >= info.direction().htlc_minimum_msat)
.filter(|(_, _, info)| amount_msats <= info.direction().htlc_maximum_msat)
.filter(|(_, scid, info)| {
scorer.channel_balance_certainty(*scid, info) >= MIN_CHANNEL_CERTAINTY
})
.map(move |(source, scid, info)| (source, scid, info, forward_node.clone()))
)
// Construct blinded paths where the counterparty's counterparty is the introduction
// node:
//
// source --- info ---> counterparty --- counterparty_forward_node ---> recipient
.filter_map(|(introduction_node_id, scid, info, counterparty_forward_node)| {
let amount_msat = amount_msats;
let effective_capacity = info.effective_capacity();
let usage = ChannelUsage { amount_msat, inflight_htlc_msat: 0, effective_capacity };
let success_probability = scorer.channel_success_probability(
scid, &info, usage, &self.score_params
);

if !success_probability.is_finite() {
return None;
}

if success_probability < MIN_SUCCESS_PROBABILITY {
return None;
}

let htlc_minimum_msat = info.direction().htlc_minimum_msat;
let htlc_maximum_msat = info.direction().htlc_maximum_msat;
let payment_relay: PaymentRelay = match info.try_into() {
Ok(payment_relay) => payment_relay,
Err(()) => return None,
};
let payment_constraints = PaymentConstraints {
max_cltv_expiry: payment_relay.cltv_expiry_delta as u32
+ counterparty_forward_node.tlvs.payment_constraints.max_cltv_expiry,
htlc_minimum_msat,
};
let introduction_forward_node = payment::ForwardNode {
tlvs: ForwardTlvs {
short_channel_id: scid,
payment_relay,
payment_constraints,
features: BlindedHopFeatures::empty(),
},
node_id: introduction_node_id.as_pubkey().unwrap(),
htlc_maximum_msat,
};
let path = BlindedPath::new_for_payment(
&[introduction_forward_node, counterparty_forward_node], recipient,
tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
);

Some(path.map(|path| (path, success_probability)))
});

let two_hop_paths = counterparty_channels
.map(|(forward_node, _)| {
BlindedPath::new_for_payment(
&[forward_node], recipient, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA,
&*self.entropy_source, secp_ctx
entropy_source, secp_ctx
)
})
.take(MAX_PAYMENT_PATHS)
.collect::<Result<Vec<_>, _>>();

match paths {
Ok(paths) if !paths.is_empty() => Ok(paths),
_ => {
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx
).map(|path| vec![path])
} else {
Err(())
}
},
}
.take(MAX_PAYMENT_PATHS);

three_hop_paths
.collect::<Result<Vec<_>, _>>().ok()
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.map(|mut paths| {
paths.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
paths.into_iter().map(|(path, _)| path).take(MAX_PAYMENT_PATHS).collect::<Vec<_>>()
})
.or_else(|| two_hop_paths.collect::<Result<Vec<_>, _>>().ok())
.and_then(|paths| (!paths.is_empty()).then(|| paths))
.or_else(|| network_graph
.node(&NodeId::from_pubkey(&recipient)).ok_or(())
.and_then(|_| BlindedPath::one_hop_for_payment(
recipient, tlvs, MIN_FINAL_CLTV_EXPIRY_DELTA, entropy_source, secp_ctx
)
)
.map(|path| vec![path])
.ok()
)
.ok_or(())
}
}

Expand Down
Loading