Lesson 36 · Narrow Corner · Defensive Boundaries

One door for the balance cache

A tiny package, a dense lesson: containing corruption with a single un-bypassable invariant. ~10 min.

Builds on: L9 · L32 · L2 Anchor: uint256 raw balances New: gateway invariant New: corruption containment

A change of scale: after whole subsystems, here's a single 150-line file. But it's worth a lesson because it solves a bug class you'll meet anywhere there's a cache — corruption that spreads because the cache trusts whatever it's handed — and the fix is a pattern worth internalizing: make one invariant un-bypassable by making the package the only door.

Your anchor: a raw balance has a shape
An ERC-20 raw balance is an unsigned uint256 — never negative, and in practice nowhere near the top of its range (even a 10-trillion-supply token at 24 decimals is only ~124 bits). So a cached "balance" that is negative, or sits at BitLen = 256, is definitionally not a balance — it's garbage. That observation is the whole invariant.

1 · The bug class: sticky cache corruption

The hot cache balance:{chain}:{token}:{holder} in Redis speeds up balance lookups. Before this package, any writer could put anything in it — and several did: a pre-floor-fix indexer path, a reconciler that piped through a quirky RPC return, a genesis rebuild that propagated already-corrupt Neo4j quantity_raw. Each wrote a 2^256 − X garbage string. Then the corruption became sticky:

garbage write  →  next batch reads it as a SEED  →  re-writes ON TOP  →  garbage persists

The cache had no notion of "valid," so once garbage was in, it propagated forward and survived every cycle. (Observed on stage, 2026-05-19.) That's the failure this package exists to close.

2 · The fix: one typed gateway, one invariant

pkg/balancecache is the single typed gateway for that Redis key. Every read and write goes through it, and it enforces one predicate — IsPlausible:

func IsPlausible(v *big.Int) bool {
    return v != nil && v.Sign() >= 0 && v.BitLen() <= MaxBalanceBits   // non-nil, non-negative, ≤ 240 bits
}
SideBehavior on an implausible value
Write (Set / SetMany)reject with a WARN + ErrImplausibleBalance; skip the write (don't retry — it won't become plausible). SetMany returns a skipped count.
Readtreat as a cache miss — drop through to the fallback (re-fetch from chain). The same predicate is shared with the indexer's seed path, so read and write can never disagree on "valid."
Un-bypassable by construction
The point isn't just "validate inputs" — it's where. By making this package the only door to the key, the import graph itself forces every writer through the check. You can't write the cache without calling Set, and Set can't be made to store garbage. The invariant holds not by everyone remembering to check, but because there's no other path. That's the same one-door discipline as the single writer (L9), applied to a cache.

3 · The threshold: calibrate for safe-side margin

Why MaxBalanceBits = 240 specifically? A small calibration story worth keeping:

ValueBitLen
10-trillion-supply token @ 24 decimals (~1e37)~124
pathological 1e54 RWA tokenisation~180
the observed corruption class256
Put the line where nothing legit can reach it
The corruption sits at 256; any real balance is well under 200. So the threshold has enormous room — and they deliberately chose 240 over an earlier 200: both reject the 256-bit garbage identically, but 240 leaves 60+ bits of margin above any conceivable real token, so it can never reject a legitimate value. When the bad case is far from the good case, set the line to maximize the safe-side margin — the same anti-false-positive instinct as L32's loose lower bound.

4 · Two smaller choices

The transferable lesson
Three ideas in 150 lines: (1) a cache that trusts its inputs turns one bad write into permanent corruption; (2) the cure is a single typed gateway whose invariant the import graph makes un-bypassable, with the same predicate on read and write; (3) when garbage is far from legit, calibrate the threshold for maximum safe-side margin. None of it is balance-specific — it's how you defend any shared mutable cache.

Check yourself

1. What made the pre-gateway cache corruption "sticky" rather than self-correcting?
2. IsPlausible rejects a value that is negative or exceeds 240 bits. Why are those "definitionally not a balance"?
3. How does the package guarantee no writer can bypass the plausibility check?
4. On the read side, what happens when a cached value fails IsPlausible?
5. The corruption sits at BitLen 256 and any real balance is under ~200. Why set the threshold at 240 rather than 200?
6. The cache is written with no TTL. How do stale balances get corrected, then?
7. Why does Set require a non-empty source string?
8. Which earlier principle does the gateway most directly echo?
↳ Ask your teacher
Try: "Show me seedPreBatchBalance and how it shares IsPlausible." · "What other writers call Set / SetMany today?" · "How does the indexer fall back when a read misses?" · "Could the corrupt Neo4j quantity_raw still seed a bad value despite this?" · "Why a 60-bit margin and not, say, exactly at 200?"

What you can now do

Grounded in: pkg/balancecache/balancecache.go (single typed gateway for balance:{chain}:{token}:{holder}, IsPlausible non-nil/non-negative/BitLen≤MaxBalanceBits=240, Set/SetMany reject + WARN + ErrImplausibleBalance + skipped-count, read-as-miss shared with seedPreBatchBalance in pkg/indexer, no-TTL SET key val 0, mandatory source; the 2026-05-19 sticky-corruption bug class). Verify against source — the code is the truth.