Drop a dust-sized change into the fee on an RBF bump and audit every refused bump (B-87, TX-11, TX-24, AUD-40, AUD-09, ADR-19)

`bump_fee` only required `change > fee delta`. A change at the 294 sat minimum (a full-balance
withdrawal) shrank to ~153 sat after the very first minimum bump, relay policy refuses a
replacement with a sub-dust output, and the bumper tried again every 30 seconds for ever, with
nothing but a log line to show for it.

- When what would be left of the reducible output (the user's change, a payout's bump
  reserve) is under DUST_LIMIT_SATS the output is dropped and its whole value goes to the fee:
  that replacement is valid and the last one possible. If the output does not exist, or even
  its whole value is below BIP125's minimum increase (vsize x 1 sat/vB), RbfError. The
  recorded fee rate uses the replacement's own, smaller, size.
- RbfError now carries a `reason` (`change_too_small`, `max_fee_rate`, `replacement_rejected`);
  RbfBumper._record_failure writes one `rbf_bump_failed` row {pending_transaction_id, kind,
  txid, reason, message[:200]} with the row's user_id/round_id, in a transaction of its own,
  once per (current txid, reason) - the audit log is the dedupe clock (AUD-09) - and the pending
  row is untouched. A new txid or a new reason writes a new row.
- bump_fee's docstring no longer cites the "bet without change" case that B-62 removed.

No existing test changed (all bumps they make leave a change well above dust). New
tests/unit/test_rbf_dust.py: the 294 sat case end to end (recipient untouched, whole change to
the fee, last bump impossible), the exact boundary (leaves 294 kept / leaves 293 dropped), a
change under the minimum increase, a payout's reserve dropped with the winner and fee outputs
byte-identical, and the audit rows for each reason, once per pair, new row on a new txid or
reason, round/user attribution. Mutation-checked: the dust test of the output and the
once-per-pair check each fail tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-16 14:53:10 +02:00
1 parent 54dbffb552
commit ccd15807ed
11 files changed
+440 -108

No files matched your search

