State model
Explicit transitions
The state engine never silently mutates state:
State0 ──operation──▶ Transition ──apply──▶ State1
Every applied operation produces a StateTransition recording the
operation, the concrete Changes, the state before, the state after, and
the events emitted. Recording is audited: a transition whose
resulting_state does not exactly match the store’s current fingerprint is
rejected as StateCorruption, so bookkeeping mistakes fail loudly.
Transitions chain: consecutive records satisfy
previous_state == preceding.resulting_state, and every transition
advances the state version by exactly one.
What the store owns
| Collection | Type key | Notes |
|---|---|---|
| Accounts | AccountId | Registration + permission state, confidential heads |
| Tokens | TokenId | Policy configuration |
| Commitments | CommitmentId | Value + blinding nonce (private simulation state) |
| Statuses | CommitmentId | Active / Consumed |
| Nullifiers | Nullifier | Replay protection; one per consumed commitment |
| Transactions | TransactionId | Deterministic records of applied operations |
| Events | EventId | Value-free observable output |
| Transitions | ordered | The audited transition log |
| Snapshots | name | Full-state captures |
All collections are BTree* so iteration order — and therefore the state
root — is deterministic.
State root
StateStore::state_ref() digests the version plus canonical JSON of every
account, token, commitment (+ status), and nullifier in sorted order. The
root is an internal simulation artifact (commitments carry private values
into it); it is what determinism tests compare and what transactions refer
to as state_before / state_after.
The commitment lifecycle
create (Active) ──consume──▶ Consumed + nullifier registered
create_commitmentrequires the owner and token to exist and rejects nonce collisions. Re-creating an already-consumed identical commitment is rejected as a double spend (ConsumedCommitment).- Consuming registers the commitment’s deterministic nullifier in the same step, so spending and replay protection cannot diverge.
- Flows spend all of an account’s active commitments for a token at once; balances are the sum of active commitment values.
Snapshots and rollback
TransactionScope::begincaptures a full-state checkpoint; dropping the scope withoutcommit()restores it exactly — failed operations can never leave partial state, nullifier gaps, or skipped sequence numbers.- Named snapshots capture the whole store;
restore_snapshotreplaces the store with the captured state and re-registers the snapshot, so the same snapshot can be restored repeatedly. Cost is O(state) — the deliberate trade for a deterministic testing environment.
Invariants enforced (and tested)
- Conservation — transfers never create or destroy value.
- Commitment integrity — consumed commitments are never reused; each consumption registers exactly one nullifier; active + consumed == created.
- Registration — unregistered accounts cannot hold or move confidential value on registration-required tokens.
- Atomicity — failed operations leave the state fingerprint bit-identical.
- Determinism — same inputs produce the same state, commitments, events, and outcomes.
The invariant suite (tests/state) drives deterministic pseudo-random
operation sequences to probe these properties.