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:
drjones
2026-08-05 15:52:50 +00:00
parent f2c02e2bde
commit dee3becd47
14 changed files with 790 additions and 42 deletions

199
pkg/fixed/edge_test.go Normal file
View File

@@ -0,0 +1,199 @@
package fixed
import (
"math"
"testing"
)
// The simulation's verifiability depends on this arithmetic behaving
// identically everywhere, including at the extremes. These tests attack the
// boundaries.
func TestMulByZeroAndOne(t *testing.T) {
for _, v := range []int64{0, 1, -1, 1000, -1000, 1 << 20} {
a := FromInt(v)
if got := a.Mul(0); got != 0 {
t.Errorf("%d * 0 = %v, want 0", v, got)
}
if got := a.Mul(One); got != a {
t.Errorf("%d * 1 = %v, want %v", v, got, a)
}
}
}
func TestDivByOneAndSelf(t *testing.T) {
for _, v := range []int64{1, -1, 7, -7, 1000, 1 << 20} {
a := FromInt(v)
if got := a.Div(One); got != a {
t.Errorf("%d / 1 = %v, want %v", v, got, a)
}
if got := a.Div(a); got != One {
t.Errorf("%d / %d = %v, want 1", v, v, got)
}
}
}
func TestDivByZeroPanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("division by zero did not panic")
}
}()
_ = One.Div(0)
}
func TestSqrtOfNegativePanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("sqrt of a negative did not panic")
}
}()
_ = Sqrt(FromInt(-1))
}
// Multiplication must stay associative-ish and exact for representable values,
// which is what keeps a replayed round identical to the original.
func TestMulIsExactForFractions(t *testing.T) {
cases := []struct {
a, b, want F
}{
{One / 2, One / 2, One / 4},
{One / 4, One / 4, One / 16},
{One / 2, One / 4, One / 8},
{One * 3 / 2, One * 2, One * 3},
}
for _, c := range cases {
if got := c.a.Mul(c.b); got != c.want {
t.Errorf("%v * %v = %v, want %v", c.a, c.b, got, c.want)
}
}
}
// Round-tripping a value through multiply and divide must return it exactly
// for powers of two, where no precision can be lost.
func TestMulDivRoundTripOnPowersOfTwo(t *testing.T) {
for shift := 0; shift < 20; shift++ {
v := FromInt(1 << shift)
for _, by := range []F{One * 2, One * 4, One * 8} {
if got := v.Mul(by).Div(by); got != v {
t.Errorf("2^%d round trip through %v gave %v, want %v", shift, by, got, v)
}
}
}
}
func TestSqrtIsMonotonic(t *testing.T) {
prev := Sqrt(0)
for i := int64(1); i < 5000; i++ {
cur := Sqrt(FromInt(i))
if cur < prev {
t.Fatalf("Sqrt decreased at %d: %v -> %v", i, prev, cur)
}
prev = cur
}
}
// Sqrt must never overshoot: its square must not exceed the input.
func TestSqrtNeverOvershoots(t *testing.T) {
for i := int64(0); i < 20000; i++ {
a := FromInt(i)
r := Sqrt(a)
if r.Mul(r) > a {
t.Fatalf("Sqrt(%d) = %v squares to %v, which exceeds %v", i, r, r.Mul(r), a)
}
}
}
func TestSqrtOfLargeValues(t *testing.T) {
// Values in the range the crash curve actually produces, up to the
// representable maximum.
for _, v := range []int64{1_000_000, 12_960_000, 100_000_000, MaxInt} {
a := FromInt(v)
r := Sqrt(a)
if r <= 0 {
t.Fatalf("Sqrt(%d) = %v, want positive", v, r)
}
if r.Mul(r) > a {
t.Fatalf("Sqrt(%d) overshoots", v)
}
}
}
func TestIntTruncatesTowardNegativeInfinity(t *testing.T) {
cases := []struct {
in F
want int64
}{
{One, 1},
{One + One/2, 1},
{One*2 - 1, 1},
{0, 0},
{-One, -1},
}
for _, c := range cases {
if got := c.in.Int(); got != c.want {
t.Errorf("(%v).Int() = %d, want %d", c.in, got, c.want)
}
}
}
func TestFromIntPanicsOutsideRange(t *testing.T) {
for _, v := range []int64{MaxInt + 1, MinInt - 1, 4_000_000_000, -4_000_000_000} {
func() {
defer func() {
if recover() == nil {
t.Errorf("FromInt(%d) did not panic", v)
}
}()
_ = FromInt(v)
}()
}
// The boundaries themselves must be accepted.
_ = FromInt(MaxInt)
_ = FromInt(MinInt)
}
func TestStringNeverPanicsAcrossRange(t *testing.T) {
values := []F{
0, 1, -1, One, -One, One / 3, math.MaxInt64, math.MinInt64 + 1,
FromInt(4_000_000),
}
for _, v := range values {
if s := v.String(); s == "" {
t.Errorf("String() of %d returned empty", int64(v))
}
}
}
// Addition and subtraction are plain integer ops, but the inverse property is
// what payout arithmetic relies on.
func TestAddSubAreInverse(t *testing.T) {
for _, a := range []F{0, One, -One, One * 12345, One / 7} {
for _, b := range []F{0, One, -One, One * 999} {
if got := a.Add(b).Sub(b); got != a {
t.Errorf("(%v + %v) - %v = %v, want %v", a, b, b, got, a)
}
}
}
}
// Determinism check: the same operations in the same order must produce
// bit-identical results every time, which is the whole premise of replay.
func TestOperationsAreBitStable(t *testing.T) {
compute := func() F {
acc := One
for i := int64(1); i < 500; i++ {
acc = acc.Mul(One + One/F(i+1))
acc = acc.Div(One + One/F(i+2))
acc = acc.Add(FromInt(i % 3))
acc = Sqrt(acc)
}
return acc
}
first := compute()
for i := 0; i < 200; i++ {
if got := compute(); got != first {
t.Fatalf("run %d diverged: %v != %v", i, got, first)
}
}
}