-
Notifications
You must be signed in to change notification settings - Fork 441
Account for missing balance in splice max commitment output tracking #4417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: { | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and below, should we do
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))] | ||
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above let's call this |
||
| } | ||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I can add coverage for |
||
| // 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: as elsewhere, use |
||
| // 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); | ||
| } | ||
There was a problem hiding this comment.
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.