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>
This commit is contained in:
@@ -44,28 +44,28 @@ const (
|
||||
|
||||
// Bet is one player's position in the current round.
|
||||
type Bet struct {
|
||||
AccountID int64
|
||||
Pubkey []byte
|
||||
Nickname string
|
||||
StakeMsat int64
|
||||
AccountID int64
|
||||
Pubkey []byte
|
||||
Nickname string
|
||||
StakeMsat int64
|
||||
CashedOutAt fixed.F // zero until they cash out
|
||||
PayoutMsat int64
|
||||
PayoutMsat int64
|
||||
}
|
||||
|
||||
// Snapshot is what clients render. It carries the seed inputs so a client can
|
||||
// verify the round the moment it settles.
|
||||
type Snapshot struct {
|
||||
RoundID int64 `json:"round_id"`
|
||||
Game string `json:"game"`
|
||||
State State `json:"state"`
|
||||
Tick int `json:"tick"`
|
||||
Multiplier string `json:"multiplier"`
|
||||
Commitment string `json:"commitment"`
|
||||
ServerSeed string `json:"server_seed,omitempty"` // only once settled
|
||||
CrashPoint string `json:"crash_point,omitempty"` // only once settled
|
||||
Players []Player `json:"players"`
|
||||
RoundID int64 `json:"round_id"`
|
||||
Game string `json:"game"`
|
||||
State State `json:"state"`
|
||||
Tick int `json:"tick"`
|
||||
Multiplier string `json:"multiplier"`
|
||||
Commitment string `json:"commitment"`
|
||||
ServerSeed string `json:"server_seed,omitempty"` // only once settled
|
||||
CrashPoint string `json:"crash_point,omitempty"` // only once settled
|
||||
Players []Player `json:"players"`
|
||||
HousePotMsat int64 `json:"house_pot_msat"`
|
||||
NextPhaseIn float64 `json:"next_phase_in_seconds"`
|
||||
NextPhaseIn float64 `json:"next_phase_in_seconds"`
|
||||
}
|
||||
|
||||
// Player is the public view of a participant.
|
||||
|
||||
@@ -91,6 +91,17 @@ func (f *fixture) startRun() {
|
||||
}
|
||||
}
|
||||
|
||||
// forceCrashPoint pins the round's crash point so cash-out tests do not depend
|
||||
// on a random draw. A genuine 1.00x crash is an instant bust where nobody can
|
||||
// cash out, which is correct behaviour but useless for testing the cash-out
|
||||
// path.
|
||||
func (f *fixture) forceCrashPoint(multiplier int64) {
|
||||
f.t.Helper()
|
||||
f.room.mu.Lock()
|
||||
f.room.crashPoint = fixed.FromInt(multiplier)
|
||||
f.room.mu.Unlock()
|
||||
}
|
||||
|
||||
// advanceTo moves the round to a specific tick without settling.
|
||||
func (f *fixture) advanceTo(tick int) {
|
||||
f.t.Helper()
|
||||
@@ -257,6 +268,7 @@ func TestCashOutOnlyWhileRunning(t *testing.T) {
|
||||
t.Fatal("cash out accepted during betting")
|
||||
}
|
||||
f.startRun()
|
||||
f.forceCrashPoint(100)
|
||||
if _, err := f.room.CashOut(id); err != nil {
|
||||
t.Fatalf("cash out rejected while running: %v", err)
|
||||
}
|
||||
@@ -268,6 +280,7 @@ func TestCannotCashOutTwice(t *testing.T) {
|
||||
f.openBetting()
|
||||
_ = f.room.PlaceBet(f.ctx, id, pk, "a", 1_000)
|
||||
f.startRun()
|
||||
f.forceCrashPoint(100)
|
||||
|
||||
if _, err := f.room.CashOut(id); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -282,6 +295,7 @@ func TestCannotCashOutWithoutABet(t *testing.T) {
|
||||
id, _ := f.player("a", 10_000)
|
||||
f.openBetting()
|
||||
f.startRun()
|
||||
f.forceCrashPoint(100)
|
||||
|
||||
if _, err := f.room.CashOut(id); err == nil {
|
||||
t.Fatal("cash out accepted with no bet placed")
|
||||
@@ -574,6 +588,7 @@ func TestConcurrentCashOutsYieldOne(t *testing.T) {
|
||||
f.openBetting()
|
||||
_ = f.room.PlaceBet(f.ctx, id, pk, "a", 5_000)
|
||||
f.startRun()
|
||||
f.forceCrashPoint(100)
|
||||
|
||||
const attempts = 10
|
||||
var wg sync.WaitGroup
|
||||
@@ -606,6 +621,7 @@ func TestSnapshotReportsCashOutMultiplier(t *testing.T) {
|
||||
f.openBetting()
|
||||
_ = f.room.PlaceBet(f.ctx, id, pk, "nick", 5_000)
|
||||
f.startRun()
|
||||
f.forceCrashPoint(100)
|
||||
|
||||
if _, err := f.room.CashOut(id); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user