The poller and the reconciler matched current_txid only. If an earlier version of
a bumped transaction confirmed it was never seen, and after 6h the row was abandoned:
a confirmed bet's participant deleted, a sent withdrawal marked failed, a paid payout
rebuilt. bump_fee also broadcast in phase 2 and persisted in phase 3 unconditionally,
so a crash or a `database is locked` in between left a transaction on the network that
the DB did not know, and an abandon or a confirmation in the meantime was overwritten.
- New table pending_transaction_txids (migration a3f5c2d9b7e1; txid unique, index on
the owning row; existing rows backfilled with current_txid + raw and the txid their
last bump replaced, raw_hex empty). Written with the row by place_bet,
request_withdrawal and _trigger_payout; removed with a bet row that a verified
refusal deletes (DAT-38).
- bump_fee in four phases: read, build and sign, commit the replacement as a candidate
version, broadcast (verdict of B-76, fanned out when accepted; an explicit refusal
raises RbfError and leaves the row), then switch the row over with
`UPDATE ... WHERE status='pending' AND current_txid=<replaced>`. Zero rows matched is
a logged conflict: nothing is overwritten and the candidate stays recorded. An
uncertain broadcast adopts the replacement. A retry rebuilds the same candidate
without tripping the unique txid.
- poll_once and reconcile_once look for every txid of a row. When the version found is
not the current one, broadcast.adopt_version makes it current (raw tx included) and
retargets spent_txid, bet_txid, withdrawals.txid and payout_txid as a bump does,
before the confirmation handler runs. _release_inputs frees inputs claimed by any
version of the row.
- RbfBumper takes the listener as `network`.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
batch_alter_table on SQLite rebuilds the table and silently drops
ix_rounds_single_active and ix_users_username_lower, autogenerate never sees them,
and no test ran the migrations (the others build the schema with create_all).
tests/unit/test_migrations.py runs `alembic upgrade head` on an empty file DB in a
subprocess and asserts both indexes exist with their definition (read from
sqlite_master) and that a second active round and a case-variant username are
refused. Verified to fail when a batch_alter_table on rounds is added to the last
migration. The current chain already preserves both indexes, so no migration changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>