Track every version of a bumped transaction and write the RBF replacement before broadcasting it (B-77, TX-12, TX-13, TX-19, TX-21, DAT-13, DAT-14, DAT-34, DAT-44, DAT-45, DAT-70, DAT-71)

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>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-14 14:46:43 +02:00
1 parent 2b6ddb019b
commit f936d6660c
23 files changed
+836 -130

No files matched your search

@@ -1,6 +1,6 @@
# ADR-31 — Tutto ciò che spende denaro si scrive prima di inviarlo
- **Stato:** Accettata — implementazione da completare (B-77)
- **Stato:** Accettata
- **Area:** Denaro in movimento
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
@@ -20,5 +20,4 @@ riconciliatore e risponde **202** ([ADR-51](ADR-51-dubbio-liberano-mai-fondi.md)
*Codice:* `app/bets/service.py:place_bet` · `app/withdrawals/service.py:request_withdrawal` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/tx/broadcast.py:bump_fee` · `app/tx/reconcile.py:reconcile_once` ·
*Test:* `tests/unit/test_bets.py::test_bet_is_persisted_before_it_is_broadcast`, `tests/unit/test_broadcast_outcome.py`, `tests/unit/test_scheduler.py::test_trigger_payout_persists_before_broadcasting`, `tests/unit/test_reconcile.py`
*Stato:* da implementare (B-77), per le sostituzioni RBF · *Oggi:*
`bump_fee` invia la sostituzione prima di salvarla.
+5 -5
View File
@@ -1,6 +1,6 @@
# ADR-51 — Nel dubbio non si liberano mai i fondi
- **Stato:** Accettata — implementazione da completare (B-77, B-78)
- **Stato:** Accettata — implementazione da completare (B-78)
- **Area:** Denaro in movimento
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
@@ -37,7 +37,7 @@ pausa la lotteria ([ADR-28](ADR-28-pausa-manutenzione-ferma-round-successivo.md)
*Codice:* `app/tx/reconcile.py:reconcile_once` · `app/tx/reconcile.py:_is_due` · `app/bets/service.py:place_bet` · `app/withdrawals/service.py:request_withdrawal` · `app/tx/confirmation.py:poll_once` · `app/tx/broadcast.py:bump_fee` ·
*Test:* `tests/unit/test_broadcast_outcome.py`
*Stato:* da implementare (B-77, B-78) · *Oggi:* l'abbandono avviene
quando la cronologia del solo server attivo non contiene il txid corrente,
dopo 120s per `building` e 6 ore per `pending`; si segue solo l'ultimo txid;
le righe `failed` non sono più controllate.
*Stato:* da implementare (B-78) · *Oggi:* l'abbandono avviene
quando la cronologia del solo server attivo non contiene alcun txid della
riga, dopo 120s per `building` e 6 ore per `pending`; le righe `failed` non
sono più controllate.
+2 -2
View File
@@ -564,8 +564,8 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
([LIM-01](limiti-noti.md#robustezza-del-ciclo-di-vita)).
- **[ARC-28]** **`ConfirmationPoller`** (10s), **`RbfBumper`** (30s) e
**`PendingTransactionReconciler`** (al primo giro subito, poi ogni 120s)
lavorano sulla tabella `pending_transactions` (e, dal piano, sulla tabella
di tutti i txid di ogni riga, B-77); il riconciliatore parte subito perché
lavorano sulla tabella `pending_transactions` (e sulla tabella
di tutti i txid di ogni riga, `pending_transaction_txids`); il riconciliatore parte subito perché
un crash durante un invio lascia proprio una riga `building` da risolvere
al riavvio. Regole in
[transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md).
+1 -2
View File
@@ -349,8 +349,7 @@ L'**invio** di una transazione non è una corroborazione ma usa gli stessi
server: dopo che il server attivo l'ha accettata, la transazione grezza viene
inviata anche a ogni server di riserva, al meglio. La regola è in
[transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md) (B-76,
B-83); oggi l'invio va anche a tutti gli altri server, salvo la sostituzione
RBF di `bump_fee` (B-77).
B-83).
## Riconnessione
+15 -40
View File
@@ -42,8 +42,7 @@ Cosa **non** c'è qui, e dove trovarlo:
## Diagramma delle relazioni
Dieci tabelle applicative nello schema atteso (nove oggi: manca
`pending_transaction_txids`, B-77). `round_config` non ha relazioni: è una
Dieci tabelle applicative. `round_config` non ha relazioni: è una
riga singola di parametri. Gli indirizzi della piattaforma che non
appartengono a un utente sono derivati dalla master key, e i loro UTXO si
leggono dalla rete, non da una tabella:
@@ -67,7 +66,7 @@ erDiagram
rounds |o--o{ pending_transactions : "bet / payout"
withdrawals |o--o{ pending_transactions : "withdrawal"
users |o--o{ pending_transactions : "bet / withdrawal"
pending_transactions ||--|{ pending_transaction_txids : "ogni versione (da implementare)"
pending_transactions ||--|{ pending_transaction_txids : "ogni versione"
users |o--o{ bug_reports : "segnala"
users |o--o{ audit_log : "riguarda"
rounds |o--o{ audit_log : "riguarda"
@@ -233,7 +232,7 @@ erDiagram
([DAT-52]). Nessuna chiave esterna ha `ON DELETE`. Le sole cancellazioni
fisiche nel codice sono quelle di una scommessa fallita (la riga
`round_participants` e la sua `pending_transactions`, con le sue versioni in
`pending_transaction_txids` nello schema atteso,
`pending_transaction_txids`,
[DAT-24], [DAT-38], [DAT-70]); nessun'altra tabella perde righe, quindi i riferimenti
restano validi per costruzione, non per vincolo.
*Codice:* `app/db/base.py:_register_sqlite_pragmas` ·
@@ -402,9 +401,7 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
stesso output risulta impegnato da un'altra transazione, quella
rivendicazione è quella valida e non va toccata.
*Codice:* `app/tx/reconcile.py:_release_inputs` ·
*Test:* `tests/unit/test_reconcile.py`
*Stato:* da implementare (B-77), per le versioni precedenti · *Oggi:* il
confronto è solo con `current_txid`.
*Test:* `tests/unit/test_reconcile.py`, `tests/unit/test_tx_versions.py`
- **[DAT-14]** Per ogni output impegnato, `spent_txid` è sempre il txid
**corrente** della transazione che lo spende, cioè il `current_txid` della sua
@@ -419,9 +416,7 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
*Codice:* `app/tx/broadcast.py:bump_fee` ·
`app/tx/broadcast.py:_retarget_txid_references` ·
`app/tx/confirmation.py:poll_once` ·
*Test:* `tests/unit/test_broadcast.py`
*Stato:* da implementare (B-77) · *Oggi:* solo il bump riallinea; la
conferma di una versione precedente non viene riconosciuta.
*Test:* `tests/unit/test_broadcast.py`, `tests/unit/test_tx_versions.py`
### `rounds`
@@ -904,7 +899,7 @@ riconciliatore (vedi
| `broadcast_at` | DateTime | no | `utcnow` | Prima trasmissione, vedi [DAT-35] | ORM, alla creazione |
| `last_broadcast_at` | DateTime | no | `utcnow` | Ultima trasmissione, vedi [DAT-35] | ORM; `bump_fee` |
| `status` | String(16) | no | `"pending"` (mai usato: si crea sempre `building`) | Vedi [DAT-33] | vedi diagramma |
| `replaced_by_txid` | String(64) | sì | `NULL` | Il txid **sostituito** dall'ultimo bump (nonostante il nome, punta all'indietro). Nello schema atteso la cronologia completa è in `pending_transaction_txids` ([DAT-70]) e questa colonna resta solo informativa | `bump_fee` |
| `replaced_by_txid` | String(64) | sì | `NULL` | Il txid **sostituito** dall'ultimo bump (nonostante il nome, punta all'indietro). La cronologia completa è in `pending_transaction_txids` ([DAT-70]) e questa colonna resta solo informativa | `bump_fee` |
| `attempt_count` | Integer | no | `1` | 1 + numero di bump riusciti | `bump_fee` |
| `failure_reason` | String(128) | sì | `NULL` | Perché la transazione è fallita; solo con `status = failed` | `_abandon`; `_release_failed_withdrawal` |
| `failed_at` | DateTime | sì | `NULL` | Istante del passaggio a `failed`; da lì si contano i 14 giorni di sorveglianza ([DAT-72]) — *da implementare (B-78)* | `_abandon`; `_release_failed_withdrawal` |
@@ -995,12 +990,7 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
`fee_rate_sat_vb` registra la commissione effettivamente pagata dalla nuova
versione, non quella obiettivo.
*Codice:* `app/tx/broadcast.py:bump_fee` · *Test:* `tests/unit/test_broadcast.py`
*Stato:* da implementare (B-77) · *Oggi:* la nuova versione è trasmessa
nella fase 2 prima di esistere nel database, e la fase 3 riscrive la riga
**senza ricontrollarne** stato e `current_txid`: un abbandono o una
conferma avvenuti nel frattempo vengono sovrascritti. `replaced_by_txid`
conserva solo il txid immediatamente precedente.
*Codice:* `app/tx/broadcast.py:bump_fee` · *Test:* `tests/unit/test_broadcast.py`, `tests/unit/test_tx_versions.py`
- **[DAT-35]** `broadcast_at` è la **prima** trasmissione e non viene mai
riscritta: è il riferimento da cui il riconciliatore misura i propri tempi
@@ -1062,7 +1052,7 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
### `pending_transaction_txids`
*Tabella da implementare (B-77).* Ogni versione che una riga
Ogni versione che una riga
`pending_transactions` abbia mai avuto: la prima, ogni sostituzione RBF
trasmessa e ogni candidata scritta prima di un bump. Serve perché una
qualunque di queste versioni può essere quella che la rete conferma.
@@ -1086,12 +1076,9 @@ qualunque di queste versioni può essere quella che la rete conferma.
`current_txid`: la conferma di uno qualunque conferma la riga ([DAT-14]).
La riga `bet` cancellata da `_release_failed_bet` ([DAT-38]) porta con sé
le proprie versioni, nella stessa transazione.
*Codice:* `app/db/models.py` · `app/tx/broadcast.py:bump_fee` ·
*Codice:* `app/db/models.py:PendingTransactionTxid` · `app/tx/versions.py` · `app/tx/broadcast.py:bump_fee` ·
`app/tx/confirmation.py:poll_once` · `app/tx/reconcile.py:reconcile_once` ·
*Test:* —
*Stato:* da implementare (B-77) · *Oggi:* la tabella non esiste; poller e
riconciliatore confrontano solo `current_txid`, e `replaced_by_txid`
ricorda un solo passo indietro.
*Test:* `tests/unit/test_tx_versions.py`, `tests/unit/test_migrations.py`
- **[DAT-71]** Una versione **candidata** è una riga di questa tabella il cui
txid non è (ancora) `current_txid`: `bump_fee` l'ha scritta ma la fase 3 non
@@ -1102,9 +1089,7 @@ qualunque di queste versioni può essere quella che la rete conferma.
Il riconciliatore, quando ritrasmette, usa il `raw_tx_hex` corrente, non
quello di una candidata.
*Codice:* `app/tx/broadcast.py:bump_fee` · `app/tx/reconcile.py:reconcile_once` ·
*Test:* —
*Stato:* da implementare (B-77) · *Oggi:* non esistono candidate: la
sostituzione esiste nel database solo dopo il suo broadcast riuscito.
*Test:* `tests/unit/test_tx_versions.py`
### `withdrawals`
@@ -1240,8 +1225,8 @@ Tabella di servizio di Alembic: una riga con la revisione applicata
| `ix_rounds_single_active` | `rounds` | `UNIQUE ((1)) WHERE status IN ('open','closing','drawing','paying_out')` | al massimo un round attivo, [DAT-17] (B-09) |
| unico (`round_id`, `user_id`) | `round_participants` | `UNIQUE` | una scommessa per utente per round, [DAT-23] |
| unico (`round_id`, `draw_position`) | `round_participants` | `UNIQUE` (i `NULL` non collidono) | una posizione di estrazione per partecipante, [DAT-66] — *da implementare (B-81)* |
| unico `txid` | `pending_transaction_txids` | `UNIQUE` | un txid appartiene a una sola transazione, [DAT-70] — *da implementare (B-77)* |
| `ix_pending_transaction_txids_pending_transaction_id` | `pending_transaction_txids` | indice | versioni di una transazione — *da implementare (B-77)* |
| unico `txid` | `pending_transaction_txids` | `UNIQUE` | un txid appartiene a una sola transazione, [DAT-70] |
| `ix_pending_transaction_txids_pending_transaction_id` | `pending_transaction_txids` | indice | versioni di una transazione |
| `ix_round_participants_round_id` | `round_participants` | indice | partecipanti di un round |
| `ix_round_participants_user_id` | `round_participants` | indice | round di un utente (`last-round-result`) |
| `ix_withdrawals_user_id` | `withdrawals` | indice | prelievi di un utente |
@@ -1267,12 +1252,7 @@ letti con una scansione completa.
`app/withdrawals/service.py:request_withdrawal` ·
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
`app/tx/broadcast.py:bump_fee` ·
*Test:* `tests/unit/test_bets.py`, `tests/unit/test_scheduler.py`
*Stato:* da implementare (B-77), per le sostituzioni RBF · *Oggi:*
l'invariante vale per la prima versione; una sostituzione RBF viene
trasmessa nella fase 2 di `bump_fee` e scritta nel database solo nella
fase 3, quindi un arresto o un `database is locked` fra le due lascia sulla
rete una transazione che il database non conosce.
*Test:* `tests/unit/test_bets.py`, `tests/unit/test_scheduler.py`, `tests/unit/test_tx_versions.py`
- **[DAT-45]** **Ogni versione è riconoscibile, una sola è corrente.** Per ogni
transazione non terminale, `pending_transactions.current_txid` coincide con
@@ -1287,12 +1267,7 @@ letti con una scansione completa.
txid.
*Codice:* `app/tx/broadcast.py:_retarget_txid_references` ·
`app/tx/broadcast.py:bump_fee` · `app/tx/confirmation.py:poll_once` ·
*Test:* `tests/unit/test_broadcast.py`
*Stato:* da implementare (B-77) · *Oggi:* l'aggiornamento dei riferimenti
avviene in una transazione del database separata dal broadcast e non
condizionata, e i txid precedenti sono dimenticati (salvo l'ultimo, in
`replaced_by_txid`, che nessuno legge): se conferma una versione
precedente, poller e riconciliatore non la riconoscono.
*Test:* `tests/unit/test_broadcast.py`, `tests/unit/test_tx_versions.py`
- **[DAT-46]** **Il montepremi di ogni round si conserva, separato dagli
altri.** Per un round pagato, con i soli partecipanti rimasti `confirmed`
+2 -2
View File
@@ -615,8 +615,8 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
indirizzo secondo il **solo server attivo**: mai partita (riga `building` da
oltre 120s) o scomparsa dal mempool (riga `pending` da oltre 6 ore dal primo
invio). Non è una prova: un server diverso da quello che ha ricevuto l'invio
può non conoscerla, e una versione precedente di una transazione accelerata
con RBF non viene cercata (B-77).
può non conoscerla (si cercano tutte le versioni di una transazione
accelerata con RBF, ma solo sul server attivo).
**Effetto e cosa fare.** Il sistema ha già annullato tutto: UTXO liberati e
saldo ricalcolato; per una scommessa il partecipante è rimosso (l'utente può
riscommettere se il round è ancora aperto); per un prelievo la richiesta è
+3 -3
View File
@@ -225,7 +225,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
| B-74 | Header accettati senza controllo della difficoltà minima; un salto di più blocchi non viene collegato; una riorganizzazione di un blocco chiude la sessione | Critica | **aperto** | — | ELC |
| B-75 | Una risposta Electrum oltre 64 KiB chiude la connessione, e la cronologia dell'indirizzo del montepremi la supera dopo ~800 transazioni | Critica | *da registrare* | `test_electrum_client.py`, `test_deposits.py`, `test_psbt_builder.py` | ELC, TX |
| B-76 | Un invio ambiguo (timeout, connessione caduta) viene annullato come se fosse rifiutato | Critica | *da registrare* | `test_broadcast_outcome.py`, `test_electrum_client.py` | TX, BET, WDR |
| B-77 | Si ricorda solo l'ultimo txid di una transazione accelerata, e la sostituzione viene inviata prima di essere salvata | Critica | **aperto** | — | TX |
| B-77 | Si ricorda solo l'ultimo txid di una transazione accelerata, e la sostituzione viene inviata prima di essere salvata | Critica | *da registrare* | `test_tx_versions.py`, `test_migrations.py`, `test_broadcast.py` | TX |
| B-78 | Una transazione viene abbandonata sulla base del silenzio di un solo server | Critica | **aperto** | — | TX, ELC |
| B-79 | Il pagamento di norma non ha resto, quindi non si può accelerare e può bloccare la lotteria | Critica | **aperto** | — | RND, TX |
| B-80 | Un solo indirizzo del montepremi per tutti i round | Critica | *da registrare* | `test_round_pool_address.py`, `test_pending_address.py`, `test_scheduler.py` | RND, BET, WDR |
@@ -252,9 +252,9 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
- **Corretti:** B-01 … B-70, tranne B-16, che è chiuso nella sostanza (il
crash è eliminato e `/guida` serve un segnaposto) ma lascia la guida vera
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76.
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77.
- **Aperti:** B-71, B-72 (gravità bassa, operativi o di documentazione) e
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (7 critici,
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (6 critici,
7 alti, 6 medi), da implementare nell'ordine di
[piano-correzioni.md](piano-correzioni.md).
- **Prossimo numero libero:** `B-99`.
+10 -24
View File
@@ -69,11 +69,8 @@ stateDiagram-v2
la versione trovata in catena non è quella corrente, `current_txid` e
`raw_tx_hex` diventano quelli della versione trovata e il txid viene
propagato ovunque sia registrato, come dopo un RBF ([TX-12]).
*Codice:* `app/tx/confirmation.py:poll_once`, `app/tx/reconcile.py:reconcile_once`, `app/tx/broadcast.py:_retarget_txid_references` ·
*Test:* —
*Stato:* da implementare (B-77) · *Oggi:* si conserva solo il txid corrente
(più il precedente in `replaced_by_txid`, che nessuno legge); se conferma
una versione precedente a un RBF non viene riconosciuta.
*Codice:* `app/tx/versions.py`, `app/tx/confirmation.py:poll_once`, `app/tx/reconcile.py:reconcile_once`, `app/tx/broadcast.py:adopt_version`, `app/tx/broadcast.py:_retarget_txid_references` ·
*Test:* `tests/unit/test_tx_versions.py`, `tests/unit/test_migrations.py`
- **[TX-28]** Ogni transazione del database che sposta denaro — la scrittura
di `building` ([TX-01]), la promozione dopo l'invio, l'annullamento dopo un
rifiuto, le due scritture dell'RBF ([TX-12]), la promozione e l'abbandono
@@ -147,9 +144,7 @@ monete:
questi invii viene solo registrato nei log, non cambia lo stato della riga
e non ritarda la risposta all'utente.
*Codice:* `app/electrum/listener.py:ElectrumListener.fan_out_transaction`, `app/tx/outcome.py:broadcast_with_verdict`, `app/bets/service.py:place_bet`, `app/withdrawals/service.py:request_withdrawal`, `app/rounds/scheduler.py:RoundScheduler._trigger_payout`, `app/tx/broadcast.py:bump_fee` ·
*Test:* `tests/unit/test_broadcast_outcome.py`
*Stato:* da implementare (B-77), per la sostituzione RBF · *Oggi:* la
sostituzione di `bump_fee` viene inviata solo al server attivo.
*Test:* `tests/unit/test_broadcast_outcome.py`, `tests/unit/test_tx_versions.py`
- **[TX-05]** Se l'invio di un pagamento non è accettato — rifiuto esplicito
o esito incerto — la riga resta `building` e si scrive `payout_failed`
(`broadcast rejected`): è il riconciliatore a decidere, interrogando la
@@ -172,10 +167,9 @@ monete:
per indirizzo a ogni giro; un indirizzo che non si riesce a leggere non
blocca gli altri.
*Codice:* `app/tx/confirmation.py:poll_once`, `app/tx/pending_address.py:own_address_for` ·
*Test:* `tests/unit/test_confirmation.py`, `tests/unit/test_round_pool_address.py`
*Stato:* da implementare (B-77) · *Oggi:* cerca solo `current_txid`,
nella cronologia dell'indirizzo del montepremi del round per i pagamenti; un'altezza maggiore di 0 riportata dal solo server
attivo basta a confermare ([TX-22]).
*Test:* `tests/unit/test_confirmation.py`, `tests/unit/test_round_pool_address.py`, `tests/unit/test_tx_versions.py`
*Stato:* da implementare (B-83) · *Oggi:* un'altezza maggiore di 0
riportata dal solo server attivo basta a confermare ([TX-22]).
- **[TX-22]** **Conferma verificata.** Un candidato ([TX-06]) è confermato
solo quando il quorum dei server
([connessione-electrum.md](connessione-electrum.md) (B-84)) restituisce per
@@ -277,11 +271,7 @@ sostituibilità fin dal primo invio.
round (`payout_txid`). Se la sostituzione è rifiutata esplicitamente la
riga non cambia ([TX-24]).
*Codice:* `app/tx/broadcast.py:bump_fee`, `app/tx/broadcast.py:_retarget_txid_references` ·
*Test:* `tests/unit/test_broadcast.py`
*Stato:* da implementare (B-77) · *Oggi:* la sostituzione viene inviata
senza essere registrata, e la riga viene aggiornata solo dopo l'invio, senza
condizioni: un crash o un `database is locked` in mezzo lascia in rete una
transazione che il database non conosce.
*Test:* `tests/unit/test_broadcast.py`, `tests/unit/test_tx_versions.py`
- **[TX-13]** L'RBF non tocca una riga che è cambiata sotto di lui. Prima di
costruire la sostituzione, se la riga non esiste più o non è più
`pending` (ad esempio è appena stata confermata), non si fa nulla. Dopo
@@ -291,11 +281,7 @@ sostituibilità fin dal primo invio.
candidata resta registrata — la conferma e la riconciliazione la cercano
comunque ([TX-19]) — e il conflitto viene scritto nei log.
*Codice:* `app/tx/broadcast.py:bump_fee` ·
*Test:* `tests/unit/test_broadcast.py`
*Stato:* da implementare (B-77) · *Oggi:* il controllo avviene solo prima
della costruzione; dopo l'invio la riga viene aggiornata senza ricontrollare
né lo stato né `current_txid`, anche se nel frattempo è stata confermata o
abbandonata.
*Test:* `tests/unit/test_broadcast.py`, `tests/unit/test_tx_versions.py`
- **[TX-24]** Un'accelerazione impossibile ([TX-11], o la riga già a
`MAX_FEE_RATE_SAT_VB`) o una sostituzione rifiutata esplicitamente dalla
rete scrivono un evento `rbf_bump_failed` ([audit-log.md](audit-log.md)
@@ -342,9 +328,9 @@ riserva di [TX-11] serve proprio a dargli un'uscita da ridurre.
([TX-17]). Un **errore di rete** non porta a nulla: la riga resta com'è
fino al giro successivo.
*Codice:* `app/tx/reconcile.py:reconcile_once`, `app/electrum/listener.py:ElectrumListener._corroborate_majority` ·
*Test:* `tests/unit/test_reconcile.py`
*Test:* `tests/unit/test_reconcile.py`, `tests/unit/test_tx_versions.py`
*Stato:* da implementare (B-78) · *Oggi:* si interroga solo il server
attivo e solo per `current_txid`, e un txid assente dalla sua cronologia è
attivo, e un txid assente dalla sua cronologia è
trattato come "sconosciuto alla rete": dopo un riavvio su un server diverso
una riga `building` viene abbandonata dopo 120s anche se la transazione è
nella mempool degli altri nodi (che la tengono fino a 336 ore con le