soma.cell

Veterancy ranks — promotion is earned and can never be rolled back

Competitive games have shipped both halves of this bug: rank-rollback (disconnect during the promotion screen and log back in demoted — Halo and several ladder games paid out compensation waves for it) and rank-injection (a modified client reports Grandmaster and the server believes it). Both are writes the server should never accept. Here XP is walled into 0..100000 — no death-penalty underflow, no injected overflow — and rank is a FORWARD-ONLY machine: novice -> adept -> master -> grandmaster. soma verify proves there are no demotion edges and that grandmaster is terminal: a rank, once earned, is permanent.

A Soma program from the games_economy part of the verified corpus: 92 lines, cells Veterancy, RankTests. It passes soma check with soma 3.2.4.

How it is built

The cell Veterancy declares 4 signals in its face: earn_xp(player: String, xp_amt: Int) -> Map, claim_promotion(player: String) -> Map, xp_of(player: String) -> Int and rank_of(player: String) -> String; keeps 1 memory slot: xp (Map<String, Int>, persistent and consistent) under the invariant xp >= 0 && xp <= 100000. Its state machine rank starts in novice and allows 3 transitions: novice -> adept, adept -> master and master -> grandmaster. grandmaster is terminal. Handlers: earn_xp (line 35), claim_promotion (line 42), xp_of (line 65) and rank_of (line 66).

The file also holds 1 test cell (RankTests) with 15 assertions, run by soma test.

Generated from soma describe on this file.

The source

veterancy_ranks.cell

// ============================================================================
// VETERANCY RANKS — promotion is earned and can never be rolled back
// ============================================================================
// Competitive games have shipped both halves of this bug: rank-rollback
// (disconnect during the promotion screen and log back in demoted — Halo
// and several ladder games paid out compensation waves for it) and
// rank-injection (a modified client reports Grandmaster and the server
// believes it). Both are writes the server should never accept. Here XP is
// walled into 0..100000 — no death-penalty underflow, no injected overflow —
// and rank is a FORWARD-ONLY machine: novice -> adept -> master ->
// grandmaster. soma verify proves there are no demotion edges and that
// grandmaster is terminal: a rank, once earned, is permanent.
// ============================================================================

cell Veterancy {
    face {
        signal earn_xp(player: String, xp_amt: Int) -> Map
        signal claim_promotion(player: String) -> Map
        signal xp_of(player: String) -> Int
        signal rank_of(player: String) -> String
    }

    memory {
        xp: Map<String, Int> [persistent, consistent]
        invariant xp >= 0 && xp <= 100000
    }

    state rank {
        initial: novice
        novice -> adept
        adept -> master
        master -> grandmaster
    }

    on earn_xp(player: String, xp_amt: Int) {
        if xp_amt <= 0 { return map("ok", false, "reason", "xp must be positive") }
        let w = try { xp.set(player, (xp.get(player) ?? 0) + xp_amt) }
        if w.error != () { return map("ok", false, "reason", "xp out of range") }
        return map("ok", true, "xp", xp.get(player))
    }

    on claim_promotion(player: String) {
        let cur = rank_of(player)
        let have = xp_of(player)
        let next = ""
        let need = 0
        if cur == "novice" {
            next = "adept"
            need = 1000
        }
        if cur == "adept" {
            next = "master"
            need = 10000
        }
        if cur == "master" {
            next = "grandmaster"
            need = 50000
        }
        if next == "" { return map("ok", false, "reason", "{player} is already grandmaster") }
        if have < need { return map("ok", false, "reason", "need {need} xp, have {have}") }
        transition(player, next)
        return map("ok", true, "rank", next)
    }

    on xp_of(player: String) { return xp.get(player) ?? 0 }
    on rank_of(player: String) { return get_status(player) }
}

cell test RankTests {
    rules {
        assert earn_xp("li", 1200).ok == true
        assert claim_promotion("li").ok == true
        assert rank_of("li") == "adept"
        // not enough XP for master — and a client cannot inject a rank skip
        assert claim_promotion("li").ok == false
        assert_fails transition("li", "grandmaster")
        // NO DEMOTION EDGE EXISTS — the rollback bug is unrepresentable
        assert_fails transition("li", "novice")
        assert earn_xp("li", 50000).ok == true
        assert claim_promotion("li").ok == true
        assert claim_promotion("li").ok == true
        assert rank_of("li") == "grandmaster"
        // grandmaster is terminal: nothing left to claim, nothing to lose
        assert claim_promotion("li").ok == false
        assert_fails transition("li", "master")
        // XP cannot underflow on death penalties or overflow on injection
        assert earn_xp("li", -500).ok == false
        assert_fails xp.set("li", -1)
        assert_fails xp.set("li", 100001)
    }
}

What the toolchain says

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ soma check veterancy_ranks.cell
✓ All checks passed.
$ soma test veterancy_ranks.cell
test RankTests ...
  ✓ assert earn_xp("li", 1200).ok == true
  ✓ assert claim_promotion("li").ok == true
  ✓ assert rank_of("li") == "adept"
  ✓ assert claim_promotion("li").ok == false
  ✓ assert_fails transition("li", "grandmaster") — raised invalid transition: adept → grandmaster. Current state: 'adept'. Valid targets: [master]
  ✓ assert_fails transition("li", "novice") — raised invalid transition: adept → novice. Current state: 'adept'. Valid targets: [master]
  ✓ assert earn_xp("li", 50000).ok == true
  ✓ assert claim_promotion("li").ok == true
  ✓ assert claim_promotion("li").ok == true
  ✓ assert rank_of("li") == "grandmaster"
  ✓ assert claim_promotion("li").ok == false
  ✓ assert_fails transition("li", "master") — raised invalid transition: grandmaster → master. Current state: 'grandmaster'. Valid targets: []
  ✓ assert earn_xp("li", -500).ok == false
  ✓ assert_fails xp.set("li", -1) — raised memory invariant violated on 'xp': xp >= 0 && xp <= 100000 — rejected write of -1 (key "li"); the slot is unchanged
  ✓ assert_fails xp.set("li", 100001) — raised memory invariant violated on 'xp': xp >= 0 && xp <= 100000 — rejected write of 100001 (key "li"); the slot is unchanged

15 tests: 15 passed, 0 failed
$ soma verify veterancy_ranks.cell
Verifying veterancy_ranks.cell...

State machine 'rank': 4 states, initial 'novice'
  States: [adept, grandmaster, master, novice]

  ✓ 4 states, 3 transitions
  ✓ all states reachable from 'novice'
  ✓ terminal states: [grandmaster]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ⚠ dynamic transition targets: handler `claim_promotion` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
  ✓ termination: all 4 handlers structurally terminate
  ⚠ refinement (partial — dynamic targets fall back to runtime check): handler `claim_promotion` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
  ✓ refinement (partial — dynamic targets fall back to runtime check): handler `claim_promotion` ⟶ <dynamic>

Veterancy/invariants:

  ✓ invariant xp >= 0 — writer 'earn_xp' proven by induction (writes (xp.get(player) ?? 0) + xp_amt)
  ⚠ invariant xp >= 0 && xp <= 100000 — runtime-checked (computed values): earn_xp → xp [xp <= 100000] because `xp` has no known range

8 passed, 3 warnings, 0 failures

  Temporal properties for 'rank':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/games_economy/veterancy_ranks.cell · examples/corpus/games_economy/veterancy_ranks.cell