Skip to content

Commit 048e3e1

Browse files
joostjagerclaude
andcommitted
chanmon_consistency: verify claimable balances after settlement
After the 0xff settlement completes with force-closed channels, check get_claimable_balances() on all chain monitors. Assert that no ClaimableOnChannelClose balances remain, since those indicate the monitor still considers a channel open when it should be closed. This catches state machine bugs where the force-close state transition is not properly reflected in balance tracking. Other balance types (ClaimableAwaitingConfirmations, MaybeTimeoutClaimableHTLC, etc.) are logged but not asserted empty, since anchor channel HTLC resolution is not yet fully handled (the BumpTransaction events are currently dropped). The check passes open channels via the ignored_channels parameter so that only closed channel balances are examined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d21a356 commit 048e3e1

1 file changed

Lines changed: 38 additions & 1 deletion

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use lightning::chain;
4141
use lightning::chain::chaininterface::{
4242
BroadcasterInterface, ConfirmationTarget, FeeEstimator, TransactionType,
4343
};
44-
use lightning::chain::channelmonitor::{ChannelMonitor, MonitorEvent};
44+
use lightning::chain::channelmonitor::{Balance, ChannelMonitor, MonitorEvent};
4545
use lightning::chain::transaction::OutPoint;
4646
use lightning::chain::{
4747
chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch,
@@ -2989,6 +2989,43 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(
29892989
);
29902990
}
29912991

2992+
// After settlement, verify that closed channels have no
2993+
// ClaimableOnChannelClose balances (which would indicate the
2994+
// monitor still thinks the channel is open).
2995+
if !closed_channels.borrow().is_empty() {
2996+
let open_channels = nodes[0]
2997+
.list_channels()
2998+
.iter()
2999+
.chain(nodes[1].list_channels().iter())
3000+
.chain(nodes[2].list_channels().iter())
3001+
.map(|c| c.clone())
3002+
.collect::<Vec<_>>();
3003+
let open_refs: Vec<&_> = open_channels.iter().collect();
3004+
for (label, monitor) in
3005+
[("A", &monitor_a), ("B", &monitor_b), ("C", &monitor_c)]
3006+
{
3007+
let balances = monitor.chain_monitor.get_claimable_balances(&open_refs);
3008+
for balance in &balances {
3009+
if matches!(balance, Balance::ClaimableOnChannelClose { .. }) {
3010+
panic!(
3011+
"Monitor {} has ClaimableOnChannelClose balance after settlement: {:?}",
3012+
label, balance
3013+
);
3014+
}
3015+
}
3016+
if !balances.is_empty() {
3017+
out.locked_write(
3018+
format!(
3019+
"Monitor {} has {} remaining balances after settlement.\n",
3020+
label,
3021+
balances.len()
3022+
)
3023+
.as_bytes(),
3024+
);
3025+
}
3026+
}
3027+
}
3028+
29923029
last_htlc_clear_fee_a = fee_est_a.ret_val.load(atomic::Ordering::Acquire);
29933030
last_htlc_clear_fee_b = fee_est_b.ret_val.load(atomic::Ordering::Acquire);
29943031
last_htlc_clear_fee_c = fee_est_c.ret_val.load(atomic::Ordering::Acquire);

0 commit comments

Comments
 (0)