Commitments
A commitment is the atomic unit of confidential state:
Commitment
├── commitment_id deterministic digest over (owner, token, value, nonce)
├── owner the account that holds it
├── token the asset it is denominated in
├── value **private simulation state** (never observable output)
├── nonce deterministic blinding nonce
└── created_at ledger sequence of creation
Commitment IDs are a pure function of their content — no randomness beyond the seeded nonce — which is exactly what makes “same seed + same operation sequence ⇒ same commitments” hold across runs and machines.
Lifecycle
create (Active) ──consume──▶ consumed + nullifier registered
- Deposit creates an active commitment for the depositor.
- Transfer consumes the sender’s active commitments (each registers a nullifier) and creates fresh commitments for the recipient and change.
- Merge consumes several owned commitments and creates one carrying their summed value.
- Withdraw consumes commitments and moves value back to the public world (with a change commitment when the withdrawal is partial).
Every mutation goes through the store’s commitment lifecycle, which guards the invariants:
- Duplicate nonces are rejected — an identical live commitment cannot be created twice.
- Consumed commitments are rejected — replaying a spent commitment
fails with
consumed_commitment(double-spend protection). - Wrong owners are rejected — an account can only consume its own commitments.
Nullifiers
When a commitment is consumed, its nullifier — a deterministic digest of
the commitment ID — is registered in the store’s append-only registry.
The lifecycle is strictly unused → consumed and never consumed → reused: any attempt to spend the same commitment again would re-derive
the same nullifier and be rejected. The registry is a first-class part of
the state root, so replaying a store reproduces it exactly, and the fuzz
suite asserts the registry always matches the consumed set.
The privacy boundary
The commitment’s value is private simulation state. It lives in the
state store and is visible to the test harness — that is the point of a
simulator — but events, transactions, proofs, and published output never
carry it. Observable records reference commitments by ID only
(inputs, outputs on a transaction), never by value.
Merkle state
For flows that depend on accumulator-style structures, the state crate
provides a sparse Merkle tree over commitment-style leaves with
deterministic roots and membership paths (see docs/state-model.md and
the crucible_state::merkle module) — the shape crucible-prover will
consume for membership proofs.