Skip to content

Commit 36fd4f5

Browse files
committed
fuzz: factor chanmon consistency cleanup helpers
Extract chain connection and finish-time relay helper boundaries. Also isolate splice quiescence warning detection without changing behavior.
1 parent 952e0cb commit 36fd4f5

1 file changed

Lines changed: 58 additions & 28 deletions

File tree

fuzz/src/chanmon_consistency.rs

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ const NUM_WALLET_UTXOS: u32 = 50;
113113
// boundaries. Mining commands are capped in `safe_mine_block_count` if
114114
// unresolved HTLCs are near expiry.
115115
const MINE_BLOCK_COUNTS: [u32; 8] = [1, 2, 3, 6, 12, 24, 48, 144];
116-
// Finish-time relay/mining rounds are capped so cleanup cannot spin forever.
117-
const MAX_FINISH_RELAY_MINE_ROUNDS: usize = 32;
116+
// Progress loops are capped so cleanup can drive realistic asynchronous
117+
// transaction work without letting a malformed state spin forever.
118+
const QUIESCENCE_ROUNDS: usize = 32;
118119

119120
struct FuzzEstimator {
120121
ret_val: atomic::AtomicU32,
@@ -402,10 +403,24 @@ impl ChainState {
402403
self.pending_txs.push((txid, tx));
403404
}
404405

405-
fn relay_transactions(&mut self, txs: Vec<Transaction>) {
406+
// Feeds broadcast transactions through modeled mempool admission. We need
407+
// this on ChainState so propagation and confirmation share one owner for
408+
// duplicate, locktime, input, and RBF rules. The return value reports
409+
// whether any broadcasts were drained, even if admission later ignores a
410+
// duplicate or invalid transaction.
411+
fn relay_transactions(&mut self, txs: Vec<Transaction>) -> bool {
412+
let found = !txs.is_empty();
406413
for tx in txs {
407414
self.admit_tx_to_mempool(tx);
408415
}
416+
found
417+
}
418+
419+
// Reports whether the modeled mempool is non-empty. Fuzz mining bytes and
420+
// cleanup loops use this to decide whether another mining pass can make
421+
// progress.
422+
fn has_pending_txs(&self) -> bool {
423+
!self.pending_txs.is_empty()
409424
}
410425

411426
// Mines `count` blocks, confirming the current mempool in the first block.
@@ -934,12 +949,17 @@ type ChanMan<'a> = ChannelManager<
934949
Arc<dyn Logger + MaybeSend + MaybeSync>,
935950
>;
936951

952+
#[inline]
953+
fn is_quiescent_disconnect_warning(msg: &msgs::WarningMessage) -> bool {
954+
msg.data.contains("already sent splice_locked, cannot RBF")
955+
}
956+
937957
#[inline]
938958
fn assert_disconnect_action(action: &msgs::ErrorAction) -> (&msgs::WarningMessage, bool) {
939959
// Since sending/receiving messages may be delayed, `timer_tick_occurred` may cause a node to
940960
// disconnect their counterparty if they're expecting a timely response.
941961
if let msgs::ErrorAction::DisconnectPeerWithWarning { ref msg } = action {
942-
let is_quiescent_msg = msg.data.contains("already sent splice_locked, cannot RBF");
962+
let is_quiescent_msg = is_quiescent_disconnect_warning(msg);
943963
if !msg.data.contains("Disconnecting due to timeout awaiting response") && !is_quiescent_msg
944964
{
945965
panic!("Unexpected disconnect case: {}", msg.data);
@@ -2611,30 +2631,7 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
26112631
// Final invariants should not depend on the input ending with explicit relay
26122632
// and mining bytes.
26132633
fn finish(&mut self) {
2614-
for _ in 0..MAX_FINISH_RELAY_MINE_ROUNDS {
2615-
let mut txs = Vec::new();
2616-
for node in &self.nodes {
2617-
txs.extend(node.broadcaster.txn_broadcasted.borrow_mut().drain(..));
2618-
}
2619-
self.chain_state.relay_transactions(txs);
2620-
if self.chain_state.pending_txs.is_empty() {
2621-
assert_test_invariants(&self.nodes);
2622-
return;
2623-
}
2624-
if self.mine_blocks(ANTI_REORG_DELAY) == 0 {
2625-
// The input ended with pending mempool transactions but no safe
2626-
// block left before an HTLC fail-back window. Leave them
2627-
// unconfirmed rather than forcing finish cleanup to advance
2628-
// the chain past that boundary.
2629-
assert_test_invariants(&self.nodes);
2630-
return;
2631-
}
2632-
}
2633-
assert!(
2634-
!self.nodes.iter().any(|node| !node.broadcaster.txn_broadcasted.borrow().is_empty())
2635-
&& self.chain_state.pending_txs.is_empty(),
2636-
"finish tx mining loop failed to quiesce",
2637-
);
2634+
self.mine_relayed_txs_until_quiet("finish");
26382635
assert_test_invariants(&self.nodes);
26392636
}
26402637

@@ -3358,6 +3355,18 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
33583355
self.chain_state.relay_transactions(txs);
33593356
}
33603357

3358+
// Relays every node's pending broadcasts into the modeled mempool. Cleanup
3359+
// uses this when it should not depend on which peer fuzz bytes propagate.
3360+
// The bool reports whether any broadcasts were drained, not whether they
3361+
// were all admitted.
3362+
fn relay_all_broadcasts(&mut self) -> bool {
3363+
let mut txs = Vec::new();
3364+
for node in &self.nodes {
3365+
txs.extend(node.broadcaster.txn_broadcasted.borrow_mut().drain(..));
3366+
}
3367+
self.chain_state.relay_transactions(txs)
3368+
}
3369+
33613370
fn earliest_pending_htlc_expiry(&self) -> Option<u32> {
33623371
let mut earliest_expiry: Option<u32> = None;
33633372
for node in &self.nodes {
@@ -3441,6 +3450,27 @@ impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> {
34413450
}
34423451
count
34433452
}
3453+
3454+
// Repeatedly relays broadcasts and mines pending transactions to depth. We
3455+
// need this for finish paths where confirmed transactions may broadcast
3456+
// child transactions that also need confirmation.
3457+
fn mine_relayed_txs_until_quiet(&mut self, context: &str) {
3458+
for _ in 0..QUIESCENCE_ROUNDS {
3459+
self.relay_all_broadcasts();
3460+
if !self.chain_state.has_pending_txs() {
3461+
return;
3462+
}
3463+
assert!(
3464+
self.mine_blocks(ANTI_REORG_DELAY) > 0,
3465+
"{context} cannot mine pending mempool transactions without crossing an unresolved HTLC timeout deadline"
3466+
);
3467+
}
3468+
assert!(
3469+
!self.nodes.iter().any(|node| !node.broadcaster.txn_broadcasted.borrow().is_empty())
3470+
&& !self.chain_state.has_pending_txs(),
3471+
"{context} tx mining loop failed to quiesce",
3472+
);
3473+
}
34443474
}
34453475

34463476
#[inline]

0 commit comments

Comments
 (0)