Deterministic execution
Determinism is the defining property of Crucible:
same configuration
+ same initial state
+ same operation sequence
+ same seed
= same resulting state + same events + same commitments + same outcomes
Where nondeterminism would hide — and how the code prevents it
| Source of nondeterminism | Prevention |
|---|---|
| System entropy (randomness) | All randomness flows through DeterministicRng (SplitMix64), a pure function of a u64 seed. Commitments, proofs, and identities never touch the system RNG. |
| Wall-clock time | Time is a synthetic LedgerContext advanced by a fixed configured step (protocol.step_seconds). Nothing reads the clock. |
| Iteration order | Every state collection is a BTree* map/set, so iteration is sorted. Hash* collections are never used for state. |
| Hash randomization | Hashing is domain-separated SHA-256 over canonical byte/JSON inputs, never DefaultHasher. |
| Serialization drift | Transaction IDs digest canonically serialized payloads (serde_json), so they do not depend on Rust formatting details. |
| PRNG draw order | Flows draw nonces in fixed code order, so the same operation sequence consumes the stream identically. |
| Floating point | All value math is integer (u128); wrapping overflow is explicit (wrapping_* or checked_*). |
The reproducible-input contract
A scenario is fully described by:
fixture (environment + seed + ordered steps)
The fixture is versioned JSON (kind, version, environment.seed,
steps); the loader version-checks it and the runner replays it against a
fresh Simulator. Identical fixtures produce identical ScenarioOutcomes
(asserted in the determinism suite, including the failure code of
expected-failure scenarios).
What is compared
The determinism suite compares, across independent runs:
- the state root after every operation,
- transaction IDs,
- event IDs,
- output (commitment) IDs,
- full serialized stores (private values included),
- operation results, and
- error codes for failed operations.
Different seeds diverge only in private state (blinding nonces change the commitments) — structural facts (counts, versions, error codes) agree.
Ledger context
LedgerContext { seq, timestamp } starts from ledger.initial_seq /
ledger.start_timestamp and advances deterministically. Simulator::advance_ledger()
is an explicit step for scenarios that want multi-ledger behavior; it never
runs on a timer.