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
41 changes: 28 additions & 13 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2779,15 +2779,21 @@ impl FundingScope {
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: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: mind remaining all these to *_prev_* ? We don't track the max anymore, we track the previous.

let prev = *prev_funding.holder_max_commitment_tx_output.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),
Comment on lines +2785 to +2786
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below, should we do checked and unwrap ? These are debug assertions, and if these things saturate, that's a bad bug I'm thinking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can without also persisting the fields, they could trigger after a node reload.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you yes we'd trigger a panic if we immediately splice out after a restart IIUC. We can keep the saturations then.

))
},
#[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),
)),
counterparty_max_commitment_tx_output: {
let prev = *prev_funding.counterparty_max_commitment_tx_output.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),
))
},
#[cfg(any(test, fuzzing))]
next_local_fee: Mutex::new(PredictedNextFee::default()),
#[cfg(any(test, fuzzing))]
Expand Down Expand Up @@ -5511,12 +5517,21 @@ impl<SP: SignerProvider> ChannelContext<SP> {
} else {
funding.counterparty_max_commitment_tx_output.lock().unwrap()
};
debug_assert!(broadcaster_max_commitment_tx_output.0 <= stats.local_balance_before_fee_msat || stats.local_balance_before_fee_msat / 1000 >= funding.counterparty_selected_channel_reserve_satoshis.unwrap());
broadcaster_max_commitment_tx_output.0 = cmp::max(broadcaster_max_commitment_tx_output.0, stats.local_balance_before_fee_msat);
debug_assert!(broadcaster_max_commitment_tx_output.1 <= stats.remote_balance_before_fee_msat || stats.remote_balance_before_fee_msat / 1000 >= funding.holder_selected_channel_reserve_satoshis);
broadcaster_max_commitment_tx_output.1 = cmp::max(broadcaster_max_commitment_tx_output.1, stats.remote_balance_before_fee_msat);
}

if stats.local_balance_before_fee_msat / 1000 < funding.counterparty_selected_channel_reserve_satoshis.unwrap() {
// If the local balance is below the reserve on this new commitment, it MUST be
// greater than or equal to the one on the previous commitment.
debug_assert!(broadcaster_max_commitment_tx_output.0 <= stats.local_balance_before_fee_msat);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above let's call this broadcaster_prev_* ?

}
broadcaster_max_commitment_tx_output.0 = stats.local_balance_before_fee_msat;

if stats.remote_balance_before_fee_msat / 1000 < funding.holder_selected_channel_reserve_satoshis {
// If the remote balance is below the reserve on this new commitment, it MUST be
// greater than or equal to the one on the previous commitment.
debug_assert!(broadcaster_max_commitment_tx_output.1 <= stats.remote_balance_before_fee_msat);
}
broadcaster_max_commitment_tx_output.1 = stats.remote_balance_before_fee_msat;
}

// This populates the HTLC-source table with the indices from the HTLCs in the commitment
// transaction.
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems strange for the "test_splice_with_pending_htlcs" test to not fail if I break get_holder_counterparty_balances_floor_incl_fee. Maybe test_balance_falls_below_reserve ?

I can add coverage for get_holder_counterparty_balances_floor_incl_fee in a follow-up.

// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as elsewhere, use prev instead of max

// 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);
}