Files
plm-lottery/CLAUDE.md
T
davideandClaude Sonnet 5 df72367f02 Implement full MVP: auth, HD wallet, Electrum client, deposits, bets, round/draw engine, payout, withdrawals, RBF, admin+audit
All 10 build-order stages complete and unit-tested (49 tests). Verified live on
mainnet: registration/address derivation, deposit crediting, a real 10 PLM bet
(broadcast + confirmed + change credited). A full round close->draw->payout
cycle was triggered live and was in progress at commit time. Withdrawal and RBF
bump are unit-tested but not yet exercised against a live broadcast. Known gaps
(scheduler doesn't resume mid-flight rounds after restart, payout has no retry,
no deployment setup, etc.) are documented in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 23:52:20 +02:00

9.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Language

The user communicates in Italian in chat — reply to them in Italian. Everything written to the repository (code, comments, commit messages, docs, this file) must be in English. Reasoning/thinking should also be done in English.

Project status

All 10 build-order stages from /home/davide/.claude/plans/scalable-mixing-sloth.md are code-complete and unit-tested (49 tests green): project skeleton, DB schema + Alembic migrations, auth, HD wallet derivation, Electrum client, deposit detection, bet flow, round/draw engine, payout, withdrawal, RBF fee-bump, admin config + audit log.

Real-money verification on mainnet, done so far: registration + address derivation, deposit crediting (1-conf), a real 10 PLM bet (broadcast, confirmed, change credited back). A full round close → draw → payout cycle was triggered live and is in progress as of this commit (round closed, winner-draw was waiting on the next confirmed block) — not yet confirmed complete; withdrawal and the RBF bump path have only been verified with unit tests, never against a live broadcast. See "Known gaps" below before treating this as production-ready.

Before writing code, always read flowchart.mmd in full: every node in the diagram corresponds to a behavior that must be implemented exactly as described, including the labels on the edges (conditions, retries, loops).

Commands

source .venv/bin/activate                          # venv already created at .venv/
pip install -e ".[dev]"                             # install/update deps

alembic upgrade head                                # apply DB migrations
alembic revision --autogenerate -m "message"         # generate a new migration after editing app/db/models.py

PYTHONPATH=. python scripts/generate_master_key.py   # one-time: create+encrypt the server's master xprv (requires XPRV_ENCRYPTION_KEY in .env)

uvicorn app.main:app --reload --port 8123            # run the dev server

python -m pytest                                     # run all tests
python -m pytest tests/unit/test_hd.py               # run one test file
python -m pytest tests/unit/test_hd.py::test_derivation_is_deterministic  # run a single test

.env (gitignored) holds real secrets for local dev; .env.example documents the required keys and how to generate them.

Tech stack (MVP)

  • Backend language: Python.
  • PLM node access: Electrum protocol only (no full node/P2P). Bootstrap server for development: santantonio.sytes.net:50002 (SSL).
  • Auth: Argon2 password hashing + JWT sessions.
  • Secrets: master xprv encrypted at rest with a symmetric scheme (AES-GCM/Fernet); the encryption key itself lives in an env var, never in the DB or in git.
  • Operational config (fee/commission address, RBF fee-bump wallet, etc.): stored in a DB config table, not env vars — must be editable without a redeploy.
  • Round duration: configurable via env var, default 10 minutes (not hardcoded).

PLM network parameters

Source of truth: PalladiumWallet repo, ChainProfiles.cs and PalladiumNetworks.cs — always re-check that repo if a value is needed that isn't listed here, rather than guessing.