+27 -34
View File
@@ -33,7 +33,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
| Limite | Blocca il non presidiato oggi? | Dopo il piano |
|---|---|---|
| ~~[LIM-01]~~ `drawing` non riprende dopo un errore o un riavvio | Non più (chiuso da B-81) | — |
| [LIM-02] RBF di una sola forma (e pagamento senza resto) | **Sì, in caso di congestione della rete** | Chiuso da B-79 e B-87 |
| [LIM-02] RBF di una sola forma | Non più il pagamento senza resto (B-79) né il resto sotto soglia (B-87); resta una transazione che esaurisce resto o riserva | Visibile in `rbf_bump_failed`; resta in attesa senza abbandono |
| [LIM-03] Attesa in `closing` senza limite di tempo | **Sì, in caso di congestione** | Resta senza limite di tempo per scelta ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)); con B-78 e B-87 finisce quando la rete decide |
| [LIM-04] Prelievo e RBF mai provati in rete reale | **Sì, come verifica** | Da chiudere con la prova su rete reale ([operativita.md](operativita.md#prova-su-rete-reale)) prima di aprire al pubblico |
| ~~[LIM-10]~~ Riorganizzazioni non gestite | Non più (chiuso da B-82 e B-85) | Resta [LIM-24] |
@@ -50,38 +50,31 @@ implementato il [piano delle correzioni](piano-correzioni.md).
stesso vincitore ([RND-36](ciclo-di-vita-del-round.md)).
- **[LIM-02]** **L'RBF gestisce una sola forma di transazione**: un'unica uscita
di resto verso il mittente, abbastanza grande da assorbire l'aumento. Non c'è
il ripiego «aggiungi un'altra moneta». Ogni aumento costa almeno 1 sat/vB
sull'intera dimensione della transazione (141 sat per una transazione
con un ingresso e due uscite) e si toglie dal resto; quando l'aumento supera
il resto, `bump_fee` solleva `RbfError`. Inoltre `bump_fee` **non controlla
che il resto ridotto resti sopra la soglia di polvere**: con un resto vicino
al minimo di 294 sat ([ADR-19](adr/ADR-19-ogni-transazione-utente-conserva-uscita-resto.md)) già il primo aumento produce un'uscita sotto
soglia, che le regole di inoltro standard rifiutano. **Il pagamento del
round, nel caso normale, non ha alcun resto** (spende esattamente il
montepremi del round, e un resto sotto soglia va alla commissione), quindi
non è accelerabile affatto.
- *Impatto:* con la rete congestionata una transazione a 1 sat/vB può
restare in mempool indefinitamente. Poiché il riconciliatore abbandona solo
ciò che il server **non conosce**, una transazione ferma ma presente in
mempool non viene mai abbandonata: una scommessa così blocca il round in
`closing`, un pagamento così lo blocca in `paying_out`. `RbfBumper`
registra un errore ogni 30s.
- *Mitigazione attuale:* resto minimo garantito sulle transazioni utente;
tetto al fee rate (`MAX_FEE_RATE_SAT_VB`); abbandono dopo 6 ore per ciò che
sparisce dalla rete; ritentativo automatico del pagamento dopo un
abbandono.
- *Stato:* da chiudere con B-87 (la riserva del pagamento, B-79, c'è).
- *Per chiuderlo:* ogni pagamento porta un'uscita di riserva che
l'accelerazione riduce ([ADR-17](adr/ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md), B-79); un aumento che lascerebbe un
resto sotto `DUST_LIMIT_SATS` elimina il resto e ne versa il valore in
commissione, e quella sostituzione è l'ultima ([ADR-19](adr/ADR-19-ogni-transazione-utente-conserva-uscita-resto.md), B-87); ogni
`RbfError` e ogni sostituzione rifiutata scrivono `rbf_bump_failed` in
audit. Resta accettato, dopo il piano: nessun ripiego «aggiungi un'altra
moneta» né CPFP; una transazione che ha esaurito resto o riserva e non
conferma nemmeno al tetto del fee rate resta in attesa, visibile, e non
viene abbandonata ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)). Il docstring di `bump_fee` cita ancora come
esempio «una scommessa senza resto», caso che dopo B-62 non esiste più.
riducibile — il resto verso il mittente per scommesse e prelievi, l'uscita
di riserva per il pagamento ([RND-38](ciclo-di-vita-del-round.md#4--pagamento), B-79).
Non c'è il ripiego «aggiungi un'altra moneta» né il CPFP. Ogni aumento costa
almeno 1 sat/vB sull'intera dimensione della transazione (141 sat per una
transazione con un ingresso e due uscite) e si toglie da quell'uscita; se
quanto resta sarebbe sotto la soglia di polvere l'uscita viene eliminata e
il suo valore va in commissione, e quella sostituzione è l'ultima (B-87,
[ADR-19](adr/ADR-19-ogni-transazione-utente-conserva-uscita-resto.md)); se
nemmeno l'intera uscita raggiunge l'aumento minimo, o non esiste, `bump_fee`
solleva `RbfError`.
- *Impatto:* con la rete congestionata una transazione a 1 sat/vB che ha
esaurito resto o riserva può restare in mempool indefinitamente. Poiché il
riconciliatore abbandona solo ciò che il server **non conosce**, una
transazione ferma ma presente in mempool non viene mai abbandonata: una
scommessa così blocca il round in `closing`, un pagamento così lo blocca
in `paying_out`.
- *Mitigazione attuale:* la riserva del pagamento è dimensionata per circa
10 sat/vB; resto minimo garantito sulle transazioni utente; tetto al fee
rate (`MAX_FEE_RATE_SAT_VB`); ogni accelerazione impossibile o rifiutata
scrive `rbf_bump_failed` (una volta per transazione e motivo); abbandono
dopo 6 ore per ciò che sparisce dalla rete; ritentativo automatico del
pagamento dopo un abbandono.
- *Stato:* accettato dopo B-79 e B-87: una transazione che ha esaurito resto
o riserva e non conferma nemmeno al tetto del fee rate resta in attesa,
visibile, e non viene abbandonata ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)).
*Codice:* `app/tx/broadcast.py:bump_fee` · `app/wallet/psbt_builder.py:build_payout_transaction` · `app/tx/reconcile.py:reconcile_once`
@@ -95,7 +88,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
nuovo round si apre nel frattempo.
- *Mitigazione attuale:* lo stato è visibile (`closing`) e il pannello
«Transazioni pendenti» mostra la scommessa che trattiene il round.
- *Stato:* B-78 e B-77 chiusi; resta da chiudere B-87 (resto minimo), e per scelta l'assenza di un limite di tempo.
- *Stato:* B-78, B-77 e B-87 chiusi; resta per scelta l'assenza di un limite di tempo.
- *Per chiuderlo:* con B-78 l'attesa finisce quando la scommessa conferma
(una qualsiasi delle sue versioni, B-77) o quando c'è la prova che non
confermerà mai ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)), non dopo un tempo fisso; con B-87 la scommessa