Skip to content

Commit 9c0de90

Browse files
shaavancodex
andcommitted
Use realistic dummy-hop defaults in blinded path admission
Replace the zeroed DummyTlvs defaults with stable relay-like fee, CLTV, and HTLC minimum values, and reuse the same minimum aggregation logic in both blinded payinfo construction and router-created blinded path admission. Dummy hops only provide cover if they resemble plausible forwarding hops. Once those defaults become non-trivial, the router also has to reject candidate blinded paths whose requested amount falls below the minimum implied by the hidden tail; otherwise path admission and advertised payinfo can diverge. Document the default constants in code, share the helper that folds downstream minimums backward through relay fees, and add router coverage for the under-minimum admission case. Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 0c0f741 commit 9c0de90

3 files changed

Lines changed: 172 additions & 30 deletions

File tree

lightning/src/blinded_path/payment.rs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -392,15 +392,57 @@ pub struct DummyTlvs {
392392
pub payment_constraints: PaymentConstraints,
393393
}
394394

395-
impl Default for DummyTlvs {
396-
fn default() -> Self {
397-
let payment_relay =
398-
PaymentRelay { cltv_expiry_delta: 0, fee_proportional_millionths: 0, fee_base_msat: 0 };
395+
// Default parameters used for dummy hops in blinded paths.
396+
//
397+
// These values are chosen to resemble typical forwarding hops while remaining
398+
// stable and predictable for tests.
399+
400+
/// Adds a realistic but stable CLTV cost per dummy hop.
401+
///
402+
/// The router folds this into the blinded path's advertised CLTV delta, so it must
403+
/// be non-trivial enough to model hidden relay latency while remaining predictable
404+
/// for tests and callers reasoning about timeout budgets.
405+
pub(crate) const DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA: u16 = 80;
406+
407+
/// Keeps dummy-hop fee aggregation linear and deterministic.
408+
///
409+
/// A non-zero proportional fee would compound across dummy hops and introduce rounding
410+
/// effects into blinded payinfo. The base fee still makes dummy hops look like plausible relays.
411+
pub(crate) const DEFAULT_DUMMY_HOP_FEE_PROPORTIONAL_MILLIONTHS: u32 = 0;
412+
413+
/// Matches the default relay base fee used by the standard test channel configuration.
414+
///
415+
/// This keeps dummy hops aligned with typical forwarding hops in tests rather than
416+
/// making them appear unrealistically cheap or expensive.
417+
pub(crate) const DEFAULT_DUMMY_HOP_FEE_BASE_MSAT: u32 = 1000;
399418

400-
let payment_constraints =
401-
PaymentConstraints { max_cltv_expiry: u32::MAX, htlc_minimum_msat: 0 };
419+
/// Leaves the dummy hop's absolute CLTV ceiling effectively unbounded by default.
420+
///
421+
/// `PaymentConstraints::max_cltv_expiry` is interpreted as an absolute block height, so using a
422+
/// fixed low value here would cause dummy hops to reject otherwise-valid payments on live chains.
423+
pub(crate) const DEFAULT_DUMMY_HOP_MAX_CLTV_EXPIRY: u32 = u32::MAX;
424+
425+
/// Matches the default test channel HTLC minimum.
426+
///
427+
/// The router takes the max of the introduction node's inbound HTLC minimum and this value,
428+
/// so keeping them aligned prevents dummy hops from unexpectedly tightening or loosening
429+
/// admission.
430+
pub(crate) const DEFAULT_DUMMY_HOP_HTLC_MINIMUM_MSAT: u64 = 1000;
402431

403-
Self { payment_relay, payment_constraints }
432+
impl Default for DummyTlvs {
433+
/// Returns the documented default relay requirements and constraints for synthetic hops.
434+
fn default() -> Self {
435+
Self {
436+
payment_relay: PaymentRelay {
437+
cltv_expiry_delta: DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA,
438+
fee_proportional_millionths: DEFAULT_DUMMY_HOP_FEE_PROPORTIONAL_MILLIONTHS,
439+
fee_base_msat: DEFAULT_DUMMY_HOP_FEE_BASE_MSAT,
440+
},
441+
payment_constraints: PaymentConstraints {
442+
max_cltv_expiry: DEFAULT_DUMMY_HOP_MAX_CLTV_EXPIRY,
443+
htlc_minimum_msat: DEFAULT_DUMMY_HOP_HTLC_MINIMUM_MSAT,
444+
},
445+
}
404446
}
405447
}
406448

@@ -791,6 +833,20 @@ pub(crate) fn amt_to_forward_msat(
791833
u64::try_from(amt_to_forward).ok()
792834
}
793835

836+
pub(crate) fn compute_next_htlc_minimum_msat(
837+
htlc_minimum_msat: u64, payment_constraints: &PaymentConstraints, payment_relay: &PaymentRelay,
838+
) -> u64 {
839+
// Fold the next hop's minimum backwards through its relay fees to recover the minimum amount the
840+
// previous hop must receive.
841+
amt_to_forward_msat(
842+
core::cmp::max(payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
843+
payment_relay,
844+
)
845+
// If reversing the fee calculation would require less than 1 msat, clamp to 1 since any
846+
// positive incoming amount already satisfies the next hop's minimum once fees are accounted for.
847+
.unwrap_or(1)
848+
}
849+
794850
// Returns (aggregated_base_fee, aggregated_proportional_fee)
795851
pub(crate) fn compute_aggregated_base_prop_fee<I>(hops_fees: I) -> Result<(u64, u64), ()>
796852
where
@@ -854,14 +910,14 @@ pub(super) fn compute_payinfo(
854910
cltv_expiry_delta =
855911
cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
856912

857-
// The min htlc for an intermediate node is that node's min minus the fees charged by all of the
858-
// following hops for forwarding that min, since that fee amount will automatically be included
859-
// in the amount that this node receives and contribute towards reaching its min.
860-
htlc_minimum_msat = amt_to_forward_msat(
861-
core::cmp::max(node.tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
913+
// The min htlc for an intermediate node is that node's min minus the fees charged by all of
914+
// the following hops for forwarding that min, since that fee amount will automatically be
915+
// included in the amount that this node receives and contribute towards reaching its min.
916+
htlc_minimum_msat = compute_next_htlc_minimum_msat(
917+
htlc_minimum_msat,
918+
&node.tlvs.payment_constraints,
862919
&node.tlvs.payment_relay,
863-
)
864-
.unwrap_or(1); // If underflow occurs, we definitely reached this node's min
920+
);
865921
htlc_maximum_msat = amt_to_forward_msat(
866922
core::cmp::min(node.htlc_maximum_msat, htlc_maximum_msat),
867923
&node.tlvs.payment_relay,
@@ -872,11 +928,11 @@ pub(super) fn compute_payinfo(
872928
cltv_expiry_delta =
873929
cltv_expiry_delta.checked_add(dummy_tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
874930

875-
htlc_minimum_msat = amt_to_forward_msat(
876-
core::cmp::max(dummy_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
931+
htlc_minimum_msat = compute_next_htlc_minimum_msat(
932+
htlc_minimum_msat,
933+
&dummy_tlvs.payment_constraints,
877934
&dummy_tlvs.payment_relay,
878-
)
879-
.unwrap_or(1); // If underflow occurs, we definitely reached this node's min
935+
);
880936
}
881937
htlc_minimum_msat =
882938
core::cmp::max(payee_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat);

lightning/src/ln/async_payments_tests.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use crate::blinded_path::message::{
1111
BlindedMessagePath, MessageContext, NextMessageHop, OffersContext,
1212
};
1313
use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs};
14-
use crate::blinded_path::payment::{DummyTlvs, PaymentContext};
14+
use crate::blinded_path::payment::{
15+
DummyTlvs, PaymentContext, DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA,
16+
};
1517
use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
1618
use crate::events::{
1719
Event, EventsProvider, HTLCHandlingFailureReason, HTLCHandlingFailureType, PaidBolt12Invoice,
@@ -3034,11 +3036,16 @@ fn held_htlc_timeout() {
30343036
// Extract the release_htlc_om, but don't deliver it to the sender's LSP.
30353037
let _ = extract_release_htlc_oms(recipient, &[sender, sender_lsp, invoice_server]);
30363038

3039+
// Dummy hops add to the blinded path's total advertised CLTV delta.
3040+
let additional_cltv_expiry =
3041+
DEFAULT_PAYMENT_DUMMY_HOPS as u32 * DEFAULT_DUMMY_HOP_CLTV_EXPIRY_DELTA as u32;
3042+
30373043
// Connect blocks to the sender's LSP until they timeout the HTLC.
30383044
connect_blocks(
30393045
sender_lsp,
30403046
MIN_CLTV_EXPIRY_DELTA as u32
30413047
+ TEST_FINAL_CLTV
3048+
+ additional_cltv_expiry
30423049
+ HTLC_FAIL_BACK_BUFFER
30433050
+ LATENCY_GRACE_PERIOD_BLOCKS,
30443051
);

lightning/src/routing/router.rs

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1};
1313
use lightning_invoice::Bolt11Invoice;
1414

1515
use crate::blinded_path::payment::{
16-
BlindedPaymentPath, DummyTlvs, ForwardTlvs, PaymentConstraints, PaymentForwardNode,
17-
PaymentRelay, ReceiveTlvs,
16+
compute_next_htlc_minimum_msat, BlindedPaymentPath, DummyTlvs, ForwardTlvs, PaymentConstraints,
17+
PaymentForwardNode, PaymentRelay, ReceiveTlvs,
1818
};
1919
use crate::blinded_path::{BlindedHop, Direction, IntroductionNode};
2020
use crate::crypto::chacha20::ChaCha20;
@@ -150,6 +150,7 @@ where
150150
let network_graph = self.network_graph.deref().read_only();
151151
let is_recipient_announced =
152152
network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient));
153+
let dummy_tlvs = DummyTlvs::default();
153154

154155
let paths = first_hops.into_iter()
155156
.filter(|details| details.counterparty.features.supports_route_blinding())
@@ -176,12 +177,32 @@ where
176177
None => return None,
177178
};
178179

179-
let cltv_expiry_delta = payment_relay.cltv_expiry_delta as u32;
180+
let cltv_expiry_delta = payment_relay.cltv_expiry_delta as u32
181+
+ dummy_tlvs.payment_relay.cltv_expiry_delta as u32 * DEFAULT_PAYMENT_DUMMY_HOPS as u32;
182+
let dummy_hops_htlc_minimum_msat = [dummy_tlvs; DEFAULT_PAYMENT_DUMMY_HOPS]
183+
.iter()
184+
.fold(1, |htlc_minimum_msat, dummy_tlvs| {
185+
compute_next_htlc_minimum_msat(
186+
htlc_minimum_msat,
187+
&dummy_tlvs.payment_constraints,
188+
&dummy_tlvs.payment_relay,
189+
)
190+
});
191+
let htlc_minimum_msat = cmp::max(
192+
details.inbound_htlc_minimum_msat.unwrap_or(0),
193+
cmp::max(
194+
tlvs.payment_constraints.htlc_minimum_msat,
195+
dummy_hops_htlc_minimum_msat,
196+
),
197+
);
198+
if amount_msats.unwrap_or(u64::MAX) < htlc_minimum_msat {
199+
return None;
200+
}
180201
let payment_constraints = PaymentConstraints {
181202
max_cltv_expiry: tlvs.payment_constraints
182203
.max_cltv_expiry
183204
.saturating_add(cltv_expiry_delta),
184-
htlc_minimum_msat: details.inbound_htlc_minimum_msat.unwrap_or(0),
205+
htlc_minimum_msat,
185206
};
186207
Some(PaymentForwardNode {
187208
tlvs: ForwardTlvs {
@@ -197,7 +218,7 @@ where
197218
})
198219
.map(|forward_node| {
199220
BlindedPaymentPath::new_with_dummy_hops(
200-
&[forward_node], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS],
221+
&[forward_node], recipient, &[dummy_tlvs; DEFAULT_PAYMENT_DUMMY_HOPS],
201222
local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &self.entropy_source, secp_ctx
202223
)
203224
})
@@ -209,7 +230,7 @@ where
209230
_ => {
210231
if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) {
211232
BlindedPaymentPath::new_with_dummy_hops(
212-
&[], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS],
233+
&[], recipient, &[dummy_tlvs; DEFAULT_PAYMENT_DUMMY_HOPS],
213234
local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &self.entropy_source, secp_ctx
214235
).map(|path| vec![path])
215236
} else {
@@ -4077,21 +4098,26 @@ fn build_route_from_hops_internal<L: Logger>(
40774098

40784099
#[cfg(test)]
40794100
mod tests {
4080-
use crate::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath};
4101+
use crate::blinded_path::payment::{
4102+
BlindedPayInfo, BlindedPaymentPath, Bolt12RefundContext, PaymentConstraints,
4103+
PaymentContext, ReceiveTlvs,
4104+
};
40814105
use crate::blinded_path::BlindedHop;
40824106
use crate::chain::transaction::OutPoint;
40834107
use crate::crypto::chacha20::ChaCha20;
40844108
use crate::ln::chan_utils::make_funding_redeemscript;
4085-
use crate::ln::channel_state::{ChannelCounterparty, ChannelDetails, ChannelShutdownState};
4109+
use crate::ln::channel_state::{
4110+
ChannelCounterparty, ChannelDetails, ChannelShutdownState, CounterpartyForwardingInfo,
4111+
};
40864112
use crate::ln::channelmanager;
40874113
use crate::ln::msgs::{UnsignedChannelUpdate, MAX_VALUE_MSAT};
40884114
use crate::ln::types::ChannelId;
40894115
use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, P2PGossipSync};
40904116
use crate::routing::router::{
40914117
add_random_cltv_offset, build_route_from_hops_internal, default_node_features, get_route,
4092-
BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path,
4118+
BlindedPathCandidate, BlindedTail, CandidateRouteHop, DefaultRouter, InFlightHtlcs, Path,
40934119
PaymentParameters, PublicHopCandidate, Route, RouteHint, RouteHintHop, RouteHop,
4094-
RouteParameters, RoutingFees, ScorerAccountingForInFlightHtlcs,
4120+
RouteParameters, Router, RoutingFees, ScorerAccountingForInFlightHtlcs,
40954121
DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE,
40964122
};
40974123
use crate::routing::scoring::{
@@ -4101,6 +4127,7 @@ mod tests {
41014127
use crate::routing::test_utils::*;
41024128
use crate::routing::utxo::UtxoResult;
41034129
use crate::types::features::{BlindedHopFeatures, ChannelFeatures, InitFeatures, NodeFeatures};
4130+
use crate::types::payment::PaymentSecret;
41044131
use crate::util::config::UserConfig;
41054132
#[cfg(c_bindings)]
41064133
use crate::util::ser::Writer;
@@ -4120,7 +4147,8 @@ mod tests {
41204147

41214148
use crate::io::Cursor;
41224149
use crate::prelude::*;
4123-
use crate::sync::{Arc, Mutex};
4150+
use crate::sign::{RandomBytes, ReceiveAuthKey};
4151+
use crate::sync::{Arc, Mutex, RwLock};
41244152

41254153
#[rustfmt::skip]
41264154
fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
@@ -8980,6 +9008,57 @@ mod tests {
89809008
} else { panic!() }
89819009
}
89829010

9011+
#[test]
9012+
fn blinded_path_creation_respects_dummy_tail_htlc_minimum() {
9013+
let secp_ctx = Secp256k1::new();
9014+
let logger = Arc::new(ln_test_utils::TestLogger::new());
9015+
let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger)));
9016+
let scorer = Arc::new(RwLock::new(ln_test_utils::TestScorer::new()));
9017+
let router = DefaultRouter::new(
9018+
Arc::clone(&network_graph),
9019+
Arc::clone(&logger),
9020+
Arc::new(RandomBytes::new([42; 32])),
9021+
Arc::clone(&scorer),
9022+
Default::default(),
9023+
);
9024+
let config = UserConfig::default();
9025+
9026+
let mut first_hop = get_channel_details(
9027+
Some(42),
9028+
ln_test_utils::pubkey(43),
9029+
channelmanager::provided_init_features(&config),
9030+
1_000_000,
9031+
);
9032+
first_hop.inbound_capacity_msat = 1_000_000;
9033+
first_hop.inbound_htlc_minimum_msat = Some(1_000);
9034+
first_hop.inbound_htlc_maximum_msat = Some(1_000_000);
9035+
first_hop.counterparty.forwarding_info = Some(CounterpartyForwardingInfo {
9036+
fee_base_msat: 1_000,
9037+
fee_proportional_millionths: 0,
9038+
cltv_expiry_delta: 18,
9039+
});
9040+
9041+
let tlvs = ReceiveTlvs {
9042+
payment_secret: PaymentSecret([0; 32]),
9043+
payment_constraints: PaymentConstraints {
9044+
max_cltv_expiry: u32::MAX,
9045+
htlc_minimum_msat: 2_500,
9046+
},
9047+
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
9048+
};
9049+
9050+
assert!(router
9051+
.create_blinded_payment_paths(
9052+
ln_test_utils::pubkey(44),
9053+
ReceiveAuthKey([41; 32]),
9054+
vec![first_hop],
9055+
tlvs,
9056+
Some(2_000),
9057+
&secp_ctx,
9058+
)
9059+
.is_err());
9060+
}
9061+
89839062
#[test]
89849063
#[rustfmt::skip]
89859064
fn path_contribution_includes_min_htlc_overpay() {

0 commit comments

Comments
 (0)