What happened
A multisig cosigner catching up on hub history panics in get_change_utxo_idx; the panic crosses the FFI boundary and aborts the host process:
INFO[multisig.rs:2051] operation 7 tx b67b9ed5… already broadcast; completing without re-finalizing
PANIC: Location { file: "src/wallet/online.rs", line: 2676 } - should exist
The failing operation is replayed again on every restart, so the wallet ends up in a crash loop with no recovery short of manual DB surgery. Hit live on the bridge stand (2026-08-17, RGB connector wallet catching up after a restore) with v0.3.0-beta.28; dev has identical code in this path and panics on the same line.
Root cause
Operation 7's OperationData had btc_change: None and change_utxo_outpoint: e1ed6794…:2. The wallet's txo table contained vouts 1, 3, 4 and 5 of that tx - but not vout 2. Pattern: present = still unspent, missing = already spent. Vout 2 was created (by a create-utxos operation) and spent (as an input of a later operation) entirely inside the window the wallet was not syncing.
get_change_utxo_idx (src/wallet/online.rs:2642) treats its two arms asymmetrically:
- the
btc_change arm tolerates a missing TXO and inserts a placeholder row (exists: false);
- the
change_utxo_outpoint arm does a bare txn.get_txo(outpoint)?.expect("should exist").
Nothing guarantees that row exists:
- every regular sync writes TXO rows via
update_db_colored_txos_from_bdk(txn, include_spent = false) - unspent only, so a created-and-spent outpoint is never backfilled;
- the spent-assignments loop in
save_transfers has a rescue path (FastSync + include_spent = true) for missing input TXOs, but the change outpoint is looked up without any rescue;
- a create-utxos replay materializes no TXO rows at all.
A wallet that never synced is silently rescued (its missing inputs trigger the backfill, which happens to also insert the change TXO); a wallet that was merely behind - the realistic recovery case - panics.
Reproducer
Test-only PR: . sync_with_hub_replay_survives_inflation_change, branch test/hub-replay-panic (off dev).
Anatomy (2-of-3 multisig, IFA with inflation rights):
- create-utxos round 1: exactly 2 outputs - the genesis occupies both;
- issue IFA (600 fungible + 1000 inflation rights);
- the lagging cosigner syncs up to here, then goes dark;
- create-utxos round 2 - now the only empty colorable outputs;
- inflation 1: rights change forced onto a round-2 output,
btc_change: None (1000-sat UTXOs make the BTC remainder sub-dust);
- inflation 2: spends inflation 1's rights-change UTXO;
- catch-up replays inflation 1 → panic at
online.rs:2676, byte-for-byte the live failure, including the txo-table pattern (unspent siblings present, the spent change target missing).
Proposed fix (verified locally, not included in the PR)
Mirror the spent-assignments rescue in the change_utxo_outpoint arm: on a miss run sync_wallet(FastSync, include_spent = true) and retry the lookup. With that patch the reproducer passes end-to-end (651/651/651 balances on all three cosigners) and the rest of the multisig suite is unaffected (4/4).
Side note on the multisig test suite
The pinned hub image ghcr.io/rgb-tools/rgb-multisig-hub:0.1.1 (2026-05-01) predates biscuit-auth 6.0.0 and rejects every token minted by current test code, so all multisig tests fail with "Missing or invalid credentials" unless the hub is built from current source. CI never notices because it runs with --no-default-features.
What happened
A multisig cosigner catching up on hub history panics in
get_change_utxo_idx; the panic crosses the FFI boundary and aborts the host process:The failing operation is replayed again on every restart, so the wallet ends up in a crash loop with no recovery short of manual DB surgery. Hit live on the bridge stand (2026-08-17, RGB connector wallet catching up after a restore) with
v0.3.0-beta.28;devhas identical code in this path and panics on the same line.Root cause
Operation 7's
OperationDatahadbtc_change: Noneandchange_utxo_outpoint: e1ed6794…:2. The wallet'stxotable contained vouts 1, 3, 4 and 5 of that tx - but not vout 2. Pattern: present = still unspent, missing = already spent. Vout 2 was created (by a create-utxos operation) and spent (as an input of a later operation) entirely inside the window the wallet was not syncing.get_change_utxo_idx(src/wallet/online.rs:2642) treats its two arms asymmetrically:btc_changearm tolerates a missing TXO and inserts a placeholder row (exists: false);change_utxo_outpointarm does a baretxn.get_txo(outpoint)?.expect("should exist").Nothing guarantees that row exists:
update_db_colored_txos_from_bdk(txn, include_spent = false)- unspent only, so a created-and-spent outpoint is never backfilled;save_transfershas a rescue path (FastSync+include_spent = true) for missing input TXOs, but the change outpoint is looked up without any rescue;A wallet that never synced is silently rescued (its missing inputs trigger the backfill, which happens to also insert the change TXO); a wallet that was merely behind - the realistic recovery case - panics.
Reproducer
Test-only PR: .
sync_with_hub_replay_survives_inflation_change, branchtest/hub-replay-panic(offdev).Anatomy (2-of-3 multisig, IFA with inflation rights):
btc_change: None(1000-sat UTXOs make the BTC remainder sub-dust);online.rs:2676, byte-for-byte the live failure, including the txo-table pattern (unspent siblings present, the spent change target missing).Proposed fix (verified locally, not included in the PR)
Mirror the spent-assignments rescue in the
change_utxo_outpointarm: on a miss runsync_wallet(FastSync, include_spent = true)and retry the lookup. With that patch the reproducer passes end-to-end (651/651/651 balances on all three cosigners) and the rest of the multisig suite is unaffected (4/4).Side note on the multisig test suite
The pinned hub image
ghcr.io/rgb-tools/rgb-multisig-hub:0.1.1(2026-05-01) predates biscuit-auth 6.0.0 and rejects every token minted by current test code, so all multisig tests fail with "Missing or invalid credentials" unless the hub is built from current source. CI never notices because it runs with--no-default-features.