Files
davideandClaude Sonnet 5.5 40ac490826 Safe restore from a backup: START_PAUSED and the derivation-gap check (B-90, REG-15, OPS-27)
START_PAUSED=true pauses the lottery as the operator before the scheduler exists.
On the first Electrum session the 20 derivation indexes past the highest
assigned one are looked up on every server; if any has history, registration
stays closed (503 registration_unavailable) and restore_gap_detected is
written, so a restored database cannot hand a lost user's address to a new one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 00:43:45 +02:00

46 lines
2.1 KiB
Python

"""Pausing the lottery from code (B-78, DAT-69, AUD-11).
A pause only stops the *next* round from opening (rounds/service.py:rounds_can_open); a
round in progress still closes, draws and pays. Besides the operator's own switch there
are pauses the system raises itself when something needs a human look — a transaction
given up for dead that confirmed after all (`resurrected_tx`, B-78) and, in later
findings, a deep reorg (`reorg`, B-85) and a payout whose winning bet left the chain
(`payout_blocked`, B-82).
"""
from sqlalchemy.ext.asyncio import AsyncSession
from app.audit.log import write_audit_log
from app.rounds.config import get_round_config
OPERATOR_REASON = "operator"
AUTOMATIC_REASONS = ("reorg", "payout_blocked", "resurrected_tx")
async def pause_automatically(session: AsyncSession, reason: str) -> None:
"""Pause the lottery for an automatic `reason` in the caller's transaction and write
the `lottery_paused` row. An automatic reason is written even over a pause that is
already active (DAT-69): the cause that needs checking must not be hidden behind an
earlier one, and an operator pause is never what overrides it."""
if reason not in AUTOMATIC_REASONS:
raise ValueError(f"not an automatic pause reason: {reason!r}")
config = await get_round_config(session)
config.paused = True
config.paused_reason = reason
await write_audit_log(session, "lottery_paused", {"reason": reason})
async def pause_by_operator(session: AsyncSession, note: str | None = None):
"""The operator's pause (`POST /admin/pause`, B-90's `START_PAUSED`): never overwrites
an automatic reason already in force — that is the one that needs a look — and the
audit row always says `operator`. Returns the config row; the caller commits."""
config = await get_round_config(session)
if not (config.paused and config.paused_reason in AUTOMATIC_REASONS):
config.paused_reason = OPERATOR_REASON
config.paused = True
payload = {"reason": OPERATOR_REASON}
if note:
payload["note"] = note
await write_audit_log(session, "lottery_paused", payload)
return config