Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
The seed was the hash of the first block seen after the close, so a miner holding tickets
could withhold blocks until one made them win, the active Electrum server could pick which
real block was used by not announcing another (a failed corroboration even moved the draw to
the next block), and the participant order was the server-only `broadcast_at` with no
tie-break, so nobody could recompute the result.
- Open: every round gets a 32-byte `server_seed`, stored only Fernet-encrypted with the
master-xprv key (`server_seed_enc`), with its published `server_seed_hash`;
`draw_confirmations` is copied from round_config. The plaintext column is written only by
the reveal.
- Entering `drawing` (one transaction, `draw_committed` audit row): participants ordered by
bet txid, each `draw_position`, `participants_hash` (sha256 of `txid:vout:address\n`
lines; new `bet_vout`), `draw_height = tip_at_close + 1`. A tip of 0 keeps the round
`closing` (`draw_baseline_tip_unknown`, once per round). All persisted, so a resumed draw
reads rather than recomputes (the `_tick` branch itself is B-81).
- `_complete_draw` / `_wait_for_draw_block` replace `_wait_for_next_block`: wait for
`draw_confirmations` blocks from `draw_height`, then `ElectrumListener.agreed_block_hash`
asks every server, the active one included, for the header at that exact height (valid
work, >= 2 other answers, strict majority of all); no agreement means the same height is
asked again and `draw_header_corroboration_failed` is written once per distinct reported
hash. `draw_stalled` now counts from the last tip advance, or from when the quorum started
to fail. Seed = sha256(server_seed || block_hash || participants_hash), index = seed mod n.
- The winner, the seed and the reveal (`server_seed`, `winner_drawn` payload) are written in
one transaction; a round closing with no participants reveals too. A round opened before
this change (no commitment) is given a secret as it enters `drawing`.
- `GET /rounds/{id}/proof` (public, 404 `round_not_found`, translated in all 7 languages) and
the new fields on `GET /rounds/current`; `draw_confirmations` in `/admin/config` and the
/admin form; `server_seed_hash`/`draw_height` in `/admin/rounds`, never the secret.
- Alembic d3e8b15a7c42 (plain add_column / create_index, expression indexes untouched).
- Docs: the B-73 `*Stato:*`/`*Oggi:*` lines are gone, guides and CLAUDE.md describe the new
draw. Ambiguities resolved in api.md: the proof's participants also carry `amount_sats`
(RND-37 lists it); `seed` is decimal in the proof and hex in the audit row, as specified.
Tests changed because the spec changed: ElectrumListener.corroborate_header (asked only the
other servers about the block the active one announced) is replaced by agreed_block_hash and
its quorum tests ported; draw_winner takes the secret and the list fingerprint; the four
_wait_for_next_block scheduler tests are replaced by _wait_for_draw_block ones (fixed height,
confirmations, same-height retry, stall clock). New: test_draw_proof.py recomputes the winner
independently from the public proof, checks the plaintext secret is nowhere in the database,
the API or the log before the reveal, and that a second pass over a `drawing` round yields
the same winner. Mutation-checked: ordering by broadcast_at, an early reveal and asking the
tip instead of the draw height each fail tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82d078e2d6
commit
db24287ea5
42 files changed
+1715
-620
No files matched your search
@@ -83,6 +83,8 @@
|
||||
<input id="admin-round-cooldown" inputmode="numeric" placeholder="es. 30">
|
||||
<label for="admin-draw-animation">Durata animazione estrazione (secondi)</label>
|
||||
<input id="admin-draw-animation" inputmode="numeric" placeholder="es. 20">
|
||||
<label for="admin-draw-confirmations">Conferme sul blocco di estrazione (1–20, dal round successivo)</label>
|
||||
<input id="admin-draw-confirmations" inputmode="numeric" placeholder="es. 3">
|
||||
<label for="admin-fee-rate">Fee rate di rete (sat/vB)</label>
|
||||
<input id="admin-fee-rate" inputmode="numeric" placeholder="es. 1">
|
||||
<label for="admin-rbf-timeout">Timeout prima del fee-bump RBF (secondi)</label>
|
||||
|
||||
@@ -150,6 +150,7 @@ async function adminLoadConfig() {
|
||||
document.getElementById('admin-round-duration').value = data.round_duration_seconds;
|
||||
document.getElementById('admin-round-cooldown').value = data.round_cooldown_seconds;
|
||||
document.getElementById('admin-draw-animation').value = data.draw_animation_seconds;
|
||||
document.getElementById('admin-draw-confirmations').value = data.draw_confirmations;
|
||||
document.getElementById('admin-fee-rate').value = data.fee_rate_sat_vb;
|
||||
document.getElementById('admin-rbf-timeout').value = data.rbf_timeout_seconds;
|
||||
renderMaintenanceState(data.paused);
|
||||
@@ -209,6 +210,7 @@ async function adminSave() {
|
||||
round_duration_seconds: parseInt(document.getElementById('admin-round-duration').value, 10),
|
||||
round_cooldown_seconds: parseInt(document.getElementById('admin-round-cooldown').value, 10),
|
||||
draw_animation_seconds: parseInt(document.getElementById('admin-draw-animation').value, 10),
|
||||
draw_confirmations: parseInt(document.getElementById('admin-draw-confirmations').value, 10),
|
||||
fee_rate_sat_vb: parseInt(document.getElementById('admin-fee-rate').value, 10),
|
||||
rbf_timeout_seconds: parseInt(document.getElementById('admin-rbf-timeout').value, 10),
|
||||
};
|
||||
|
||||
@@ -149,6 +149,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'You have {pending_plm} PLM pending confirmation — it is not spendable yet.',
|
||||
'error.amount_below_network_fee': 'The amount is too small to cover the network fee.',
|
||||
'error.invalid_address': 'Not a valid PLM address (it must start with plm1q…).',
|
||||
'error.round_not_found': 'Round not found.',
|
||||
'error.amount_below_minimum': 'The minimum withdrawal amount is {minimum_plm} PLM.',
|
||||
'error.current_password_incorrect': 'The current password is incorrect.',
|
||||
'error.password_too_short': 'The new password must be at least {minimum} characters.',
|
||||
@@ -316,6 +317,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'Hai {pending_plm} PLM in attesa di conferma — non ancora disponibili per la spesa.',
|
||||
'error.amount_below_network_fee': "L'importo è troppo basso per coprire la fee di rete.",
|
||||
'error.invalid_address': 'Indirizzo PLM non valido (deve iniziare con plm1q…).',
|
||||
'error.round_not_found': 'Round non trovato.',
|
||||
'error.amount_below_minimum': "L'importo minimo di prelievo è {minimum_plm} PLM.",
|
||||
'error.current_password_incorrect': 'La password attuale non è corretta.',
|
||||
'error.password_too_short': 'La nuova password deve avere almeno {minimum} caratteri.',
|
||||
@@ -483,6 +485,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'Tienes {pending_plm} PLM pendientes de confirmación — todavía no se pueden gastar.',
|
||||
'error.amount_below_network_fee': 'El importe es demasiado pequeño para cubrir la comisión de red.',
|
||||
'error.invalid_address': 'Dirección PLM no válida (debe empezar por plm1q…).',
|
||||
'error.round_not_found': 'Ronda no encontrada.',
|
||||
'error.amount_below_minimum': 'El importe mínimo de retiro es {minimum_plm} PLM.',
|
||||
'error.current_password_incorrect': 'La contraseña actual no es correcta.',
|
||||
'error.password_too_short': 'La nueva contraseña debe tener al menos {minimum} caracteres.',
|
||||
@@ -650,6 +653,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'Vous avez {pending_plm} PLM en attente de confirmation — pas encore disponibles.',
|
||||
'error.amount_below_network_fee': 'Le montant est trop faible pour couvrir les frais de réseau.',
|
||||
'error.invalid_address': 'Adresse PLM invalide (elle doit commencer par plm1q…).',
|
||||
'error.round_not_found': 'Manche introuvable.',
|
||||
'error.amount_below_minimum': 'Le montant minimum de retrait est de {minimum_plm} PLM.',
|
||||
'error.current_password_incorrect': "Le mot de passe actuel n'est pas correct.",
|
||||
'error.password_too_short': 'Le nouveau mot de passe doit comporter au moins {minimum} caractères.',
|
||||
@@ -817,6 +821,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'Sie haben {pending_plm} PLM, die noch auf Bestätigung warten — noch nicht verfügbar.',
|
||||
'error.amount_below_network_fee': 'Der Betrag ist zu klein, um die Netzwerkgebühr zu decken.',
|
||||
'error.invalid_address': 'Keine gültige PLM-Adresse (sie muss mit plm1q… beginnen).',
|
||||
'error.round_not_found': 'Runde nicht gefunden.',
|
||||
'error.amount_below_minimum': 'Der Mindestauszahlungsbetrag beträgt {minimum_plm} PLM.',
|
||||
'error.current_password_incorrect': 'Das aktuelle Passwort ist nicht korrekt.',
|
||||
'error.password_too_short': 'Das neue Passwort muss mindestens {minimum} Zeichen lang sein.',
|
||||
@@ -984,6 +989,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': 'У вас есть {pending_plm} PLM, ожидающих подтверждения — они пока недоступны для расходования.',
|
||||
'error.amount_below_network_fee': 'Сумма слишком мала, чтобы покрыть комиссию сети.',
|
||||
'error.invalid_address': 'Некорректный адрес PLM (он должен начинаться с plm1q…).',
|
||||
'error.round_not_found': 'Раунд не найден.',
|
||||
'error.amount_below_minimum': 'Минимальная сумма вывода — {minimum_plm} PLM.',
|
||||
'error.current_password_incorrect': 'Текущий пароль указан неверно.',
|
||||
'error.password_too_short': 'Новый пароль должен содержать не менее {minimum} символов.',
|
||||
@@ -1151,6 +1157,7 @@ const TRANSLATIONS = {
|
||||
'error.balance_pending_confirmation': '您有 {pending_plm} PLM 待确认 —— 尚不可用于支出。',
|
||||
'error.amount_below_network_fee': '金额太小,不足以支付网络手续费。',
|
||||
'error.invalid_address': 'PLM 地址无效(必须以 plm1q… 开头)。',
|
||||
'error.round_not_found': '未找到该轮次。',
|
||||
'error.amount_below_minimum': '最低提现金额为 {minimum_plm} PLM。',
|
||||
'error.current_password_incorrect': '当前密码不正确。',
|
||||
'error.password_too_short': '新密码长度至少需要 {minimum} 个字符。',
|
||||
|
||||
Reference in new issue
Block a user