Commit Graph

15 Commits

Author SHA1 Message Date
drjones
1da3b6760e feat(admin): operations console, fees wired into payouts
Fees now flow through settlement. The payout and the deduction are posted
as separate ledger transactions rather than netted, so a player's history
shows the full win and the charge as itemised lines instead of a quietly
smaller win.

The admin console shows treasury, liability, revenue, every posting,
every round, and risk flags. Auth is a constant-time token compare and
the surface is not mounted at all unless ARCADE_ADMIN_TOKEN is set, so a
default deployment has no admin endpoint to attack. The token lives in
browser memory only.

It is read-only over game outcomes by design: seeds show only after
settlement and nothing can alter a crash point. A control that could
would make the fairness proof a lie.

The console immediately found a real bug: 343 unresolved rounds, because
the reconciler only considered rounds with bets and abandoned empty ones
accumulated forever, burying the signal. Now cleared automatically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:34:16 +00:00
drjones
e70258c54d feat(fees): disclosed rake and rounding, with generated disclosure
Two deductions on payouts: a percentage rake and flooring to whole
satoshis. Neither can be hidden — every millisatoshi is a double-entry
posting, so each appears as its own line in the player's history and the
conservation check fails if any amount goes unaccounted.

A rake makes the advertised 99% false, so EffectiveRTPBasisPoints
computes what players actually receive and the disclosure page is
rendered from it. A test simulates 200,000 rounds and asserts the
published figure matches what was really paid.

Fixes an overflow found by the tests: gross * RakeBP exceeds int64 for
large payouts, so the multiplication is split into whole and remainder
parts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:26:12 +00:00
drjones
f097721304 feat(lightning): deposits, withdrawals, and solvency behind a node interface
The node is an interface, so the code where money is actually at risk is
tested against a fake that can be made to fail, stall, or lie. Plugging
in Alby Hub is configuration, not new code.

Crediting a deposit is idempotent by payment hash: a node reporting the
same settlement twice must not mint money. Withdrawals debit before they
pay, because a payment that succeeds while the ledger write fails loses
money permanently, whereas the reverse is recoverable. Withdrawals above
a threshold wait for a human, which bounds what a stolen session token
can remove.

17 tests including concurrent settlement, concurrent double-spend,
concurrent processors, failed payment refunds, fee caps, and solvency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:23:51 +00:00
drjones
3bdb518f9c feat: refund abandoned rounds; full-journey and capacity tests
Fixes the money bug flagged earlier. When an instance died mid-round its
players had already been debited, so their stakes sat with the house:
balanced books, quietly robbed players. Every instance now sweeps for
unresolved rounds and refunds them.

Such a round is marked void, not settled. The schema caught this: the
reveal_is_complete constraint requires a settled round to publish its
seed, and an abandoned round has no outcome to reveal. Void is a distinct
state with its own column and a check that the two are exclusive.
Claiming happens before money moves, so concurrent reconcilers on
different instances refund exactly once.

Adds TestFullPlayerJourney: sign-in with no account, fund, scratch, bet
with an auto target, settle, verify the round independently, check the
ledger history is continuous, transfer to a friend, and confirm the books
still sum to zero. It asserts against the ledger rather than the API's
own summary.

Adds cmd/loadtest. One instance on 4 cores held 25,000 concurrent
websocket connections with zero failures at 586MB RSS, about 26KB per
connection, with the load generator competing for the same CPU.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:18:29 +00:00
drjones
c12640cf52 feat(cluster): zero-config horizontal scaling by cloning
An instance decides what it is at startup instead of being told: it
generates its own identity, registers a heartbeat, and campaigns for
each game. Exactly one instance drives a game's rounds and publishes
frames; the rest relay them and forward mutations to the leader. Clone
the VM, boot it, done.

Sessions and the scratch nonce move to Redis. Both were per-instance
state that would have broken behind a load balancer: a token minted by
one clone was unknown to the others, and two clones would have handed
the same nonce to different players, which for the same key means the
same outcome.

Fixes a bug found by running two instances: /api/games read the local
room object, so a follower reported a permanently settled game and its
clients never saw a betting window. Hubs now serve the last frame they
saw, produced or relayed.

Failover measured at 6s after kill -9 on an instance leading two games.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:07:47 +00:00
drjones
038550b6ff perf: single-statement postings, marshal-once broadcast, client interpolation
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>
2026-08-05 22:36:30 +00:00
drjones
5210266fd6 feat(pqid): hybrid post-quantum identity, AGPL-3.0 license
Player identity is now Ed25519 and ML-DSA-65 (NIST FIPS 204) together,
both signatures required. An attacker must break lattice assumptions and
elliptic curves, not either one — which covers both the quantum threat to
Ed25519 and the possibility that a 2024 lattice standard does not hold.

Signatures are domain-separated to this application so one captured from
another ML-DSA protocol cannot be replayed.

Licensed AGPL-3.0: a fork stood up as a service must publish its changes,
which is what keeps a provably-fair platform honest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 22:17:54 +00:00
drjones
48a9120fe4 feat: auto cash-out targets, 1% house edge, terminal aesthetic
Auto cash-out closes a position at exactly the chosen target rather than
the next tick's multiplier, and fires whenever the target is at or below
the crash point. This is the feature that makes the game playable over a
network, where manual timing is at the mercy of latency.

House edge drops from 2% to 1% across crash and scratch. Scratch prize
tables retuned so the published 99% RTP is exact.

Adds docs/API.md: the client uses no private endpoints, so anyone can
write a bot against the same API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:24:31 +00:00
drjones
dee3becd47 fix(sim): cap crash point so extreme seeds cannot overflow or bankrupt
At u=1 the unsigned quotient exceeded int64 and wrapped negative, so the
rarest and most valuable outcome silently became an instant 1.00x loss.
At u=2 it produced a 2.1-billion-times payout the house could never
cover, which would have left settlement failing and the player unpaid.
The crash point is now capped at the largest multiplier the curve can
express, which is unreachable anyway since the round hits its tick
ceiling first.

FromInt now panics outside the Q32.32 integer range instead of wrapping
a positive input into a negative value.

Raises coverage to 88% overall; adds a Makefile with db-reset, since the
append-only ledger steadily consumes bridge headroom across test runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:52:50 +00:00
drjones
f2c02e2bde fix(ledger): reject posting sets that overflow the zero-sum check
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>
2026-08-05 15:43:21 +00:00
drjones
2a2a1db8de feat: playable arcade — rooms, identity, client, deployment
Round length is now bounded: the multiplier follows a hyperbolic curve
diverging at 60s, replacing an exponential one where a 275x crash point
produced a two-and-a-half minute round.

Fixes seed reveal, which silently failed every round because pgx cannot
encode a fixed-size byte array as bytea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:34:52 +00:00
drjones
41c1bb2fdf feat(fair,scratch): add commit-reveal fairness and scratch tickets
Scratch odds tables are derived from the same tier list that generates
outcomes, so the published odds cannot drift from reality. Tests assert
observed frequencies and empirical RTP against the published figures;
the initial prize tables claimed 98% but actually paid 56%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:28:27 +00:00
drjones
8045e37c16 feat(ledger): add append-only double-entry engine
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>
2026-08-05 03:23:49 +00:00
drjones
8fccbb2a9a feat(sim): add deterministic RNG and crash curve
Seed expansion uses SplitMix64 so all 32 seed bytes affect the stream;
copying the seed directly into xoshiro state left the first draw
dependent only on bytes 8-15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:48:17 +00:00
drjones
9413537251 feat(fixed): add Q32.32 deterministic fixed-point arithmetic
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:19:06 +00:00