bark: Avoid exit deadlock by accidentally double-spending input

Claude's words:

What the flake is

The exit of a round VTXO broadcasts a multi-tx exit chain, each tx fee-bumped by its own CPFP child that draws a fee input from barkd's on-chain wallet. In these tests the wallet has a single funded UTXO:

  1. tx1's CPFP spends the UTXO and confirms at height 121.
  2. barkd's on-chain wallet hasn't synced that spend yet, so when it builds tx2's CPFP it reuses the same (now-spent) UTXO.
  3. tx2's CPFP (14592f01) can never enter the mempool — its input is already spent (bad-txns-inputs-missingorspent).
  4. The recovery path just rebroadcasts that same doomed child every tick, forever → the exit never reaches Claimable → 120s timeout.

I confirmed this from the daemon log (barkd1/debug.log): CPFP broadcast conflict … another CPFP may already be in mempool at height 121, then fell out of mempool, rebroadcasting + inputs are missing or already spent ~78 times until the wall. No round-validation error appears, and the refresh round completed normally — so vul-55 is not involved.

The fix

In the AwaitingConfirmation → child NotFound → rebroadcast branch, if the rebroadcast fails specifically with MissingOrSpentInputs, the stored CPFP is permanently dead, so instead of erroring (and retrying it forever) I reset the exit tx to AwaitingCpfpBroadcast. The daemon's progress_exits_with_cpfp loop then builds a fresh CPFP from currently-spendable UTXOs (by then, tx1's CPFP change has confirmed), which broadcasts cleanly and confirms. It self-heals in a block or two.

This mirrors the two recovery patterns already right above it (None => AwaitingCpfpBroadcast for "child disappeared"). It's narrowly scoped:

  • Only triggers when a stored CPFP is genuinely gone (NotFound) and rebroadcast fails with inputs-already-spent.
  • I match the exact MissingOrSpentInputs variant, not the broader is_mempool_conflict() — so AlreadyKnown (a legitimate third-party CPFP already in the mempool) is untouched and still waits, no regression there.
  • Healthy exits (rebroadcast succeeds) are unchanged.
Edited by Steven Roose

Merge request reports

Loading
Loading