Skip to content

Latest commit

 

History

History
271 lines (225 loc) · 15.2 KB

File metadata and controls

271 lines (225 loc) · 15.2 KB

The Application contract

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.

The execution methods

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.

Cross-cutting contracts

1. Determinism & purity

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.

2. Replay safety — rejection, inclusion, and failure

These outcomes have different protocol meanings:

  • A validation rejection changes no state, consumes no nonce or fee, and is not persisted. InvalidReason covers 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, whether Internal or Io, 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 terminal Internal fault 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.

User nonces

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::MAX has no successor, so an account that reaches it is exhausted: when app validation accepts an op at u32::MAX, the shared execution boundary rejects it (NonceExhausted). Any other op carrying u32::MAX fails 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.

3. The safe-block clock — last_executed_safe_block

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).

4. Canonical history cursor: executed_input_count

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.

5. Operational capacity for L1 reconciliation

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.

6. Checkpoint lifecycle

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), or O_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. On Ok, all files and directory entries referencing them, including the parent of prefix, 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, possibly prefix itself. 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 own info.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.

7. Canonical recovery integration

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.