An app plugs into the sequencer by implementing
Application. It owns its state,
including execution progress; the protocol defines how that progress advances.
The shared execution boundary verifies each successful transition. Application
code remains self-trusted: Rust capabilities cannot establish the correctness
of a native engine, its FFI, or its canonical counterpart
(threat model, "Self-trust").
This document owns the contract. AGENTS.md is the map.
The wallet is the reference implementation. A
production application may execute natively or wrap a Cartesi Machine.
The C application binding adapts this contract to
native engines; its build guide includes
a reference wallet integration.
| Method | Contract |
|---|---|
validate_user_op(sender, op, current_fee) -> Result<ValidationOutcome, AppError> |
Pure predicate: Accept, Reject(InvalidReason), or fatal engine failure. No state change. |
apply_valid_user_op(valid, safe_block) -> Result<AppOutputs, AppError> |
Execute and advance embedded progress exactly once on success, using the frame clock. |
apply_direct_input(input) -> Result<AppOutputs, AppError> |
Execute and advance embedded progress exactly once on success, using the input's L1 block. |
progress() -> ApplicationProgress |
Return the count/clock pair from the engine by value. |
Execution callers use validate_and_execute_user_op, execute_valid_user_op,
and execute_direct_input. The first enforces max_fee >= current_fee before
app validation and rejects an accepted op at the exhausted nonce u32::MAX
after it. App validation checks the nonce and fee balance; a rejection is not
persisted. Trusted replay
uses the stored ValidUserOp, whose fee and sender were established at
inclusion, without validating a second time.
Before an apply hook, the boundary computes the checked expected successor.
After Ok, it asserts that the engine reports exactly that successor and
returns the input's pre-execution count as its history offset. An engine may
use ApplicationProgress::advance or implement the same transition natively.
Adapters report the engine-owned progress rather than maintaining a separate
count or clock mirror.
Overflow fails before the hook runs. An error defines no successor: callers
terminate the execution path and discard the instance, without attempting to
roll back or inspect partially updated state.
Application::max_method_payload_bytes() returns the engine's stable method
payload bound without constructing an instance. Zero permits only empty method
payloads. HTTP rejects oversized method payloads; the lane uses the declared
bound plus signed-op metadata to compute batch capacity. It is not a canonical
scheduler rejection rule.
Execution must be deterministic over (input, current state), including output
bytes and their order. Do not depend on wall time, randomness, pointer values,
nondeterministic iteration, or host-specific arithmetic in consensus paths.
Validation is read-only even though it may run on a different schedule during
live execution, canonical execution, and replay. State changes happen through
apply hooks; dump creation may change backing resources but preserves logical
state.
Wrapping an existing engine must preserve its transaction encoding, rejection/inclusion semantics, and canonical state bytes.
Application: Send + Sized permits moving the engine to the lane's blocking
worker. It requires neither Sync nor Clone: the lane owns one mutable
engine, and an independent state fork is a fallible checkpoint/restore
operation. A shared-handle Clone would not establish independence. Engines
using FFI must justify Send from their actual handle and thread-lifetime
rules.
These outcomes have different protocol meanings:
- A validation rejection changes no state, consumes no nonce or fee, and is
not persisted.
InvalidReasoncovers nonce mismatch, an exhausted nonce, insufficient max fee, and insufficient fee balance. - A successfully applied input is included even if the business operation fails or is ignored. It advances progress, and an included user op consumes its sender's nonce (below). The wallet also charges the fee for a malformed method or failed transfer after validation; malformed direct inputs are included no-ops. These semantics must agree with the canonical application.
AppError, whetherInternalorIo, is fatal in validation and execution. It must not be disguised as a client rejection or an included no-op. Fatal here means discarding the engine: the host still distinguishes a terminalInternalfault from an operational I/O failure that may clear after restart.
Every input executed successfully live must execute successfully against the same prior state on replay. The sequencer persists included inputs and replays them on restart. It does not recover an instance after a failed hook.
Every application follows one nonce rule. The sequencer relies on it:
GET /nonce derives a sender's next nonce from the user ops it persisted,
without asking the engine.
- A genesis state starts every account at nonce 0.
- Validation accepts an op only when its nonce equals the sender's expected nonce.
- Each included user op, business failures included, advances its sender's expected nonce by exactly one. Nothing else changes a nonce: not direct inputs, not other senders' ops.
- Nonces are
u32.u32::MAXhas no successor, so an account that reaches it is exhausted: when app validation accepts an op atu32::MAX, the shared execution boundary rejects it (NonceExhausted). Any other op carryingu32::MAXfails app validation with the expected nonce.
Under this rule a sender's expected nonce is one past its latest included op.
The exception is a sender with no surviving op since the era baseline. A
genesis baseline puts it at 0, but a rebuilt baseline
carries nonces the rebuilt database has no ops for, so GET /nonce reads 0
until that sender's first op, and again if a later recovery invalidates all of
its post-rebuild ops. An engine outside the rule (gapped or keyed
nonces, a deposit that resets one) makes GET /nonce quote nonces the engine
rejects.
The clock is the maximum block carried by an executed input: frame safe_block
for user ops, L1 inclusion block for directs, or zero at genesis. On success,
the engine advances it with max(old_clock, input_clock). Count zero implies
clock zero. Both fields survive checkpoint round-trips.
The frame-clock policy may deliver a delayed head jump as one step. Newly covered directs execute first with their exact inclusion blocks; all user ops in the frame receive the same frame clock. An empty clock-only frame does not execute an input or advance the app clock.
Recovery reads this clock as A, the safe block reflected in the checkpoint;
a wrong clock mis-defines the reconstructed (A, B] fridge range
(cockroach recovery).
The count is the next application-history offset, never a SQLite cursor.
An input executed at count X returns offset X and advances to X + 1.
Included no-ops count; rejections, merely queued inputs, envelopes, and empty
batches do not. Arithmetic is checked, never wrapping or saturating.
The count belongs to the checkpoint's logical state. Standard recovery may roll it back to a retained prefix and then advance over replacement inputs; cockroach recovery supplies an absolute starting count from the recovered engine. SQLite stores an independent expected snapshot count and per-input execution offsets, checked during catch-up.
HTTP snapshot metadata and mandatory WS claims carry the history version and this count. Both restart and subscriber replay read the same current application sequence. The history guide owns coordinates and replica bootstrap; the API contract owns wire shapes.
A supported production application must promptly execute the complete accumulated input range the persisted frontier can expose in one L1 reconciliation turn, including backlog within the supported operating envelope. The lane processes that range before returning to user-op work. There is no elapsed-time cutoff, preemption, or durable timeout-and-resume cursor. Paging may bound memory; the complete-range reconciliation commit remains atomic.
This is a deployment assumption. A request overlapping reconciliation or synchronous checkpoint creation may see extra acknowledgement latency. Revisit scheduling only when application cost, L1 capacity/finality/backlog, or measured checkpoint latency demonstrates the need.
A recovery checkpoint contains everything needed to resume the engine. A canonical comparison file contains the deterministic state the watchdog compares against the canonical application. They may be the same file; a machine checkpoint may instead contain a separate app-state projection.
| Engine | Recovery checkpoint | Canonical comparison file |
|---|---|---|
| Wallet | SSZ wallet state | The same SSZ file, also returned by canonical inspect |
| Cartesi Machine wrapper | Full multi-file machine state | Deterministic app-state projection stored alongside it |
| Native DEX design | Fixed-memory state M plus required resumable metadata |
M exactly as the canonical machine's labeled state range holds it |
The DEX row describes an integration requirement, not a verified private implementation. The watchdog guide owns how the comparison is transported and which state source a deployment uses; the wallet format owns its SSZ representation.
Comparison bytes. The comparison file must byte-equal what the canonical
machine yields after the same inputs, through the deployment's state source,
whether the engine ran them uninterrupted or was restored with from_dump or
rebuilt by recovery partway. For a range source, anything that shapes the
range's layout (allocator state, hash seeds, capacity policy) belongs to the
checkpoint and must not depend on the process:
-
Range source: the entire flash drive or NVRAM carrying the configured user label, all of its configured length including any unused zero tail, raw: no filesystem, header, or trimming. The guest must have written the state into the range before it finishes each input. NVRAM has no page cache; a flash drive goes through the guest page cache, so the guest must flush (
fsync,msync(MS_SYNC), orO_DIRECT) first. The Cartesi Machine facts explain why the range must be raw. -
Inspect source: the single report the canonical application returns for the inspect query
state. -
create_dump(&mut self, prefix)creates a checkpoint at an absent path, which may become a file or directory. OnOk, all files and directory entries referencing them, including the parent ofprefix, must be durable against an immediate kernel crash. SQLite references the checkpoint only after this returns. The live logical state and progress stay unchanged; subsequent execution cannot mutate the checkpoint. -
from_dump(prefix)restores equivalent logical state, including progress, into an independent mutable engine. Execution cannot change the source checkpoint or another restored instance. The restored engine must remain usable after the source checkpoint is garbage-collected. -
state_file_in_dump(prefix)is a pure path function naming a single file, possiblyprefixitself. Its bytes are the comparison bytes above. -
All checkpoint-owned artifacts reside at or below
prefix, including the canonical comparison file. Disposal requires only ordinary filesystem deletion: the sequencer removes the enclosing directory, including its owninfo.toml. Removing a checkpoint must leave other checkpoints and restored engines usable; external resource cleanup is outside this contract.
Mutable checkpoint creation permits flushing, replacing mappings, or changing working backing files inside an adapter. It does not permit a logical state transition. CoW sharing is allowed if writes remain isolated and durability is met. No public flush/clone/reopen protocol or delta chain is required.
from_dump must preserve Io(NotFound) for absent checkpoints. Structural
corruption may return Internal (as the wallet decoder does) or a specific
I/O kind such as InvalidData or UnexpectedEof. Operational I/O failures
retain their actual error kind; startup uses these distinctions to classify
a broken referenced checkpoint versus a retryable storage failure.
Genesis construction stays on the concrete engine because its inputs vary by
implementation. CanonicalState::canonical_snapshot_bytes is a separate
inspection trait required by the shared Rust scheduler's inspection method and
canonical harness, not by the native sequencer. Human-readable debugging state
also stays on the concrete application.
Every supported production application must provide a reproducible mapping from a trusted canonical machine checkpoint and pinned deployment configuration to its native recovery artifact. All logical state needed for future application execution, including progress, must be recoverable this way. Native caches and backing resources may be reconstructed; indispensable mutable application state cannot exist only in the sequencer's local storage.
For the native DEX integration, the native application state is the designated
canonical machine drive/memory region. The integration must pin its location,
layout, and extraction procedure for each supported image. Other applications
may require a different mapping. The recovery bundle also needs the exact L1
boundary and next scheduler nonce, obtained from trusted canonical execution;
these are separate from merely extracting application bytes.
The exporter must also verify the canonical pending-direct queue satisfies
checkpoint eligibility.
An accurate application clock and A < B do not prove that condition after
faulty sequencing.
Each integration supplies a versioned export command and operator procedure, and demonstrates recovery from a non-genesis canonical checkpoint before production use. The recovery readiness requirements own the procedure and drill. This is an integration/release requirement, not a claim that the private engine or current tooling has passed it.
Canonical extraction belongs to application-specific recovery tooling. The
runtime Application trait stays independent of machine image layouts and host
export tooling; requiring a method to compile would establish its availability,
not the correctness or operational readiness of the recovery path.