Mainnet:

  • BIP44/84 coin type: 746 (i.e. HD path m/84'/746'/0'/0/index)
  • Bech32 HRP: plm
  • P2PKH address version byte: 55 (addresses start with P)
  • P2SH address version byte: 5
  • WIF prefix: 0x80
  • Block time: 120s
  • BIP32 extended key headers (Legacy/native-segwit zprv/zpub etc.): see ExtKeyHeaders in ChainProfiles.cs

MVP business parameters

  • Fixed bet cost: 10 PLM per round.
  • Prize split: 70% winner / 30% fees (fee address configurable in DB).
  • Minimum deposit/withdrawal amount: 1 PLM (business-friendly floor, above the network's technical dust limit).
  • Confirmations required for all tx types (deposit, bet, payout, withdrawal): 1.

What is PLM Lottery

A periodic-round lottery system built on a Bitcoin-like coin (PLM, mainnet). Each user gets a dedicated P2WPKH address (server-side HD wallet); they deposit PLM to that address, place a fixed-cost bet to enter the current round, and when the round closes a winner is drawn who receives 70% of the prize pool (the remaining 30% goes to fees).

Architecture (from the flowchart subgraphs)

The flow is organized into 5 phases, each a subgraph in flowchart.mmd:

  • REG (Registration): on signup the server derives a new P2WPKH address via BIP84 (m/84'/coin'/0'/0/index, one index per user) from a master xprv encrypted at rest. This address is permanent and serves as both the deposit address and the address that receives winnings and withdrawals.
  • DEP (Balance top-up): an ElectrumClient/SPV subscribes to the user's address scripthash. Internal balance (DB) is credited after 1 confirmation only — the reorg risk at 1-conf is knowingly accepted in v1, with no rollback logic.
  • PLAY (Bet): fixed cost per round, at most one active bet per user at a time in v1. The server builds a PSBT user-address → pool-address for the fixed amount, with a change output back to the same user address (the user's balance must never exactly equal the bet amount). Fee minimized (~1 sat/vB), deducted from the bet amount. If the tx doesn't confirm within a timeout, fee-bump (RBF) and rebroadcast.
  • DRAW (Periodic draw): configurable timer (default 10 minutes). Round closing waits for all already-broadcast bets to confirm before proceeding (avoids losing bets at the round boundary). The next round only opens once the previous round's payout tx is confirmed — rounds never overlap in v1. v1 draw algorithm (deliberately simple, meant to be replaced later): wait for the first block confirmed after round closing, use its hash as seed, index = seed mod participant_count over the participant list ordered by broadcast timestamp (this is also the tie-break when two bets confirm in the same block). Every participant has equal probability regardless of bet amount (consistent with the fixed bet amount). The payout (70% winner / 30% fees) is signed with the pool address key; the payout fee is deducted from the winner's 70%, the 30% fee share stays intact. Same timeout → RBF → rebroadcast pattern here too.
  • WITHDRAW (Withdrawal): the only way to move funds out of the platform to an external address. PSBT user-address → external-address + change back to the user address, fee deducted from the withdrawn amount, same RBF retry pattern.

PLAY and WITHDRAW share a per-user DB lock: a user can never have a bet-build and a withdrawal-build in flight at the same time, since both would otherwise spend from the same UTXO set on the user's dedicated address.

Non-obvious domain decisions

These choices were made explicitly during design (not derivable from reading a single file) and must be respected in any implementation:

  • Private keys (xprv) are generated and held server-side — this is not a non-custodial system: the user never controls their own keys until they make an explicit withdrawal.
  • The user's personal deposit address always doubles as the winnings-receiving address: there is no separate "winner address".
  • 1 confirmation is the chosen threshold for all tx types (deposits, bets, payouts, withdrawals): don't introduce different thresholds (e.g. 3 or 6 confirmations) without an explicit decision.
  • The draw algorithm (node R) is deliberately simple and should be treated as a replaceable/pluggable component, not the final design — don't architect around its current implementation.

Known gaps / TODO

Not blockers for reading the code, but must be addressed before this is production-ready:

  • Scheduler doesn't resume mid-flight rounds after a restart. rounds/scheduler.py's _tick() only acts on rounds with status == "open". If the process restarts while a round is closing/drawing/paying_out, it's permanently stuck — nothing re-enters _wait_for_next_block or retries _trigger_payout. Needs a startup routine that inspects in-progress rounds and resumes (or a periodic "unstick" check) before this can run unattended.
  • RBF bump only handles one case: a single change output, paying back to the tx's own sender address, large enough to absorb the fee increase. No additional-input selection fallback — an exact-amount tx (no change) or a change output too small to absorb the bump raises RbfError and needs manual operator intervention. Documented in tx/broadcast.py.
  • Payout retry: if _trigger_payout fails (e.g. insufficient pool UTXOs, Electrum disconnected), it just logs and returns — the round stays stuck in paying_out with no automatic retry.
  • Withdrawal and RBF bump have never been exercised against a live broadcast — only deposit and bet flow are verified end-to-end with real PLM as of this commit.
  • No user-facing history endpoints (list my bets / withdrawals / past rounds) — only /users/me (balance) exists.
  • No deployment setup: no Dockerfile, process manager, or reconnect/supervision beyond the in-process asyncio tasks. Currently only run manually via uvicorn in a dev venv.
  • Admin auth is a single shared bearer token (ADMIN_TOKEN, X-Admin-Token header) — no per-admin identity or audit trail of who changed config.
  • No rate limiting / abuse protection on any endpoint (register, bet, withdrawal).
  • No automated integration tests against a live Electrum connection — all live-network verification so far has been manual (ad hoc scripts + real mainnet transactions), not part of the pytest suite.