Introduce Dummy Hop support in Blinded Path Constructor
What changed, and why it matters
This commit adds a new privacy feature to rust-lightning's blinded message paths. It lets callers insert up to 10 fake 'dummy hops' before the real destination, making it harder for outside observers to guess how far apart sender and receiver are or to identify the final recipient. The change is additive: the old constructor still exists and now delegates to the new one with zero dummy hops. There is no obvious security bug in the patch itself.
No immediate security action required. Treat as a normal feature addition. If reviewing further, verify that dummy hops are indistinguishable from real forwarding hops to observers and that the 10-hop cap aligns with protocol limits and DoS bounds.
Security signals we found
New privacy-oriented API for padding blinded paths with dummy hops
Hard cap of 10 dummy hops to limit path length
Old constructor retained for backward compatibility
No validation changes to cryptographic blinding logic visible in diff
No mention of security fixes, CVEs, or vulnerability disclosure in commit message
Evidence from the diff
The patch introduces BlindedMessagePath::new_with_dummy_hops, which forwards to blinded_hops with a new dummy_hop_count parameter. blinded_hops caps dummy_hop_count at MAX_DUMMY_HOPS_COUNT (10), appends that many dummy hops using the recipient’s node ID and receive key, and emits ControlTlvs::Dummy entries for them. The existing public constructor new() is preserved as a thin wrapper passing 0 dummy hops. The change is purely additive and appears intended to improve sender/receiver privacy by padding blinded onion-message paths.
Changed components
lightning/src/blinded_path/message.rsBlindedMessagePath construction APIblinded_hops helperInspect captured patch +37 / −3
diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs
index 5f62cf8..8105bb2 100644
--- a/lightning/src/blinded_path/message.rs
+++ b/lightning/src/blinded_path/message.rs
@@ -31,9 +31,9 @@ use crate::types::payment::PaymentHash;
use crate::util::scid_utils;
use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer};
-use core::mem;
use core::ops::Deref;
use core::time::Duration;
+use core::{cmp, mem};
/// A blinded path to be used for sending or receiving a message, hiding the identity of the
/// recipient.
@@ -74,6 +74,29 @@ impl BlindedMessagePath {
local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()>
+ where
+ ES::Target: EntropySource,
+ {
+ BlindedMessagePath::new_with_dummy_hops(
+ intermediate_nodes,
+ recipient_node_id,
+ 0,
+ local_node_receive_key,
+ context,
+ entropy_source,
+ secp_ctx,
+ )
+ }
+
+ /// Same as [`BlindedMessagePath::new`], but allows specifying a number of dummy hops.
+ ///
+ /// Note:
+ /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path.
+ pub fn new_with_dummy_hops<ES: Deref, T: secp256k1::Signing + secp256k1::Verification>(
+ intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey,
+ dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext,
+ entropy_source: ES, secp_ctx: &Secp256k1<T>,
+ ) -> Result<Self, ()>
where
ES::Target: EntropySource,
{
@@ -91,6 +114,7 @@ impl BlindedMessagePath {
secp_ctx,
intermediate_nodes,
recipient_node_id,
+ dummy_hop_count,
context,
&blinding_secret,
local_node_receive_key,
@@ -635,15 +659,24 @@ impl_writeable_tlv_based!(DNSResolverContext, {
/// to pad message blinded path's [`BlindedHop`]
pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100;
+/// The maximum number of dummy hops that can be added to a blinded path.
+/// This is to prevent paths from becoming too long and potentially causing
+/// issues with message processing or routing.
+pub const MAX_DUMMY_HOPS_COUNT: usize = 10;
+
/// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`.
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[MessageForwardNode],
- recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey,
- local_node_receive_key: ReceiveAuthKey,
+ recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext,
+ session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey,
) -> Result<Vec<BlindedHop>, secp256k1::Error> {
+ let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT);
let pks = intermediate_nodes
.iter()
.map(|node| (node.node_id, None))
+ .chain(
+ core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count),
+ )
.chain(core::iter::once((recipient_node_id, Some(local_node_receive_key))));
let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some());
@@ -658,6 +691,7 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
.map(|next_hop| {
ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None })
})
+ .chain((0..dummy_count).map(|_| ControlTlvs::Dummy))
.chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) })));
if is_compact {
Why this scored 19/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.