- bet/withdrawal routes read listener.client after taking the user lock - a reduced full-balance withdrawal is checked against the minimum again - a bet covered only by pending balance answers balance_pending_confirmation - a recovered bet/withdrawal/payout writes bet_placed/withdrawal_sent/payout_sent - an abandoned payout clears winner/fee/reserve amounts with payout_txid - the reconciler pings SSE only for rows it changed - closing_stalled every 30 minutes past the deadline; closing_waiting_since on /rounds/current - one refresh per user at a time; a failed refresh no longer ends the Electrum session Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
28 KiB
Transazioni e riconciliazione
Come ogni movimento di denaro — scommessa, prelievo, pagamento del round — viene registrato, inviato, accelerato e infine confrontato con la catena. L'obiettivo è che il sistema si rimetta in ordine da solo dopo un crash o un problema di rete, senza interventi manuali sul database, e che nel dubbio non liberi mai i fondi: una transazione si annulla solo con una prova che non è partita, mai per un timeout o per il silenzio di un solo server (decisione 2 di piano-correzioni.md).
I requisiti usano il prefisso TX, secondo la convenzione di
processo-di-modifica.md. Le colonne delle tabelle
pending_transactions e pending_transaction_txids sono descritte in
modello-dati.md, gli eventi di audit in
audit-log.md, le attività in background e le loro cadenze in
architettura.md.
Il principio: prima si scrive, poi si invia
Nessuna transazione viene inviata alla rete senza che il database ne abbia già
traccia. Ogni transazione in uscita ha una riga PendingTransaction che
attraversa questi stati (comportamento atteso):
stateDiagram-v2
[*] --> building: transazione firmata,<br/>monete segnate come spese,<br/>salvato PRIMA dell'invio
building --> pending: il server accetta l'invio<br/>(anche "già nota")
building --> building: invio incerto (timeout,<br/>connessione caduta): nulla liberato
building --> [*]: scommessa rifiutata esplicitamente<br/>(riga cancellata, tutto annullato)
building --> failed: prelievo rifiutato esplicitamente<br/>(tutto annullato)
building --> pending: il riconciliatore trova in rete<br/>uno dei suoi txid (dopo 120s)
building --> failed: il riconciliatore ha la prova<br/>che non è partita (dopo 120s)
pending --> pending: nessuna conferma<br/>entro il tempo limite → RBF
pending --> confirmed: prova merkle del quorum<br/>per uno dei suoi txid
pending --> failed: il riconciliatore ha la prova<br/>che non è partita (dopo 6h dal primo invio)
failed --> failed: conferma tardiva entro 14 giorni:<br/>pending_tx_resurrected + pausa
confirmed --> [*]
failed --> [*]: dopo 14 giorni
- [TX-01] Ogni transazione in uscita ha una riga
PendingTransactiondi tipobet,withdrawalopayout, con statobuilding,pending,confirmedofailed. La riga (statobuilding, con la transazione firmata completa) viene salvata e committata prima dell'invio, nella stessa transazione del database in cui le monete spese vengono segnate come tali e si registra l'oggetto collegato (partecipante, prelievo, dati di pagamento del round). Codice:app/bets/service.py:place_bet,app/withdrawals/service.py:request_withdrawal,app/rounds/scheduler.py:RoundScheduler._trigger_payout· Test:tests/unit/test_bets.py,tests/unit/test_scheduler.py - [TX-02] Il database non viene mai tenuto aperto durante una chiamata di
rete: la scrittura di
buildingè committata, poi si invia, poi si aggiorna (in una nuova sessione per il pagamento). Se il processo muore proprio in mezzo, resta la prova di cosa stava succedendo, non monete spese in catena senza alcun record. Codice:app/rounds/scheduler.py:RoundScheduler._trigger_payout,app/tx/broadcast.py:bump_fee· Test:tests/unit/test_scheduler.py,tests/unit/test_broadcast.py - [TX-03]
broadcast_atè l'orario in cui la riga è stata scritta, cioè del primo invio, e non viene mai riscritto;last_broadcast_atè quello dell'ultimo invio, ed è l'unico che l'RBF aggiorna. Codice:app/db/models.py:PendingTransaction,app/tx/broadcast.py:bump_fee· Test:tests/unit/test_broadcast.py - [TX-19] Tutte le versioni sono registrate. Ogni txid che una riga
ha avuto, con la sua transazione completa, è una riga di
pending_transaction_txids(modello-dati.md (B-77)): la prima versione è scritta insieme alla rigaPendingTransaction([TX-01]), ogni sostituzione RBF prima di essere inviata ([TX-12]). Conferma ([TX-06]), riconciliazione ([TX-15]) e sorveglianza delle righefailed([TX-23]) cercano ogni txid della riga, non solocurrent_txid. Quando la versione trovata in catena non è quella corrente,current_txideraw_tx_hexdiventano quelli della versione trovata e il txid viene propagato ovunque sia registrato, come dopo un RBF ([TX-12]). 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 del riconciliatore, la conferma ([TX-07]) — si apre conBEGIN IMMEDIATE, cioè prende il blocco in scrittura di SQLite prima delle proprie letture, così quelle letture non possono cambiare fino al commit. Se il blocco non si ottiene entro il tempo di attesa di SQLite (5 secondi,_SQLITE_BUSY_TIMEOUT_MS) l'operazione fallisce condatabase is lockede viene annullata: prima dell'invio non si è mosso nulla; dopo un invio riuscito le righe restanobuildinge vale [TX-20]. La durabilità dei commit (synchronous=FULL) è descritta in architettura.md (B-88). Codice:app/db/base.py:begin_immediate,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_sqlite_transactions.py
Invio rifiutato
Un invio può finire in tre modi, e solo uno di essi permette di liberare le monete:
| Esito | Cosa significa | Cosa si fa |
|---|---|---|
| Accettato | Il server risponde con il txid, oppure rifiuta ma almeno un server conosce la transazione | Si promuove la riga (BET-17, WDR-12) |
| Rifiutato esplicitamente | Il server risponde con un errore JSON-RPC e nessun server interrogato conosce la transazione | Si annulla tutto ([TX-04]) |
| Incerto | Timeout, connessione caduta, errore di scrittura sul socket, oppure un rifiuto la cui verifica non ottiene risposte: la transazione può essere partita | Non si libera nulla ([TX-20]) |
-
[TX-04] Solo un rifiuto esplicito — una risposta del server con un oggetto
errorJSON-RPC — all'invio di una scommessa o di un prelievo annulla tutto: monete liberate, saldo ripristinato, evento scritto nel registro di audit (bet_broadcast_failed,withdrawal_broadcast_failed), notifica SSE. Per una scommessa partecipante e riga vengono cancellati; per un prelievo prelievo e riga restano, in statofailed. L'utente ricevebroadcast_failedcon codice HTTP 502, perché è stata la rete a rifiutare, non la sua richiesta a essere sbagliata.Prima di annullare, il rifiuto si verifica. Il testo dell'errore non basta a distinguere un rifiuto vero da un "già nota" (i server usano messaggi diversi e non esiste un codice standard), quindi dopo ogni rifiuto si chiede
blockchain.transaction.getper il txid al server attivo e a tutti i server di riserva (10 s ciascuno):- se almeno uno restituisce la transazione, l'invio vale come accettato (la transazione è in rete);
- se tutti quelli che rispondono dicono di non conoscerla, e rispondono almeno il server attivo e il numero minimo di server del quorum (connessione-electrum.md), si annulla;
- se le risposte non bastano, l'esito è incerto ([TX-20]).
Il riconoscimento dei messaggi "già nota" (
already known,already in mempool,txn-already-known,txn-already-in-mempool,transaction already in block chain) resta solo una scorciatoia che salta la verifica: un messaggio non riconosciuto non può più far liberare le monete di una transazione partita, al massimo costa una verifica in più. Codice:app/tx/outcome.py:broadcast_with_verdict,app/electrum/client.py:ElectrumRpcError,app/electrum/listener.py:ElectrumListener.transaction_known,app/bets/service.py:_release_failed_bet,app/withdrawals/service.py:_release_failed_withdrawal· Test:tests/unit/test_broadcast_outcome.py,tests/unit/test_electrum_client.py,tests/unit/test_bets.py,tests/unit/test_withdrawals.py -
[TX-20] Invio incerto. Se l'invio di una scommessa o di un prelievo finisce senza una risposta del server (timeout, connessione caduta, errore di scrittura), o se il commit della promozione successiva a un invio accettato fallisce ([TX-28]), non si libera nulla: monete spese, partecipante o prelievo e riga
PendingTransactionrestanobuilding, e sarà il riconciliatore a stabilire se la transazione è partita ([TX-16], [TX-17]). L'utente riceve HTTP 202 con l'oggetto nello statobuilding(BET-23, WDR-16) e si invia una notifica SSE. Codice:app/bets/service.py:place_bet,app/withdrawals/service.py:request_withdrawal,app/api/routes/bets.py:create_bet,app/api/routes/withdrawals.py:create_withdrawal· Test:tests/unit/test_broadcast_outcome.py,tests/unit/test_sqlite_transactions.py -
[TX-21] Invio a tutti i server. Dopo che il server attivo ha accettato una transazione ([TX-04]) — scommessa, prelievo, pagamento o sostituzione RBF — la stessa transazione viene inviata anche a ogni altro server configurato. È un tentativo in più, non una condizione: un errore di 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,tests/unit/test_tx_versions.py -
[TX-05] Se l'invio di un pagamento non è accettato — rifiuto esplicito o esito incerto — la riga resta
buildinge si scrivepayout_failed(broadcast rejected): è il riconciliatore a decidere, interrogando la rete, se la transazione è partita o no ([TX-16], [TX-17]). Un rifiuto "già nota" ([TX-04]) vale come invio accettato: la riga passa apendinge si scrivepayout_sent. Codice:app/rounds/scheduler.py:RoundScheduler._trigger_payout· Test:tests/unit/test_scheduler.py,tests/unit/test_broadcast_outcome.py
Conferma
-
[TX-06]
ConfirmationPollercontrolla ogni 10 secondi le transazioni in statopending(non quellebuilding), e salta il giro se non c'è connessione. Per ciascuna legge la cronologia (blockchain.scripthash.get_history) dell'indirizzo che la spende — l'indirizzo dell'utente, o l'indirizzo del montepremi del round per un pagamento — e cerca ogni txid della riga ([TX-19]); un txid che vi compare con altezza maggiore di 0 è il candidato alla conferma, che viene accettata solo alle condizioni di [TX-22]. La cronologia è letta una volta 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,tests/unit/test_tx_versions.py,tests/unit/test_confirmation_proof.py -
[TX-22] Conferma verificata. Un candidato ([TX-06]) è confermato solo quando il quorum dei server (connessione-electrum.md (B-84)) restituisce per quel txid una prova merkle valida (
blockchain.transaction.get_merkle) contro l'header dell'altezza indicata. L'header è quello già validato dal listener (connessione-electrum.md (B-74)); se il listener non lo tiene più, è quello su cui il quorum concorda. Senza quorum, o con una prova non valida, la riga restapendinge si riprova al giro successivo. Codice:app/tx/confirmation.py:poll_once,app/electrum/listener.py:ElectrumListener.confirmation_proven,app/electrum/merkle.py:proof_matches_header· Test:tests/unit/test_confirmation_proof.py -
[TX-07] Alla conferma la riga passa a
confirmede, nella stessa transazione del database, viene chiamato il gestore del suo tipo; poi si invia una notifica SSE:Tipo Cosa succede alla conferma Scommessa Il partecipante (se buildingobroadcast) diventaconfirmed, conconfirmed_at, ed entra nell'estrazionePrelievo Il prelievo (se buildingobroadcast) diventaconfirmed, conconfirmed_atPagamento Il round (se paying_out) passa aclosed,closed_atdiventa l'orario della conferma e parte la pausa prima del successivoCodice:
app/tx/confirmation.py:poll_once,app/bets/confirmation.py:_on_bet_confirmed,app/withdrawals/confirmation.py:_on_withdrawal_confirmed,app/rounds/confirmation.py:_on_payout_confirmed· Test:tests/unit/test_confirmation.py -
[TX-08] I gestori ritrovano l'oggetto a cui la transazione si riferisce tramite identificativi fissi (round e utente per la scommessa, prelievo, round per il pagamento), mai principalmente tramite il txid, perché il txid cambia a ogni RBF; il txid serve solo come ripiego per righe prive di quei collegamenti. Codice:
app/bets/confirmation.py:_on_bet_confirmed,app/rounds/confirmation.py:_on_payout_confirmed· Test:tests/unit/test_confirmation.py
I gestori sono registrati all'avvio importando i moduli confirmation di
scommesse, round e prelievi (vedi architettura.md).
RBF: accelerare una transazione lenta
Se una transazione resta senza conferma oltre rbf_timeout_seconds (900s
predefiniti), RbfBumper la ricostruisce con una commissione più alta e la
reinvia (Replace-By-Fee). Tutte le transazioni della piattaforma segnalano la
sostituibilità fin dal primo invio.
- [TX-09]
RbfBumpercontrolla ogni 30 secondi le righe in statopending(salta il giro senza connessione) e accelera quelle il cuilast_broadcast_atè più vecchio dirbf_timeout_seconds: dopo ogni accelerazione la transazione ha di nuovo l'intero tempo limite. Un errore su una riga non ferma le altre. Codice:app/tx/broadcast.py:RbfBumper._tick,app/tx/broadcast.py:should_bump· Test:tests/unit/test_broadcast.py - [TX-10] La nuova commissione punta a 1 sat/vB in più della tariffa
registrata (
_FEE_RATE_INCREMENT), senza superare 5.000 sat/vB (MAX_FEE_RATE_SAT_VB); l'aumento assoluto non è mai inferiore alla dimensione stimata della transazione per 1 sat/vB, il minimo richiesto da BIP125 per una sostituzione. Una riga già a 5.000 sat/vB non viene più accelerata. Gli importi delle monete spese sono letti dalle transazioni precedenti come interi in satoshi. Codice:app/tx/broadcast.py:bump_fee,app/tx/broadcast.py:_prevout_amount· Test:tests/unit/test_broadcast.py - [TX-11] Chi paga l'accelerazione. La sostituzione spende le stesse
monete e riduce del solo aumento un'unica uscita, quella che torna alla
piattaforma o al mittente: il resto dell'utente per scommesse e
prelievi, l'uscita di riserva verso l'indirizzo di riserva del
montepremi (
m/84'/746'/0'/1/0) per i pagamenti (ciclo-di-vita-del-round.md (B-79)). Le uscite verso la controparte (montepremi, indirizzo esterno, vincitore, indirizzo commissioni) non vengono mai toccate. Se dopo la riduzione quell'uscita scenderebbe sottoDUST_LIMIT_SATS(294 sat), l'uscita viene eliminata e tutto il suo valore va in commissione: la sostituzione non ha più nulla da ridurre ed è quindi l'ultima, e la riga non viene più accelerata. Se l'uscita non esiste, o se nemmeno la sua eliminazione raggiunge l'aumento minimo di [TX-10], l'accelerazione non è possibile ([TX-24]). Una transazione che non si può accelerare restapendingfinché conferma o finché il riconciliatore ha la prova che non è partita ([TX-17]): finché almeno un server la tiene in mempool non viene mai abbandonata. Codice:app/tx/broadcast.py:bump_fee,app/tx/broadcast.py:_find_change_output· Test:tests/unit/test_broadcast.py,tests/unit/test_rbf_dust.py - [TX-12] Prima si scrive la sostituzione, poi si invia. La
sostituzione firmata viene registrata come candidata in
pending_transaction_txids(txid e transazione completa, [TX-19]) e committata prima dell'invio. Dopo l'invio (accettato o incerto) la riga viene aggiornata con un aggiornamento condizionato (UPDATE … WHERE id = … AND status = 'pending' AND current_txid = <txid sostituito>): nuovo txid (current_txid), nuova transazione (raw_tx_hex), txid sostituito (replaced_by_txid), tariffa effettivamente pagata, numero di tentativi elast_broadcast_at; nella stessa transazione il nuovo txid viene propagato ovunque sia registrato: monete spese (spent_txid), partecipante (bet_txid), prelievo (txid), 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,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 l'invio, se l'aggiornamento condizionato di [TX-12] non trova la riga (stato ocurrent_txidcambiati nel frattempo: confermata, abbandonata o già sostituita), è un conflitto: la riga non viene sovrascritta, la 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,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 eventorbf_bump_failed(audit-log.md (B-87)) con la riga, il txid corrente e il motivo; la riga restapendinge il tentativo si ripete al giro successivo in cui è dovuto ([TX-09]). L'evento si scrive una volta per coppia (txid corrente, motivo): un tentativo che fallisce di nuovo per lo stesso motivo sullo stesso txid va solo nei log, così una transazione non accelerabile non riempie il registro con una riga ogni 30 secondi. Codice:app/tx/broadcast.py:RbfBumper._tick,app/tx/broadcast.py:RbfBumper._record_failure· Test:tests/unit/test_rbf_dust.py
Il resto minimo di scommesse e prelievi è di 294 sat (BET-14, WDR-09), ma non basta a una riduzione: una scommessa con una moneta in ingresso e due uscite misura 141 vB, quindi l'aumento minimo a 1 sat/vB è di 141 sat e lascerebbe un resto di 153 sat, sotto la soglia di dust. Con un resto minimo la prima accelerazione elimina il resto ([TX-11]) ed è anche l'ultima; un resto più grande permette più accelerazioni prima di arrivare lì. Il pagamento del round, invece, non ha resto quando il montepremi contiene esattamente le scommesse del round: la riserva di [TX-11] serve proprio a dargli un'uscita da ridurre.
Riconciliazione
-
[TX-14]
PendingTransactionReconcilergira all'avvio e poi ogni 120 secondi (salta il giro senza connessione), e considera ogni transazione ancorabuildingopendingtrascorso il suo periodo di tolleranza, misurato dabroadcast_at(il primo invio, che l'RBF non sposta): 120s perbuilding, 6 ore perpending, così l'RBF ha il tempo di fare i suoi tentativi. Il periodo di tolleranza stabilisce solo da quando la riga viene esaminata: l'abbandono richiede comunque una prova ([TX-17]). Codice:app/tx/reconcile.py:PendingTransactionReconciler.run,app/tx/reconcile.py:_is_due· Test:tests/unit/test_reconcile.py -
[TX-15] Il riconciliatore chiede a ogni server del quorum (connessione-electrum.md (B-84)) la cronologia (
blockchain.scripthash.get_history) dell'indirizzo che spende la riga, e cerca ogni txid della riga ([TX-19]), in mempool o in un blocco. La transazione è presente se almeno un server conosce almeno uno dei suoi txid ([TX-16]); è assente solo se nessun server del quorum ne conosce alcuno, e anche in quel caso l'assenza da sola non basta ad abbandonarla ([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.get_histories· Test:tests/unit/test_reconcile_proof.py,tests/unit/test_reconcile.py,tests/unit/test_tx_versions.py -
[TX-26] Prima di scrivere un esito, il riconciliatore rilegge la riga nella propria transazione del database ([TX-28]) e non fa nulla se il suo stato o il suo
current_txidnon sono più quelli che aveva quando ha interrogato la rete (una conferma o un RBF avvenuti nel frattempo). Codice:app/tx/reconcile.py:reconcile_once· Test:tests/unit/test_reconcile_proof.py -
[TX-16] Presente in rete: una riga
buildingviene promossa apending(e partecipante o prelievo dabuildingabroadcast), con un eventopending_tx_recoverede l'evento che l'invio riuscito avrebbe scritto:bet_placedper una scommessa,withdrawal_sentper un prelievo,payout_sentper un pagamento. Se il txid trovato non è quello corrente vale [TX-19]. Da lì la conferma segue la via normale. Una riga giàpendingpresente in rete non viene toccata. Codice:app/tx/reconcile.py:_promote· Test:tests/unit/test_reconcile.py,tests/unit/test_state_consistency.py -
[TX-17] Abbandono solo con prova. Una riga viene abbandonata solo se vale una di queste due prove:
- un suo ingresso risulta speso in un blocco da una transazione che non è nessuna delle sue versioni ([TX-19]), confermato dal quorum (connessione-electrum.md (B-84));
- nessun server del quorum conosce alcuno dei suoi txid ([TX-15]) e
il reinvio della sua versione più recente (
raw_tx_hex) a tutti i server viene rifiutato esplicitamente perché gli ingressi mancano o sono già spesi (missing-inputs,bad-txns-inputs-missingorspent).
Un rifiuto del reinvio per commissione insufficiente non è una prova: una riga
pendingresta com'è e la accelera l'RBF ([TX-09]); una rigabuildingpassa apendingcome in [TX-16], così che l'RBF la possa accelerare. Un reinvio accettato da almeno un server significa che la transazione è viva: vale [TX-16]. Con la prova, la riga passa afailedcon il motivo infailure_reason; le monete che risultano ancora impegnate da questa transazione vengono liberate (non quelle già spese da altro); il partecipante viene rimosso, o il prelievo segnatofailedsenza txid; il saldo dell'utente viene ricalcolato; si scrivepending_tx_abandoned(con il numero di monete liberate) e si invia una notifica SSE. Codice:app/tx/reconcile.py:_abandon,app/tx/reconcile.py:_release_inputs,app/tx/reconcile.py:reconcile_once,app/electrum/listener.py:ElectrumListener.confirmed_spender,app/electrum/listener.py:ElectrumListener.rebroadcast_everywhere· Test:tests/unit/test_reconcile_proof.py,tests/unit/test_reconcile.py -
[TX-23] Le righe
failedrestano sorvegliate per 14 giorni dal passaggio afailed(failed_at, modello-dati.md (B-78)): a ogni giro il riconciliatore cerca i loro txid come in [TX-15]. Se uno di essi risulta confermato ([TX-22]) — una transazione data per morta che invece è arrivata in un blocco — si scrive una sola voltapending_tx_resurrected(audit-log.md (B-78)) e la lotteria viene messa in pausa (paused_reason = 'resurrected_tx') perché l'operatore rimetta in ordine partecipante, prelievo o pagamento; il riconciliatore non corregge da solo le righe collegate. Codice:app/tx/reconcile.py:reconcile_once,app/tx/reconcile.py:_resurrect,app/rounds/pause.py:pause_automatically· Test:tests/unit/test_reconcile_proof.py,tests/unit/test_pause_reason.py -
[TX-27] Il riconciliatore invia una notifica SSE solo per le righe di cui ha cambiato qualcosa (promozione, abbandono, riallineamento del txid), non per ogni riga esaminata. Codice:
app/tx/reconcile.py:reconcile_once· Test:tests/unit/test_state_consistency.py -
[TX-25] Una scommessa in volo esce dall'attesa del round in
closing(ciclo-di-vita-del-round.md) in due soli modi: confermata ([TX-07]) o abbandonata con prova ([TX-17]). Non esiste un tempo massimo: finché almeno un server tiene la transazione in mempool, il round resta inclosing. Codice:app/rounds/scheduler.py:RoundScheduler._tick,app/tx/reconcile.py:reconcile_once· Test:tests/unit/test_scheduler.py -
[TX-29] L'attesa in
closingsenza tempo massimo ([TX-25]) è visibile: quando un round resta inclosingoltre la scadenza per più diCLOSING_STALL_THRESHOLD_SECONDS(1800 s), lo scheduler scrive un eventoclosing_stalled(AUD-46) con il round, il numero di scommesse ancora in volo e i loro txid, e lo ripete ogni 1800 s finché l'attesa dura;GET /rounds/currentespone da quando il round attende (closing_waiting_since). L'operatore decide con la procedura di OPS-14; il sistema non esclude da solo una scommessa in volo dall'estrazione. Codice:app/rounds/scheduler.py:RoundScheduler._tick· Test:tests/unit/test_state_consistency.py
Pagamenti del round: riprovare senza pagare due volte
Il pagamento ha bisogno di più protezioni degli altri tipi, perché è
l'operazione che lo scheduler ritenta da solo. Le regole sono in
ciclo-di-vita-del-round.md:
ogni tentativo fallito scrive un evento payout_failed con il motivo, così
/admin mostra perché un round è fermo, e i tentativi sono limitati a
uno ogni 60 secondi usando proprio l'ultimo payout_failed come orologio
(RND-25); non si costruisce un
pagamento se ne esiste già uno building o pending per quel round, e le
monete del montepremi già impegnate da un pagamento non risolto sono escluse
dalla selezione (RND-26). Senza
queste ultime due, un nuovo tentativo potrebbe pagare il vincitore due volte.
- [TX-18] Un pagamento abbandonato ([TX-17]) non libera monete utente
(quelle del montepremi non sono registrate): azzera
payout_txid,winner_amount_satsefee_amount_satsdel round, che resta inpaying_outsenza transazione né importi di un pagamento che non esiste, e lo scheduler ricostruisce il pagamento al tentativo successivo. Codice:app/tx/reconcile.py:_abandon,app/rounds/scheduler.py:RoundScheduler._retry_payout_if_due· Test:tests/unit/test_state_consistency.py
Dove guardare
Nel pannello /admin (vedi guida-admin.md e
operativita.md):
- Transazioni pendenti — tutto ciò che è
buildingopending, con orari di invio e candidati all'RBF; - Audit log —
payout_sent,payout_failed,pending_tx_recovered,pending_tx_abandoned,draw_stallede tutti gli altri eventi; con le correzioni in corso ancherbf_bump_failed([TX-24]) epending_tx_resurrected([TX-23]); - Round — stato di ogni round, vincitore e txid del pagamento.