0d2fef650256630d937186f12fd1daaeda845c01
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4124dc08e6 |
Check confirmation/existence via scripthash history, not verbose replies (B-41)
poll_once and reconcile.py's existence check both called
blockchain.transaction.get(txid, verbose=True). Several Electrum server
implementations and versions reject the verbose flag outright
("verbose transactions are currently unsupported"), which would have
meant no confirmations and no reconciliation ever running against such
a server, read as a plain transport error. reconcile.py additionally
decided whether to abandon a transaction - releasing its funds - by
substring-matching the error text ("missing", "not found", ...), which
only works against ElectrumX's specific wording.
Both now ask blockchain.scripthash.get_history for the address that
owns every input of the transaction (a user's own address for a
bet/withdrawal, the pool address for a payout) and look for the txid in
the result: present with height > 0 means confirmed, present with
height <= 0 means still in the mempool, absent means the server
doesn't know it. get_history is a plain, universally-supported Electrum
method, and "not in the list" replaces the old substring-matching
entirely - no more guessing at error wording to decide whether to
release funds. History is cached per scripthash within one pass, since
every "payout" row shares the same pool address.
New app/tx/pending_address.py factors out own_address_for (the
address derivation was previously duplicated informally inside
tx/broadcast.py's signing context) so confirmation.py and reconcile.py
share one definition instead of two that could compute different
addresses for the same row.
tests/unit/test_confirmation.py and test_reconcile.py needed real User
rows and a master-key bootstrap they didn't have before, since address
derivation is now exercised for real rather than assumed. Suite grows
from 217 to 222 tests. BUGS.md moves B-41 to Previously fixed - no
Medium-severity finding remains open.
|
||
|
|
933760e948 |
Decouple the RBF abandon clock from the bump clock (B-27)
bump_fee (tx/broadcast.py) used to overwrite PendingTransaction. broadcast_at on every fee bump, but reconcile.py's abandon-after-N- hours grace period is measured from that same column. A transaction successfully bumped every rbf_timeout_seconds (900s by default) but never mined reset that clock before it could ever reach the 6-hour abandon window, so it was never abandoned: its UTXOs never returned to the user, and if it was a bet the round stayed in "closing" indefinitely. PendingTransaction gains a last_broadcast_at column (migration 861e76aaf34c, backfilled from broadcast_at for existing rows before the NOT NULL constraint is applied). broadcast_at is now never rewritten after creation, so reconcile.py's _is_due keeps measuring from the first broadcast unchanged. bump_fee updates last_broadcast_at instead, and should_bump now reads last_broadcast_at rather than broadcast_at — correct, since whether another bump is due should reset after every bump, unlike the reconciler's abandon check, which must not. BUGS.md moves B-27 to "Previously fixed" with the fix description; the suite grows from 148 to 151 tests, including a direct proof that a tx bumped a minute ago but first broadcast 7 hours ago still gets abandoned. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d528c5b475 |
Let the system recover from a broadcast that never confirms
The code treated a broadcast as final: money moved on-chain and the DB was updated on the assumption it would either confirm or be fee-bumped until it did. Neither is guaranteed, and every way that assumption broke was permanent (BUGS.md B-02, B-03, B-04, B-07, B-08, B-20, B-21). Persist before broadcasting. place_bet and request_withdrawal now write their rows in a "building" state and commit, then broadcast, then promote to broadcast/pending in a second commit. Before, a failure or crash between the broadcast and the commit left the coins irreversibly spent with no trace: no participant (so no entry in the draw), no pending row (so no RBF and no confirmation tracking), and the UTXOs not even marked spent, so the next bet would try to double-spend them. A refused broadcast now releases the reserved UTXOs, restores the balance, removes the participant (or marks the withdrawal failed), audit-logs it, and answers a translatable broadcast_failed — as 502, since the network refused it, not the caller, where it used to be an opaque 500. Reconcile what's in flight against the chain. New PendingTransactionReconciler (app/tx/reconcile.py, every 120s and once at startup) asks whether each non-terminal tx exists: present -> promote, gone -> mark failed with a reason, release the inputs, roll the domain row back, audit-log it. Grace periods differ by state (120s for "building", 6h for "pending", so the RBF bumper gets its attempts first). It is deliberately biased to inaction: only a server that positively doesn't know the tx counts as absent, and a transport failure never abandons anything, because releasing a UTXO whose tx is actually alive would invite a double-spend. Verified against the live server, which answers "No such mempool or blockchain transaction" for an unknown txid. Stop keying on a value that changes. An RBF bump changes the txid, and _on_bet_confirmed looked the participant up by bet_txid — so a bumped bet confirmed under a txid no participant carried, the row stayed "broadcast" forever, and the scheduler waited on it forever: the round could never close and the lottery stopped. Handlers now resolve by immutable ids (round_id/user_id, withdrawal_id), and bump_fee retargets every stored txid — bet_txid, Withdrawal.txid, Round.payout_txid and UtxoEvent.spent_txid — plus records the previous one in replaced_by_txid, which was never written at all. One bad row no longer blocks the rest. The confirmation poller's per-tx lookup is guarded: a txid the server can't resolve used to abort the whole pass, so nothing confirmed again until an operator intervened. It also selects plain columns instead of hydrating entities that outlive their session. Tests: 6 reconciler cases including "a broken connection must not release coins"; the bet-ordering test probes committed state from an independent session during the broadcast, and caught a real mistake in the first draft of this change (the _pending_transaction helper still hardcoded status="pending", so rows were born already-broadcast and would have got the 6-hour grace instead of 120s). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |