Follow and record chain reorganisations, pausing on deep ones (B-85, ELC-09, ELC-22, ELC-23, ELC-24, AUD-38, DAT-69)
A reorganisation was neither recorded nor acted upon, and a competing header at the tip's own height was ignored for as long as the tip stood, leaving the listener on an orphan. - A header at the height we hold with a different hash is followed through the same fork-point search as any other reorg (ELC-09) instead of being ignored; it still has to bring valid work and a connected chain. - ElectrumListener._record_reorg writes `chain_reorg_detected` (depth, fork height, old and new tip height and hash) before the window moves, so a failed write ends the session and the next server's announcement of the same fork is recorded then. Depth 1 is a transaction of its own; depth >= 2 also pauses the lottery with paused_reason 'reorg' and writes `lottery_paused` in the same transaction (begin_immediate first). - Docs: ELC-09/22/23/24, AUD-03/11/38, DAT-30/69, OPS-30, RND-04, ADR-26/52 lose their B-85 "da implementare" lines. Tests changed because the spec changed: test_apply_header_ignores_a_competing_header_at_ the_current_tip_height encoded the B-64 behaviour that ELC-09 now reverses; it is replaced by test_a_competing_header_at_the_tip_height_is_a_reorg_to_follow and the depth tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca50ef62f3
commit
50b68d2957
16 files changed
+254
-111
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# ADR-26 — Validazione degli header concatenata, senza ricalcolo della difficoltà
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-85)
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **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*.
|
||||
|
||||
@@ -32,7 +32,4 @@ in [connessione-electrum.md](../connessione-electrum.md#4-header-validati-non-cr
|
||||
quorum ([ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)) e il fatto che il seme dipende anche da `server_seed`.
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/draw.py:header_meets_its_own_target` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py::test_a_multi_block_jump_is_validated_by_fetching_the_headers_in_between`, `tests/unit/test_electrum_listener.py::test_a_fork_is_walked_back_and_followed_instead_of_ending_the_session`, `tests/unit/test_draw.py::test_a_header_below_the_pow_limit_is_rejected_even_if_it_meets_its_own_target`
|
||||
*Stato:* da implementare (B-85), per l'header concorrente alla stessa
|
||||
altezza · *Oggi:* un header concorrente alla stessa altezza della punta è
|
||||
ignorato invece di essere seguito.
|
||||
*Test:* `tests/unit/test_electrum_listener.py::test_a_multi_block_jump_is_validated_by_fetching_the_headers_in_between`, `tests/unit/test_electrum_listener.py::test_a_fork_is_walked_back_and_followed_instead_of_ending_the_session`, `tests/unit/test_electrum_listener.py::test_a_competing_header_at_the_tip_height_is_a_reorg_to_follow`, `tests/unit/test_draw.py::test_a_header_below_the_pow_limit_is_rejected_even_if_it_meets_its_own_target`
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-52 — Le riorganizzazioni della catena si contengono invece di ignorarle
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-82, B-85)
|
||||
- **Stato:** Accettata — implementazione da completare (B-82)
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **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*.
|
||||
|
||||
@@ -42,9 +42,7 @@ e [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
|
||||
irrecuperabile e limitano il danno a ciò che l'attaccante ha rimosso
|
||||
([LIM-24]).
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-82, B-85) · *Oggi:* una riorganizzazione è
|
||||
seguita ma non registrata e non mette in pausa la lotteria; un header
|
||||
concorrente alla stessa altezza è ignorato; il pagamento non verifica le
|
||||
monete del round.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._record_reorg` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* il pagamento non verifica le monete
|
||||
del round: le prime due misure (rilevare, fermare) sono attive.
|
||||
@@ -528,7 +528,7 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
- il listener conserva gli ultimi 10 header ricevuti; un salto di più
|
||||
blocchi scarica e valida gli header intermedi, e un header che si
|
||||
concatena a uno di quelli conservati ma non alla punta è una
|
||||
riorganizzazione da seguire, non la fine della sessione (B-74, B-85);
|
||||
riorganizzazione da seguire, non la fine della sessione;
|
||||
- una sessione connessa la cui punta non avanza da 360s, mentre un altro
|
||||
server ne riporta una più alta, viene chiusa e si passa al server
|
||||
successivo (B-86);
|
||||
@@ -539,9 +539,7 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
`app/electrum/client.py:ElectrumClient.connect` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_electrum_client.py`
|
||||
*Stato:* da implementare (B-85, B-86) · *Oggi:* una riorganizzazione
|
||||
è seguita ma non registrata né segnalata con una pausa, un header
|
||||
concorrente alla stessa altezza è ignorato, e il listener non confronta la
|
||||
*Stato:* da implementare (B-86) · *Oggi:* il listener non confronta la
|
||||
propria punta con gli altri server.
|
||||
- **[ARC-27]** **`RoundScheduler`.** A ogni giro (5s) chiama
|
||||
`open_new_round_if_needed` e poi avanza il round attivo, in qualunque stato
|
||||
|
||||
+12
-13
@@ -43,9 +43,10 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure`,
|
||||
`app/tx/broadcast.py:RbfBumper._tick`, `app/main.py:lifespan`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header`
|
||||
*Stato:* da implementare (B-85, B-87, B-90, B-96), per i quattro eventi
|
||||
nuovi · *Oggi:* gli eventi osservativi sono solo i primi quattro.
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg`
|
||||
*Stato:* da implementare (B-87, B-90, B-96), per tre degli eventi
|
||||
nuovi · *Oggi:* gli eventi osservativi sono solo i primi quattro e
|
||||
`chain_reorg_detected`.
|
||||
|
||||
- **[AUD-04]** Il `payload` è un dizionario serializzato in JSON nella colonna
|
||||
`payload_json`. `created_at` è impostato dall'applicazione, non dal
|
||||
@@ -210,14 +211,14 @@ flowchart LR
|
||||
ogni pausa, anche se la lotteria era già in pausa. Stessa transazione di
|
||||
`paused = true`.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery`, `app/main.py:lifespan`,
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header`,
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/tx/reconcile.py:reconcile_once` ·
|
||||
*Test:* `tests/unit/test_pause_reason.py`
|
||||
*Stato:* da implementare (B-82, B-85, B-90) · *Oggi:* scritto da
|
||||
`POST /admin/pause` (`operator`) e dal riconciliatore (`resurrected_tx`);
|
||||
`reorg`, `payout_blocked` e l'avvio con `START_PAUSED` non lo scrivono
|
||||
ancora.
|
||||
*Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* scritto da
|
||||
`POST /admin/pause` (`operator`), dal riconciliatore (`resurrected_tx`) e dal
|
||||
listener (`reorg`); `payout_blocked` e l'avvio con `START_PAUSED` non lo
|
||||
scrivono ancora.
|
||||
|
||||
- **[AUD-12]** `lottery_resumed` — `POST /admin/resume`. Payload
|
||||
`{"reason"}`: il `paused_reason` che la ripresa ha tolto (`null` se la
|
||||
@@ -632,10 +633,8 @@ flowchart LR
|
||||
`lottery_paused` con `reason = "reorg"` ([AUD-11]). Una biforcazione più
|
||||
profonda di 10 header non viene seguita e non scrive questo evento (la
|
||||
sessione si chiude). Nessuna logica rilegge la riga.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-85) · *Oggi:* nessuna riorganizzazione è
|
||||
rilevata; l'evento non esiste.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._record_reorg` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
|
||||
## Cosa non viene registrato
|
||||
|
||||
|
||||
@@ -97,9 +97,8 @@ richiesta di scommessa (vedi
|
||||
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/rounds/service.py:rounds_can_open` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py`
|
||||
*Stato:* da implementare (B-85, B-79) · *Oggi:* l'unica pausa è quella
|
||||
manuale (`paused`) e `rounds_can_open` controlla solo che `fee_address` non
|
||||
sia vuoto.
|
||||
*Stato:* da implementare (B-79) · *Oggi:* `rounds_can_open` controlla solo
|
||||
che `fee_address` non sia vuoto.
|
||||
- **[RND-05]** All'apertura il round **fotografa** i parametri correnti che lo
|
||||
governano: durata (`round_duration_seconds`, 600s predefiniti), pausa
|
||||
successiva (`round_cooldown_seconds`, 30s predefiniti), costo della
|
||||
|
||||
@@ -169,9 +169,6 @@ chiudere la sessione, perché non indicano un server ostile:
|
||||
(lo stesso blocco riannunciato). Un header per l'altezza già nota con un
|
||||
hash **diverso** non è ignorato: è una riorganizzazione in punta e si
|
||||
segue secondo [ELC-23].
|
||||
*Stato:* da implementare (B-85), per il caso con hash diverso · *Oggi:*
|
||||
anche un hash diverso è ignorato e quello già adottato non viene mai
|
||||
sostituito.
|
||||
- **[ELC-10]** un header privo del contenuto binario: non c'è nulla da
|
||||
validare né da usare per l'estrazione.
|
||||
|
||||
@@ -236,9 +233,6 @@ passare a un altro server.
|
||||
un'altra catena), la sessione si chiude come per [ELC-11].
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._follow_reorg` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-85), per l'header con hash diverso alla stessa
|
||||
altezza della punta · *Oggi:* un header concorrente alla stessa altezza è
|
||||
ignorato ([ELC-09]); gli altri casi sono seguiti.
|
||||
- **[ELC-24]** Ogni riorganizzazione seguita scrive l'evento
|
||||
`chain_reorg_detected` con la profondità (numero dei nostri blocchi
|
||||
sostituiti, cioè vecchia altezza della punta meno altezza del punto di
|
||||
@@ -248,9 +242,12 @@ passare a un altro server.
|
||||
[modello-dati.md](modello-dati.md) B-85) finché l'operatore non la
|
||||
riprende: come ogni pausa, impedisce solo l'apertura del round successivo.
|
||||
Con profondità 1 si scrive solo l'evento.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header`, `app/db/models.py:RoundConfig` · *Test:* —
|
||||
*Stato:* da implementare (B-85) · *Oggi:* l'evento non esiste e nulla mette
|
||||
in pausa la lotteria da solo.
|
||||
L'evento è scritto **prima** che la finestra si sposti: se la scrittura
|
||||
fallisce la sessione termina e la stessa biforcazione, annunciata dal server
|
||||
successivo, viene registrata allora (nessuna riorganizzazione è seguita
|
||||
senza traccia).
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._record_reorg`, `app/electrum/listener.py:ElectrumListener._follow_reorg`, `app/db/models.py:RoundConfig` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
|
||||
## 5. Il quorum
|
||||
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
| [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-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 | **Sì** — una scommessa sparita rende il round impagabile | Chiuso da B-82 e B-85; resta [LIM-24] |
|
||||
| [LIM-10] Riorganizzazioni non gestite | **Sì** — una scommessa sparita rende il round impagabile | Chiuso da B-82 e B-85 (la pausa e il registro sono attivi; resta il pagamento, B-82); resta [LIM-24] |
|
||||
| [LIM-11], [LIM-12] Quorum facoltativo e parziale | **Sì** — un solo server decide conferme e abbandoni | Chiusi da B-83 e B-84 (resta la lettura delle monete del montepremi, B-82) |
|
||||
| [LIM-13] Estrazione manipolabile | **Sì** — per un giocatore che mina | Chiuso da B-73; resta [LIM-25] |
|
||||
| [LIM-14] Header senza difficoltà minima | **Sì** — un header falso congela ogni estrazione | Chiuso da B-74 per il blocco; resta il costo basso di un header a difficoltà minima |
|
||||
@@ -250,7 +250,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
su una moneta scomparsa è rifiutata dalla rete e annullata; per una
|
||||
scommessa già confermata non esiste alcuna mitigazione (la versione
|
||||
precedente di questa voce diceva il contrario).
|
||||
- *Stato:* da chiudere con B-82 e B-85.
|
||||
- *Stato:* da chiudere con B-82; le riorganizzazioni sono già rilevate, registrate e (se profonde) in pausa (B-85).
|
||||
- *Per chiuderlo:* depositi a `deposit_confirmations` conferme e coinbase a
|
||||
120 ([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)); riorganizzazioni rilevate, registrate e, se profonde,
|
||||
con pausa; monete del round verificate prima del pagamento, con i
|
||||
|
||||
@@ -826,8 +826,8 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
[DAT-69], che scrivono la propria voce di audit.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery` ·
|
||||
`app/api/routes/admin.py:resume_lottery` · *Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-82, B-85) · *Oggi:* l'unica pausa automatica è
|
||||
`resurrected_tx` (B-78); quelle di B-82 e B-85 non esistono ancora.
|
||||
*Stato:* da implementare (B-82) · *Oggi:* le pause automatiche sono
|
||||
`resurrected_tx` (B-78) e `reorg` (B-85); quella di B-82 non esiste ancora.
|
||||
|
||||
- **[DAT-62]** `deposit_confirmations` (predefinito 6) è il numero di
|
||||
conferme dopo cui un output che paga un utente si accredita ([DAT-10]);
|
||||
@@ -864,12 +864,12 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
successivo.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery` ·
|
||||
`app/api/routes/admin.py:resume_lottery` ·
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
`app/tx/reconcile.py:reconcile_once` · *Test:* `tests/unit/test_pause_reason.py`
|
||||
*Stato:* da implementare (B-82, B-85, B-90) · *Oggi:* si scrivono solo
|
||||
`operator` (`POST /admin/pause`) e `resurrected_tx` (il riconciliatore);
|
||||
`reorg`, `payout_blocked` e `START_PAUSED` non hanno ancora chi li scrive.
|
||||
`app/tx/reconcile.py:reconcile_once` · *Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* si scrivono
|
||||
`operator` (`POST /admin/pause`), `resurrected_tx` (il riconciliatore) e
|
||||
`reorg` (il listener); `payout_blocked` e `START_PAUSED` non hanno ancora chi li scrive.
|
||||
|
||||
- **[DAT-31]** Con `fee_address` vuoto (o di soli spazi) non si apre nessun round
|
||||
(B-66). Una volta impostato, l'API non permette di svuotarlo di nuovo: la
|
||||
|
||||
+5
-5
@@ -815,13 +815,13 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery`,
|
||||
`app/api/routes/admin.py:resume_lottery`,
|
||||
`app/rounds/service.py:open_new_round_if_needed`,
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header`,
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg`,
|
||||
`app/tx/confirmation.py:poll_once`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-85, B-82, B-90) · *Oggi:* il sistema mette in
|
||||
pausa la lotteria solo per `resurrected_tx` (B-78); `reorg` e
|
||||
`payout_blocked` non esistono ancora.
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* il sistema mette in
|
||||
pausa la lotteria per `resurrected_tx` (B-78) e `reorg` (B-85);
|
||||
`payout_blocked` non esiste ancora.
|
||||
|
||||
### Intervento manuale sui fondi
|
||||
|
||||
|
||||
+2
-2
@@ -92,8 +92,8 @@ Restano **accettati**, e descritti in
|
||||
riorganizzazioni con pausa sopra la profondità 1 e verifica delle monete
|
||||
del round prima del pagamento ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md), B-82,
|
||||
B-85). Il denaro che un attaccante riesce a sottrarre è perso per la
|
||||
piattaforma. *Oggi:* depositi a 1 conferma, nessuna riorganizzazione
|
||||
rilevata, nessuna verifica prima del pagamento.
|
||||
piattaforma. *Oggi:* depositi a 1 conferma e nessuna verifica prima
|
||||
del pagamento (B-82); le riorganizzazioni sono rilevate e, da profondità 2, mettono in pausa.
|
||||
|
||||
## Inventario dei segreti
|
||||
|
||||
|
||||
+3
-3
@@ -233,7 +233,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | **aperto** | — | DEP, RND |
|
||||
| B-83 | Conferme e reintegri si basano solo sul server attivo | Alta | *da registrare* | `test_confirmation_proof.py`, `test_confirmation.py` | ELC, TX, DEP |
|
||||
| B-84 | Il quorum può ridursi a un solo server o a nessuno senza avvisi; certificati TLS non verificati | Alta | *da registrare* | `test_electrum_client.py`, `test_electrum_listener.py` | ELC, SEC |
|
||||
| B-85 | Le riorganizzazioni della catena non vengono né seguite né segnalate | Alta | **aperto** | — | ELC, RND |
|
||||
| B-85 | Le riorganizzazioni della catena non vengono né seguite né segnalate | Alta | *da registrare* | `test_electrum_listener.py` | ELC, RND |
|
||||
| B-86 | Un server connesso ma fermo non viene mai sostituito | Alta | **aperto** | — | ELC, OPS |
|
||||
| B-87 | L'RBF può lasciare un resto sotto la soglia di dust | Alta | **aperto** | — | TX |
|
||||
| B-88 | Le transazioni SQLite non sono quelle che il codice presuppone; `synchronous=NORMAL` può perdere gli ultimi commit | Alta | *da registrare* | `test_sqlite_transactions.py`, `test_db_base.py` | DAT, ARC |
|
||||
@@ -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, B-77, B-78, B-83, B-74.
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85.
|
||||
- **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 (4 critici,
|
||||
6 alti, 6 medi), da implementare nell'ordine di
|
||||
5 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
Reference in new issue
Block a user