package ledger_test import ( "context" "errors" "math/rand" "testing" "github.com/drjones/quantum-arcade/pkg/ledger" ) // Across a long run of random transfers, bets, and payouts, total value must // never change and no player balance may go negative. This is the property that // makes end-of-night settlement trustworthy. func TestRandomActivityConservesValue(t *testing.T) { l := ledger.New(testPool(t)) ctx := context.Background() // The test uses its own counterparty rather than the shared house account, // and sums only its own accounts. Asserting on a global total would fail // whenever another package's tests run against the same database in // parallel — a broken test, not a broken ledger. house, err := l.EnsurePlayer(ctx, uniqueKey(t, "counterparty")) if err != nil { t.Fatal(err) } if _, err := l.Deposit(ctx, house, 5_000_000); err != nil { t.Fatal(err) } const players = 8 ids := make([]int64, players) for i := range ids { id, err := l.EnsurePlayer(ctx, uniqueKey(t, string(rune('a'+i)))) if err != nil { t.Fatal(err) } ids[i] = id if _, err := l.Deposit(ctx, id, 100_000); err != nil { t.Fatal(err) } } // sumOwn totals only the accounts this test created. sumOwn := func() int64 { t.Helper() var total int64 for _, id := range append(append([]int64{}, ids...), house) { bal, err := l.Balance(ctx, id) if err != nil { t.Fatal(err) } total += bal } return total } before := sumOwn() rng := rand.New(rand.NewSource(1)) roundID := int64(0) for i := 0; i < 400; i++ { amt := int64(rng.Intn(5000) + 1) player := ids[rng.Intn(players)] var err error switch rng.Intn(3) { case 0: // peer transfer other := ids[rng.Intn(players)] if other == player { continue } _, err = l.Transfer(ctx, player, other, amt) case 1: // bet: player pays the house roundID++ r := roundID _, err = l.Post(ctx, "bet", &r, []ledger.Posting{ {AccountID: player, AmountMsat: -amt}, {AccountID: house, AmountMsat: amt}, }) case 2: // payout: house pays the player roundID++ r := roundID _, err = l.Post(ctx, "payout", &r, []ledger.Posting{ {AccountID: house, AmountMsat: -amt}, {AccountID: player, AmountMsat: amt}, }) } // Running out of funds is a legitimate outcome; nothing else is. if err != nil && !errors.Is(err, ledger.ErrInsufficientFunds) { t.Fatalf("iteration %d: %v", i, err) } } if after := sumOwn(); before != after { t.Fatalf("value not conserved: %d -> %d", before, after) } for _, id := range ids { bal, err := l.Balance(ctx, id) if err != nil { t.Fatal(err) } if bal < 0 { t.Fatalf("account %d went negative: %d", id, bal) } } } // Every transaction sums to zero, so the sum across all accounts including the // external bridge must be exactly zero at all times. func TestBooksAlwaysBalanceToZero(t *testing.T) { l := ledger.New(testPool(t)) ctx := context.Background() p, err := l.EnsurePlayer(ctx, uniqueKey(t, "p")) if err != nil { t.Fatal(err) } if _, err := l.Deposit(ctx, p, 7_777); err != nil { t.Fatal(err) } total, err := l.ConservationCheck(ctx) if err != nil { t.Fatal(err) } if total != 0 { t.Fatalf("books do not balance: total across all accounts = %d", total) } }