Update Default Blinded Path constructor to use Dummy Hops
What changed, and why it matters
This change improves privacy in Lightning onion messages by adding fake 'dummy hops' to blinded paths by default, making it harder for outside observers to guess how far the real recipient is from the sender. It is a defensive privacy hardening patch, not a fix for an active exploit.
Review and merge as a privacy improvement. Ensure downstream consumers relying on exact blinded hop counts for non-compact paths are aware of the new fixed length. No urgent security response is indicated by the diff alone.
Security signals we found
Privacy hardening: pads blinded path length to obscure true recipient position
Replaces direct BlindedMessagePath::new with dummy-hop-aware constructor
Adds constant PADDED_PATH_LENGTH (4 hops) for non-compact paths
Compact paths explicitly excluded from padding
Includes debug_assert length sanity check
Evidence from the diff
The commit modifies DefaultMessageRouter in rust-lightning’s onion message messenger so that BlindedMessagePath construction uses BlindedMessagePath::new_with_dummy_hops with a fixed PADDED_PATH_LENGTH of 4 hops for non-compact paths. Compact paths remain unpadded. The change pads intermediate_hops plus the final recipient to a constant length, replacing direct calls to BlindedMessagePath::new. A debug_assert verifies the padded length. This reduces path-length side channels that could leak recipient proximity.
Changed components
lightning/src/onion_message/messenger.rsDefaultMessageRouterBlindedMessagePath constructionInspect captured patch +45 / −31
diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs
index bb8cbba..ede82b4 100644
--- a/lightning/src/onion_message/messenger.rs
+++ b/lightning/src/onion_message/messenger.rs
@@ -540,6 +540,14 @@ where
entropy_source: ES,
}
+// Target total length (in hops) for non-compact blinded paths.
+// We pad with dummy hops until the path reaches this length,
+// obscuring the recipient's true position.
+//
+// Compact paths are optimized for minimal size, so we avoid
+// adding dummy hops to them.
+pub(crate) const PADDED_PATH_LENGTH: usize = 4;
+
impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, ES: Deref> DefaultMessageRouter<G, L, ES>
where
L::Target: Logger,
@@ -595,40 +603,46 @@ where
a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse())
});
- let entropy = &**entropy_source;
- let paths = peer_info
+ let build_path = |intermediate_hops: &[MessageForwardNode]| {
+ let dummy_hops_count = if compact_paths {
+ 0
+ } else {
+ // Add one for the final recipient TLV
+ PADDED_PATH_LENGTH.saturating_sub(intermediate_hops.len() + 1)
+ };
+
+ BlindedMessagePath::new_with_dummy_hops(
+ intermediate_hops,
+ recipient,
+ dummy_hops_count,
+ local_node_receive_key,
+ context.clone(),
+ &**entropy_source,
+ secp_ctx,
+ )
+ };
+
+ // Try to create paths from peer info, fall back to direct path if needed
+ let mut paths = peer_info
.into_iter()
- .map(|(peer, _, _)| {
- BlindedMessagePath::new(
- &[peer],
- recipient,
- local_node_receive_key,
- context.clone(),
- entropy,
- secp_ctx,
- )
- })
+ .map(|(peer, _, _)| build_path(&[peer]))
.take(MAX_PATHS)
- .collect::<Result<Vec<_>, _>>();
-
- let mut paths = match paths {
- Ok(paths) if !paths.is_empty() => Ok(paths),
- _ => {
- if is_recipient_announced {
- BlindedMessagePath::new(
- &[],
- recipient,
- local_node_receive_key,
- context,
- &**entropy_source,
- secp_ctx,
- )
+ .collect::<Result<Vec<_>, _>>()
+ .ok()
+ .filter(|paths| !paths.is_empty())
+ .or_else(|| {
+ is_recipient_announced
+ .then(|| build_path(&[]))
+ .and_then(|result| result.ok())
.map(|path| vec![path])
- } else {
- Err(())
- }
- },
- }?;
+ })
+ .ok_or(())?;
+
+ // Sanity check: Ones the paths are created for the non-compact case, ensure
+ // each of them are of the length `PADDED_PATH_LENGTH`.
+ if !compact_paths {
+ debug_assert!(paths.iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH));
+ }
if compact_paths {
for path in &mut paths {
Why this scored 40/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.