Assert that a balance under a post-splice reserve did not budge
What changed, and why it matters
This commit adds internal consistency checks (debug-only assertions) for the Lightning channel splicing logic. It verifies that if a party's balance after a splice would fall below the required channel reserve, that balance must not have changed from before the splice. In other words, a party cannot splice funds into a channel and end up with a balance below the new reserve. These checks only run in debug/test builds, so they do not by themselves prevent attacks in production releases.
Treat as a defensive hardening commit. Review whether the same invariant should be enforced as a runtime error rather than a debug-only assertion, especially for production splicing flows. Monitor for follow-up commits that convert the assert to a proper error path or add test coverage demonstrating the invariant.
Security signals we found
Adds debug assertions enforcing reserve compliance after splice
Concern: balance below reserve after splice could indicate protocol violation or implementation bug
Only active under debug_assertions, so no production runtime protection
No explicit security advisory, CVE, or incident disclosure supplied
Evidence from the diff
The patch modifies FundingScope::with_funding_transaction in lightning/src/ln/channel.rs. It refactors how counterparty_selected_channel_reserve_satoshis is computed and then adds conditional assert_eq! checks inside two #[cfg(debug_assertions)] blocks (holder_prev_commitment_tx_balance and counterparty_prev_commitment_tx_balance). For each side, after adding the splice contributions, if the new balance is below the other side’s selected reserve, the code asserts the new balance equals the previous balance. This is a defensive invariant check, not a runtime enforcement in release builds.
Changed components
lightning/src/ln/channel.rsFundingScope::with_funding_transactionChannel splicing / v2 channel reserve logicInspect captured patch +26 / −10
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 15aa1da..2bea5aa 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -2788,7 +2788,7 @@ impl FundingScope {
// New reserve values are based on the new channel value and are v2-specific
let counterparty_selected_channel_reserve_satoshis =
- Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS));
+ get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS);
let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis(
post_channel_value,
context.counterparty_dust_limit_satoshis,
@@ -2798,23 +2798,39 @@ impl FundingScope {
channel_transaction_parameters: post_channel_transaction_parameters,
value_to_self_msat: post_value_to_self_msat,
funding_transaction: None,
- counterparty_selected_channel_reserve_satoshis,
+ counterparty_selected_channel_reserve_satoshis: Some(
+ counterparty_selected_channel_reserve_satoshis,
+ ),
holder_selected_channel_reserve_satoshis,
#[cfg(debug_assertions)]
holder_prev_commitment_tx_balance: {
let prev = *prev_funding.holder_prev_commitment_tx_balance.lock().unwrap();
- Mutex::new((
- prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000),
- prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000),
- ))
+ let new_holder_balance_msat =
+ prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000);
+ let new_counterparty_balance_msat =
+ prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000);
+ if new_holder_balance_msat < counterparty_selected_channel_reserve_satoshis {
+ assert_eq!(new_holder_balance_msat, prev.0);
+ }
+ if new_counterparty_balance_msat < holder_selected_channel_reserve_satoshis {
+ assert_eq!(new_counterparty_balance_msat, prev.1);
+ }
+ Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat))
},
#[cfg(debug_assertions)]
counterparty_prev_commitment_tx_balance: {
let prev = *prev_funding.counterparty_prev_commitment_tx_balance.lock().unwrap();
- Mutex::new((
- prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000),
- prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000),
- ))
+ let new_holder_balance_msat =
+ prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000);
+ let new_counterparty_balance_msat =
+ prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000);
+ if new_holder_balance_msat < counterparty_selected_channel_reserve_satoshis {
+ assert_eq!(new_holder_balance_msat, prev.0);
+ }
+ if new_counterparty_balance_msat < holder_selected_channel_reserve_satoshis {
+ assert_eq!(new_counterparty_balance_msat, prev.1);
+ }
+ Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat))
},
#[cfg(any(test, fuzzing))]
next_local_fee: Mutex::new(PredictedNextFee::default()),
Why this scored 21/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.