From 6ef40a5739f1723ec26cc32dfc48fe764f776dca Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 19:57:45 -0400 Subject: fix: durable payouts survive journal failure, cap bank withdrawals, hash passwords off the owner loop. --- building_guide/concurrency.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'building_guide') diff --git a/building_guide/concurrency.md b/building_guide/concurrency.md index 984a61d..2fa4558 100644 --- a/building_guide/concurrency.md +++ b/building_guide/concurrency.md @@ -37,6 +37,17 @@ Rules: per-message write deadlines. A full queue disconnects only that client. - Saves run on the owner and serialize a value snapshot; concurrent read-modify-write cycles of the same files cannot happen. +- Password work is the one deliberate exception: Argon2id verification and + hashing (~64 MiB, tens of milliseconds) run on the CALLER's connection + goroutine in `HandleSessionOnOwner` (`StatePassword` and + `StateNewAccount` are intercepted before any owner task is submitted). + A burst of logins therefore costs memory and CPU on connection + goroutines, never a stall of the owner's command/tick queue. The owner + task re-checks the stored hash against the one that was verified so a + password change mid-login cannot authorize a stale attempt, and + `player.hashLimiter` caps concurrent Argon2id computations at 4. + All resulting state transitions (echo, session state, account saves) + still happen on the owner. Unit tests construct Games without an owner; such Games run submitted tasks inline on the caller's goroutine (legacy behavior), which is fine for @@ -126,6 +137,26 @@ an insufficient-funds message). A failed payout keeps its record open (the reported win is never announced as paid) and startup recovery re-pays the owed settlement. +If the JOURNAL ITSELF cannot be appended (persistent write failure, e.g. a +deleted or unwritable `data/economy`), appends are retried and then the +wallet degrades safely: + +- A **wager** is never charged without a journal record — the bet is + rejected with the persistence message (unchanged). +- A **payout** falls back to paying through the character file alone (the + save is the commit point), so the settlement is real, announced, and + durable even with no journal record. `ResolveStakes` then records the + unresolved stake IDs in the character file (`player.Player + .ResolvedStakes`) as durable resolution evidence, and recovery settles + those stakes without refunding them (no double pay, and a settlement's + chip stake is not returned in kind on top of credits that already + returned it). If the character save fails too, no evidence exists and + the open wager record drives the ordinary refund-on-lost-state recovery. +- A journal that cannot be fully READ (oversized or unreadable line) + aborts recovery and compaction entirely: records are preserved verbatim + for operator inspection instead of compensating over a partial fold or + truncating the file. + At startup (`game.New`), open records are compensated in two phases: 1. Open **payout** records (append order, evolving balance): file at the @@ -145,7 +176,10 @@ At startup (`game.New`), open records are compensated in two phases: Currency arithmetic is checked (`internal/casino/credits.go`): explicit `MaxCredits` cap, bounded `DefaultMaxBet` default (the explicit unlimited sentinel is still honored), floor conversion for payout multipliers, and -overflow rejected instead of wrapping. +overflow rejected instead of wrapping. The cap is shared by the wallet: +bank withdrawals are rejected when they would push the on-hand balance +past it, and gameplay credit grants (shop sales, pickups, activity +payouts) clamp at the cap and log rather than wrap. ## Account lifecycle transactions -- cgit v1.2.3