Raises bet throughput from ~166 to ~318 per second, measured. A 20-second
betting window now absorbs roughly 6,400 bets instead of 3,300.
A bet reserves synchronously in memory and writes in the background. The
reservation counts against the balance immediately, so two concurrent
spends of the same funds cannot both succeed while the first sits in the
buffer — twelve goroutines racing for one balance yield exactly one
winner.
The first attempt was slower than no batching at all, because Flush still
called Post per transaction and each kept its own commit. Amortising the
scheduling is worthless; the fsync is the cost. PostMany now writes the
whole batch in one database transaction, and a rejected group falls back
to individual writes to isolate the offender.
Safety rests on co-location: the reservation buffer and the round live in
the same process, so a crash loses both together — the player was not
charged and is not in the round. A round that flushed and then lost its
process is already handled by the reconciler.
Fixes a data race the detector found: MaxDelay was a public mutable field
read by the flush loop, so any operator tuning it live would have raced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured, then fixed, the three things that made a crowd impossible.
Ledger: Post issued three round trips per posting, so settlement scaled
in network latency rather than work. It is now two statements regardless
of leg count — settling 1000 winners went 844ms to 220ms. The lock and
the balance read must stay separate statements: a single statement, even
one whose CTE does FOR UPDATE, evaluates against a snapshot taken before
the locks are held, so concurrent transactions read stale balances and
money disappears. The conservation tests caught exactly that.
Broadcast: every connection marshalled its own copy, ~355us each. At any
real crowd that exceeds the tick interval by orders of magnitude. Frames
are now serialised once per broadcast and shared.
Feed: the player list is capped at 24 and carries no public keys, and
running rounds broadcast at 5Hz instead of 60Hz. Clients compute the
multiplier locally from the round start time, which the deterministic
curve makes exact. Frame size fell from 3.6KB to 1.8KB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An adversarial posting set of two MaxInt64 legs plus one of 2 wraps to
zero in int64 arithmetic, so the balance check passed and the ledger
minted 18 quintillion millisatoshis from nothing. The sum is now
accumulated in big.Int, per-account balance arithmetic is checked for
wraparound, and the audit totals parse through big.Int so a corrupt
ledger reports a clear error rather than failing to scan.
Adds room package tests (0% -> covered) and ledger edge cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Lightning bridge is modelled as the boundary with the outside
world and is the one account permitted to go negative; its negative
balance is exactly what is owed to players inside the system. All
other accounts are floored at zero by both the application and a
database trigger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>