Skip to content
Open
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
45 changes: 39 additions & 6 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2772,21 +2772,54 @@ impl FundingScope {
context.counterparty_dust_limit_satoshis,
);

// Account for in-flight HTLCs and anchor outputs when initializing the max
// commitment tx output trackers. Unlike a fresh channel open (which has no HTLCs),
// a splice may have pending HTLCs whose amounts are subtracted from the balances
// in the commitment transaction. Without this adjustment, the monotonicity debug
// assertion in `build_commitment_transaction` would fire on the first commitment.
#[cfg(debug_assertions)]
let (local_balance_msat, remote_balance_msat) = {
let pending_outbound_htlcs_value_msat: u64 =
context.pending_outbound_htlcs.iter().map(|h| h.amount_msat).sum();
let pending_inbound_htlcs_value_msat: u64 =
context.pending_inbound_htlcs.iter().map(|h| h.amount_msat).sum();
let channel_type = &post_channel_transaction_parameters.channel_type_features;
let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() {
ANCHOR_OUTPUT_VALUE_SATOSHI * 2
} else {
0
};
let post_value_to_remote_msat =
(post_channel_value * 1000).saturating_sub(post_value_to_self_msat);
if post_channel_transaction_parameters.is_outbound_from_holder {
(
post_value_to_self_msat
.saturating_sub(pending_outbound_htlcs_value_msat)
.saturating_sub(total_anchors_sat * 1000),
post_value_to_remote_msat.saturating_sub(pending_inbound_htlcs_value_msat),
)
} else {
(
post_value_to_self_msat.saturating_sub(pending_outbound_htlcs_value_msat),
post_value_to_remote_msat
.saturating_sub(pending_inbound_htlcs_value_msat)
.saturating_sub(total_anchors_sat * 1000),
)
}
};

Self {
channel_transaction_parameters: post_channel_transaction_parameters,
value_to_self_msat: post_value_to_self_msat,
funding_transaction: None,
counterparty_selected_channel_reserve_satoshis,
holder_selected_channel_reserve_satoshis,
#[cfg(debug_assertions)]
holder_max_commitment_tx_output: Mutex::new((
post_value_to_self_msat,
(post_channel_value * 1000).saturating_sub(post_value_to_self_msat),
)),
holder_max_commitment_tx_output: Mutex::new((local_balance_msat, remote_balance_msat)),
#[cfg(debug_assertions)]
counterparty_max_commitment_tx_output: Mutex::new((
post_value_to_self_msat,
(post_channel_value * 1000).saturating_sub(post_value_to_self_msat),
local_balance_msat,
remote_balance_msat,
)),
#[cfg(any(test, fuzzing))]
next_local_fee: Mutex::new(PredictedNextFee::default()),
Expand Down
59 changes: 59 additions & 0 deletions lightning/src/ln/splicing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2459,3 +2459,62 @@ fn test_splice_buffer_invalid_commitment_signed_closes_channel() {
);
check_added_monitors(&nodes[0], 1);
}

#[test]
fn test_splice_with_pending_htlcs() {
// Test that splicing works correctly when there are bidirectional pending HTLCs (both outbound
// and inbound). This exercises the debug logic in `build_commitment_transaction` where the
// `holder_max_commitment_tx_output` and `counterparty_max_commitment_tx_output` trackers must
// account for in-flight HTLCs and anchor costs when initializing a new splice funding scope.
// Without the fix, the monotonicity debug assertion would fire on the first commitment
// transaction built for the splice funding.
let chanmon_cfgs = create_chanmon_cfgs(2);
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
let mut config = test_default_channel_config();
config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);

let initial_channel_value_sat = 100_000;
// Push 10k sat to node 1 so it has balance to send HTLCs back.
let push_msat = 10_000_000;
let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(
&nodes,
0,
1,
initial_channel_value_sat,
push_msat,
);

let coinbase_tx = provide_anchor_reserves(&nodes);

// Create bidirectional pending HTLCs (routed but not claimed).
// Outbound HTLC from node 0 to node 1.
let (preimage_0_to_1, _hash_0_to_1, ..) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000);
// Large inbound HTLC from node 1 to node 0, bringing node 1's remaining balance down to
// 2000 sat. The old reserve (1% of 100k) is 1000 sat so this is still above reserve.
let (preimage_1_to_0, _hash_1_to_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 8_000_000);

// Splice-in 200k sat. The new channel value becomes 300k sat, raising the reserve to 3000
// sat. Node 1's remaining 2000 sat is now below the new reserve, which means the debug
// assertion's `balance / 1000 >= reserve` fallback (3000 > 2000) cannot mask a broken
// tracker initialization.
let initiator_contribution = SpliceContribution::splice_in(
Amount::from_sat(200_000),
vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
Some(nodes[0].wallet_source.get_change_script().unwrap()),
);
let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution);

// Confirm and lock the splice.
mine_transaction(&nodes[0], &splice_tx);
mine_transaction(&nodes[1], &splice_tx);
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);

// Claim both pending HTLCs to verify the channel is fully functional after the splice.
claim_payment(&nodes[0], &[&nodes[1]], preimage_0_to_1);
claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0);

// Final sanity check: send a payment using the new spliced capacity.
let _ = send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
}