This file tells AI coding agents and human contributors how to work effectively in this repository. Start here.
Build and evolve a DeFi sequencer — the off-chain component that gives users low-latency soft confirmations while preserving the on-chain scheduler's canonical authority.
This is security-critical infrastructure. Treat every change with the care that financial systems demand. Correctness, determinism, and safety come before features.
The current application (examples/app-core/) is a hardcoded placeholder (deposit, transfer, withdrawal). It will be replaced by a production DeFi application. The sequencer itself is the product; the app is a stand-in for development.
In order of importance:
- Low latency —
POST /txack under 500 ms. - Financially sustainable — the system must pay for itself through fees.
- Low cost transactions — cheaper than native L1.
- Dispute compatibility — the design already accounts for rollup dispute resolution. Preserve it.
- Wallet-compatible signing — users sign with standard wallets via EIP-712. Never introduce custom signing schemes.
- Deposit availability < 10 minutes — happy path. The censorship-resistance backstop (
MAX_WAIT_BLOCKS, ~4h) is the worst case.
- App-specific sequencer. The sequencer may link against the application, enabling validation and execution at ingress time. This is a deliberate design choice.
- Soft confirmations may be invalidated. Under adversarial conditions (network, infrastructure, provider, or L1 outages), soft confirmations can be rolled back via recovery. This is by design, not a bug — it is what makes the sequencer sound in the face of liveness failures.
- App UX may depend on the sequencer. Without the sequencer, user experience may degrade substantially. This is an acceptable tradeoff: the on-chain scheduler remains the canonical source of truth; the sequencer only accelerates the UX.
The system has two components in an asymmetric relationship:
The scheduler runs inside the rollup and defines the canonical transaction ordering. For each batch read from L1 safe inputs, it processes frames in order: drain all pending direct inputs whose block number is ≤ safe_block, then execute the frame's user ops. The scheduler treats the sequencer as potentially Byzantine — it enforces ordering and staleness rules regardless of what the sequencer claims.
The sequencer knows the scheduler's algorithm. It uses that knowledge to predict what the canonical ordering will be once its batches land on L1, and issues soft confirmations to users ahead of time. The sequencer has write priority on the execution queue: as long as it keeps advancing safe_block and submitting batches, it controls ordering.
Each frame carries a safe_block chosen by the sequencer. It serves two purposes:
- It tells the scheduler how far to drain direct inputs before executing the frame's user ops.
- It is the sequencer's commitment that it has accounted for all direct inputs up to that block.
The sequencer must advance safe_block honestly. If it freezes safe_block (to censor deposits) or stops submitting batches, the staleness mechanism detects this and forces recovery.
Under honest sequencer operation and no infrastructure outages, soft confirmations match the canonical order. This is an optimistic guarantee — the sequencer is predicting a future the scheduler has not yet computed. When the sequencer goes offline, submits stale batches, or tries to censor direct inputs, the scheduler's force-drain backstop kicks in and the affected soft confirmations become invalid.
The precise acceptance algorithm — decode → sender → nonce → structural →
staleness → frame execution order → nonce advance — is owned by
docs/protocol/scheduler-semantics.md.
This section is the map.
Scheduler-acceptance semantics exist in exactly three implementations that must agree:
- the canonical fold —
Scheduler<A>(sequencer-core/src/scheduler/mod.rs), the same source compiled into the on-chain machine and driven bare-metal by the recovery fold; - the off-chain acceptance predicate —
ProtocolTiming::scheduler_accepts(sequencer-core/src/protocol.rs), which feedssafe_accepted_batches; - the inclusion lane's live prediction (drain + execution order).
The expected-nonce fold is homed next to scheduler_accepts as advance_expected_batch_nonce (same file); the submitter's decide_submit_start consumes it, and populate_safe_accepted_batches keeps a deliberate inline copy (its advance is interleaved with storage-only side effects — the R2 content-identity check and the divergence freeze — that can't move below the protocol layer). Touching any of these means re-checking the others — their agreement is the system's most load-bearing invariant (see docs/invariants.md).
Two mechanical facts the agreement rests on:
- Drain attribution. At a safe-frontier advance, the newly-drained directs are sequenced into the new frame — the frame stamped with the new
safe_block. So frame K's wire content reads "directs ≤ S_K, then user ops validated on top of them", exactly the scheduler's drain-before-ops rule. - Empty batches are never stale and consume the nonce (no first frame to measure staleness against). Consistent across all implementations, test-pinned.
A batch is stale when inclusion_block - first_frame.safe_block >= MAX_WAIT_BLOCKS (1200 blocks, ~4h). Staleness catches two failure modes:
- Liveness failure — the sequencer went offline and failed to submit batches in time.
- Censorship — the sequencer kept submitting batches but froze
safe_blockto hold back direct inputs.
When the scheduler encounters a stale batch, it skips it entirely — no nonce consumed, no state change. This is the censorship-resistance backstop: the sequencer cannot hold write priority indefinitely without advancing the drain cursor. Direct inputs are force-drained at MAX_WAIT_BLOCKS, guaranteeing deposit availability within ~4h even under adversarial conditions.
If a batch is stale, all existing subsequent batches are also invalid. The scheduler's expected-nonce counter does not advance on a stale skip, so every subsequent batch arrives at an unexpected nonce and is rejected. Invalidation is a suffix operation: marking batch N invalid cascades to N+1, N+2, …, including the open batch. New batches created after recovery are unaffected.
Rather than waiting for a batch to go stale on L1, the sequencer uses a danger threshold (MAX_WAIT_BLOCKS − MARGIN). The threshold is only a trigger: it tells the system "stop running, hand off to recovery." It does not encode "this batch is doomed" — that decision belongs to the post-flush cascade.
The cycle crosses a process boundary by design: the in-process
DangerDetector polls
Storage::check_danger on a cadence and exits the process when any
non-Safe arm fires (stopping the process is how the sequencer goes offline);
the orchestrator respawns; startup syncs the L1 safe head, re-runs
check_danger, and decide_startup_action
dispatches — Proceed (no recovery writes), RecoverTip (invalidate the aging
Tip directly; it has no L1 footprint, so no flush), FlushAndCascade (flush
every wallet-nonce slot, re-sync, cascade everything past the gold frontier),
or Refuse (surface to the operator). Then normal operation resumes.
The authoritative dispatch table, the "everything past gold is doomed" model,
and the per-path rationale live in
docs/recovery/README.md — that document owns
the recovery design; this section is only the map. Do not restate dispatch
details here.
Staleness is only checked against L1 safe state, never latest. Stale batches in latest that haven't reached safe yet will eventually become safe, and the check will fire at that point. This avoids reacting to L1 reorgs.
When the sequencer's view of L1 stops advancing — most often because the RPC gateway is stalled or returning stale reads, occasionally because L1 itself is unhealthy — the DB-based staleness check sees a frozen current_safe_block and may fail to trigger. The danger detector uses two wall-clock signals: the recorded L1 safe block timestamp must remain younger than CARTESI_SEQUENCER_L1_READ_STALE_AFTER_BLOCKS, and unresolved batches are also checked with estimated_missed_blocks = (now − last_safe_progress_ms) / seconds_per_block by adjusting the danger threshold downward. This prevents silently issuing doomed soft confirmations during stale-provider periods or L1 outages.
The preemptive recovery design is verified by bounded TLA+ model checking. See docs/recovery/ for the full design, TLA+ specs, and design history. When touching recovery code, read the TLA+ first.
See docs/threat-model/README.md for the full model. Key points when reading or writing code:
- Trusted: InputBox contract, our own Ethereum node (fail-stop, not byzantine), operator config, batch-submitter key.
- Adversarial:
POST /txcallers, direct-input senders, the L1 mempool and block builders (zombie transactions are a first-class threat). - RPC endpoint: single (
CARTESI_SEQUENCER_BLOCKCHAIN_HTTP_ENDPOINT), trusted fail-stop, must be one consistent node — no fallback tier exists yet (see the threat model's actor table). - Self-trust: the sequencer trusts its own code is correct. Bugs that emit malformed batches are fault states requiring manual intervention, not threats to defend against at runtime.
- In scope: correctness bugs and exploitation. Under rollup semantics, a correctness bug that causes scheduler/sequencer state divergence is as severe as direct theft.
Top-level layout follows the system's data flow. Each sequencer module corresponds to a writer role; the matching storage/<role>.rs holds its storage half.
sequencer/— sequencer library (no binary). App crates compose it into a binary.sequencer-core/— shared domain types (Application,SignedUserOp,SequencedL2Tx,Batch,Frame).examples/app-core/— placeholder wallet app implementing theApplicationtrait.examples/wallet-sequencer/— binary crate: wallet app + sequencer library. The model for what an app author builds (theirApplicationimpl ≙app-core; their binary crate ≙ this).examples/canonical-app/— on-chain scheduler reference implementation.examples/canonical-test/— e2e test harness for the canonical app.sdk/rust-client/— Rust client library for the sequencer API.tests/{benchmarks,e2e,harness}/— test infrastructure.
sequencer/src/lib.rs— public sequencer API. The thin binary entrypoints live inexamples/wallet-sequencer/.sequencer/src/harness.rs— CLI harness: thesetup/run/flush-mempoolsubcommand parser,dispatch, and the R4 exit-code projection. An app'smainis ~5 lines (run_main+ a genesis-app closure).sequencer/src/http.rs— shared HTTP error type, JSONErrorResponse,ApiConfig, andaxum::serveorchestration.sequencer/src/runtime/— process orchestration:setup(phase A — pin identity, initial sync, genesis snapshot,setup_completemarker),run(phase B — boot workers from a set-up DB),flush(flush-mempool), plusconfig,error(incl. exit-code projection),shutdown, sharedclock::unix_now_ms, and theworkerslifecycle.sequencer/src/ingress/— public write path.api.rs—POST /txhandler, JSON-rejection mapping.inclusion_lane/— single-lane hot-path loop (mod.rs), catch-up replay, config, error types.
sequencer/src/egress/— internal read path.api/—/ws/subscribe,/livez,/readyz,/healthz.l2_tx_feed/— DB-backed ordered-tx feed.
sequencer/src/l1/— L1 client surface.reader.rs— safe-input ingestion from InputBox into SQLite.submitter/— stateless batch submitter (worker.rs+poster.rs).fee_oracle/— setup-pinned L1 Uniswap V3 TWAP →batch_policy.log_gas_price(+log_gas_price_updated_at_ms); fixed mode writes once at setup and has no worker.eip1559.rs— shared EIP-1559 fee estimation (poster + oracle).provider.rs— alloy provider construction.partition.rs— long-block-range retry helper.
sequencer/src/recovery/— preemptive recovery startup procedure (mod.rs), runtime danger detector (detector.rs), and mempool flusher (flusher.rs).sequencer/src/storage/— SQLite persistence, split by writer role (ingress,egress,l1_inputs,l1_submission,recovery,admin,safe_accepted_batches,snapshot_dumps, plus sharedmod,open,convert,queries,mutations, andmigrations/).
- Chunk — bounded list of user ops processed and persisted together to amortize SQLite cost.
- Frame — ordering boundary; commits
safe_block+ user ops. - Batch — list of frames posted on-chain as one L1 transaction (SSZ-encoded).
- Inclusion lane — hot-path single-lane loop that dequeues, executes, persists, and rotates frame/batch boundaries. The only writer of open batch/frame state.
- Batch submitter — stateless worker that bulk-submits all pending batches each tick. Nonces are assigned by storage (structural
parent.nonce + 1) when batches are closed; the submitter just reads them. - Danger detector — background worker that polls
Storage::check_dangeron a fixed cadence and exits withRecoveryRequiredwhen any non-Safedanger status fires. Never writes to the DB; never talks to L1. Crashes the process so startup recovery or refusal can run. - Fee oracle — setup pins either a fixed exponent or a reviewed Uniswap V3 WETH/X TWAP tuple into deployment identity, and writes the first
log_gas_price(+ freshness stamp) in both modes. Fixed mode has no worker; Uniswap refreshesbatch_policy.log_gas_priceon a poll loop. Transient L1 failures atrunboot and at runtime retain the persisted price untillog_gas_price_updated_at_msexceeds the L1 read-staleness window; misconfig stays terminal. The 10× margin lives inbatch_policy.log_slack; frame fees stay immutable until the next frame opens. - Input reader — ingests safe inputs from L1 InputBox into SQLite.
- L2 tx feed — DB-backed ordered-tx stream used by WS subscribers.
- Soft confirmation — sequencer's predicted ordering, emitted before the batch lands on L1.
- Snapshot — durable copy of the app's canonical state at a known L2-tx offset; pending at batch close, promoted to finalized on L1 observation (per-range, atomically with the drain), garbage-collected when superseded. Backs catch-up, the watchdog, and indexers. Lifecycle + rationale (incl. the promote/drain crash-safety):
docs/snapshots/lifecycle.md.
- API validates the EIP-712 signature and enqueues a
SignedUserOp. Method payload decoding happens during application execution, not at ingress. - Deposits are direct-input-only (L1 → L2) and must not be represented as user ops.
- Rejections (
InvalidNonce,InvalidMaxFee,InsufficientFeeBalance) produce no state mutation and are not persisted. These are protocol-level rejection semantics every app must implement: nonces prevent user-op replay, fees prevent spam against the sequencer's DA budget. ("Fee", not "gas" — the fee tracks DA; compute metering, if it ever exists, is a separate future concept.) - Included txs are persisted as frame/batch data in
batches,frames,user_ops,safe_inputs, andsequenced_l2_txs. Recovery metadata lives insafe_accepted_batches; batch lifecycle state (sealed/invalidated) lives on thebatchesrow itself as write-once timestamps. - Frame fee is persisted in
frames.feeand is fixed for the lifetime of that frame. The next frame's fee is sampled frombatch_policy_derived.recommended_feeat rotation; oracle bootstrap writes the price before any Tip can sample it, andlog_slackapplies the 10× margin in log space. - Wallet state (balances, nonces) is in-memory today — not persisted.
- EIP-712 domain fields:
name,version,chainId,verifyingContract.chainIdandverifyingContractcome fromCARTESI_SEQUENCER_BLOCKCHAIN_IDandCARTESI_SEQUENCER_APP_ADDRESS(validated against the RPC chain id at startup). All four fields must be present on both sides — both the sequencer and the on-chain scheduler construct the domain viasequencer_core::build_input_domain, the canonical shared constructor.
- The input reader ingests every
InputAddedevent from InputBox. Each event carries an authenticatedmsg_sender(delivered by the Cartesi framework fromEvmAdvanceCall). - Classification is by sender address, not by a tag byte:
- Sender == batch-submitter address → SSZ-decoded as
Batch(scheduler side). The sequencer does not ingest its own batch submissions as direct inputs. - Any other sender → stored verbatim as a direct input (deposit).
- Sender == batch-submitter address → SSZ-decoded as
- The payload is opaque to the classification layer. Application-specific decoding happens inside
Application::execute_direct_input.
Implementors of the Application trait must respect these contracts. The sequencer assumes them without runtime enforcement. The full, code-grounded contract — method table, dump round-trip durability, the safe-block clock — is owned by docs/protocol/application-contract.md; the essentials follow.
The sequencer persists every included user op and every ingested direct input. On restart, catch-up replays them in order against a fresh Application instance to rebuild state. Any input that succeeded live must succeed on replay.
execute_direct_inputandexecute_valid_user_opmust not returnAppError::Internalfor any byte sequence that previously executed successfully. Catch-up treatsInternalas fatal: it aborts startup and leaves the sequencer unable to resume.- Prefer
ExecutionOutcome::Invalidfor malformed or ill-typed input caught at the app level. ReserveAppError::Internalfor genuine invariant violations ("validated user op cannot pay fee") — real bugs, not adversarial inputs.Invalidis replay-safe;Internalis not. validate_user_opmust be pure over the current app state. No side effects, no time dependence, no randomness.
Application state changes must flow exclusively through execute_valid_user_op and execute_direct_input. Mutating state from validate_user_op breaks replay determinism.
User ops are executed only through sequencer_core::application::validate_and_execute_user_op (a free function, deliberately not an overridable trait method): it enforces the protocol-level max_fee >= current_fee guard before app validation, so no Application impl can skip it. Both the inclusion lane and the canonical scheduler call it — part of the duality agreement.
- API ack is tied to chunk durability, not frame/batch closure. "Durable" means power-loss-durable: WAL with
synchronous=FULL, so every commit fsyncs before anything externalizes on it (review R3). - Chunk commit and ack remain low-latency; frame closure is orthogonal and can happen less frequently.
POST /txqueue admission:try_sendon a full queue returns429 OVERLOADEDwith messagequeue full.- Frame closure happens when direct inputs are drained, and also whenever batch closure happens.
- Batch closure is controlled by batch policy (size and/or deadline).
- Preserve single-lane deterministic ordering. Do not introduce extra concurrency in hot-path ordering logic without explicit approval.
Writer roles — one writer per table; reads over batch data go through the valid_* views:
| Writer | Writes |
|---|---|
| inclusion lane | batches (insert + sealed_at_ms), frames, user_ops, sequenced_l2_txs, dumps/pending_snapshots (batch close), finalized_snapshot (promotion) |
| input reader | safe_inputs, l1_safe_head, safe_accepted_batches, deployment_identity, canonical_divergence (poison marker, review R2) |
| recovery (startup) | batches.invalidated_at_ms, Tip reopen, scoped pending_snapshots clear, wallet_nonce_watermark (flush no-ops, write-before-broadcast) |
| batch submitter | wallet_nonce_watermark (write-before-broadcast, review R1a — its only write) |
| egress (HTTP) | dumps.lease_count (leases) |
| admin | batch_policy alpha knobs (log_alpha, log_one_plus_alpha) |
| setup | batch_policy.log_gas_price + log_gas_price_updated_at_ms (first write; Fixed and Uniswap) |
| fee_oracle | batch_policy.log_gas_price + log_gas_price_updated_at_ms (Uniswap mode only; stamps on every successful refresh) |
- Storage model is append-oriented; avoid mutable status flags for open/closed entities.
- Open batch/frame are derived by "latest row" convention.
- A frame's leading direct-input prefix is derivable from
sequenced_l2_txsplusframes.safe_block. - Safe cursor/head values should be derived from persisted facts when possible, not duplicated as mutable fields.
- Replay/catch-up uses persisted ordering plus persisted frame fee (
frames.fee) to mirror inclusion semantics exactly. - Cursor pagination for ordered L2 txs uses SQLite rowid, not count-based offsets. Holes from invalidated batches would break count-based pagination.
- Included user-op identity is tracked by application nonce logic; no DB uniqueness constraint (removed to allow resubmission after recovery).
- Reads over batch data go through
valid_batches,valid_closed_batches,valid_open_batch, andvalid_sequenced_l2_txsviews. These encapsulate the "exclude invalidated rows" filter so individual queries don't repeat it. Writers go to the base tables. batchesrow columns partition cleanly by writer.sealed_at_msis owned by the inclusion lane (set when closing a batch);invalidated_at_msis owned by recovery (set during cascade). Each is write-once (NULL → non-NULL, never back) and enforced by triggers. The partial unique indexux_single_valid_tipguarantees at most one row has both NULL — the Tip.- The inclusion lane is the only writer of open batch/frame state.
Storage::append_user_ops_chunkand theclose_*methods trust the in-memoryWriteHead; the Tip-targeting triggers and thepos_in_framePK catch stale-WriteHeadbugs for user ops. Direct-input sequencing has no structural uniqueness guard (re-drain support requires duplicatesafe_input_indexacross invalidated batches) — double-sequencing prevention rests on the lane's drain-cursor discipline and its startup re-derivation (seedocs/invariants.md).
SignedUserOp— ingress/API signature domain (post-validation, pre-execution).ValidUserOp— application execution domain (after validation boundary).SequencedL2Tx— ordered replay/fanout domain (UserOp | DirectInput).- Keep DB-only helper types private to storage modules; prefer shared domain types at module boundaries.
- Ingress (public-facing):
POST /tx. - Egress (internal indexers/watchdog):
GET /ws/subscribe,GET /finalized_state,GET /finalized_state/inclusion_block,GET /latest_snapshot,GET /livez,GET /readyz,GET /healthz. The snapshot/state endpoints are operator-only (no auth) and must not be exposed publicly; the streaming routes hold a GC lease for the response lifetime (docs/snapshots/lifecycle.md).
Today both sides serve from one listener; the planned API split puts each side on its own port (same binary) so internal probes and subscribers can be firewalled from public submit traffic.
Message shapes, caps, close codes, and health semantics are owned by README.md (the API contract) — do not restate them here.
Split by subcommand (the phase split). setup (required):
CARTESI_SEQUENCER_BLOCKCHAIN_HTTP_ENDPOINTCARTESI_SEQUENCER_BLOCKCHAIN_IDCARTESI_SEQUENCER_APP_ADDRESSCARTESI_SEQUENCER_BATCH_SUBMITTER_ADDRESS(the submitter address —setupis L1-read-only and never signs). Must be a dedicated address:setup's detection gate refuses if the submitter's wallet nonce is unsettled, so reusing a busy address (e.g. the contract deployer, whose deploy-tx tail isn't safe at setup time) false-positives. The devnet uses anvil account 9 (DEVNET_SEQUENCER_ADDRESS), distinct from the account-0 deployer.CARTESI_SEQUENCER_CHECKPOINT_BLOCK(optional, default0= genesis) — the trusted checkpoint machine's L1 inclusion block.setuprefuses (typedSetupRefuse, exit 40 = runsetup --recovery) if a previous instance left work past it. PR3 detects only; loading a non-genesis checkpoint machine issetup --recovery(PR5).
run (required) — chain id / app address / submitter address are read from the DB setup pinned, not from args:
CARTESI_SEQUENCER_BLOCKCHAIN_HTTP_ENDPOINTCARTESI_SEQUENCER_AUTH_PRIVATE_KEYorCARTESI_SEQUENCER_AUTH_PRIVATE_KEY_FILE
Optional (names only — defaults and semantics are owned by
sequencer/src/runtime/config.rs; a
defaults list here drifted once already): CARTESI_SEQUENCER_HTTP_ADDR, CARTESI_SEQUENCER_DATA_DIR,
CARTESI_SEQUENCER_LONG_BLOCK_RANGE_ERROR_CODES, CARTESI_SEQUENCER_BATCH_SUBMITTER_IDLE_POLL_INTERVAL_MS,
CARTESI_SEQUENCER_BATCH_SUBMITTER_CONFIRMATION_DEPTH, CARTESI_SEQUENCER_PREEMPTIVE_MARGIN_BLOCKS,
CARTESI_SEQUENCER_L1_READ_STALE_AFTER_BLOCKS (fixed default, independent of the margin;
must be strictly below the danger threshold or startup refuses),
CARTESI_SEQUENCER_SECONDS_PER_BLOCK, and the runtime-only
CARTESI_SEQUENCER_FEE_ORACLE_POLL_INTERVAL_MS. Setup-only fee-oracle source knobs are
(CARTESI_SEQUENCER_FEE_ORACLE_FIXED_LOG_GAS_PRICE,
CARTESI_SEQUENCER_FEE_TOKEN_ADDRESS,
CARTESI_SEQUENCER_WETH_ADDRESS, CARTESI_SEQUENCER_UNISWAP_V3_POOL,
CARTESI_SEQUENCER_FEE_ORACLE_TWAP_WINDOW_SECS — mainnet/Sepolia default to
pinned USDC pool presets; other chains require an explicit source).
- Prefer small, composable functions at module boundaries (
ingress::api→ingress::inclusion_lane→storage::ingress;egress::l2_tx_feed←storage::egress). - Keep application validation and execution deterministic for a given input/state. No
SystemTime::now(),HashMapiteration order, or floating-point in consensus paths. - Surface user-facing errors via
ApiError(inhttp.rs); keep internal failures descriptive but safe. - Avoid introducing heavy dependencies without strong reason.
- Documentation style: lean. Module headers (1–4 lines) + docs on public methods only when the contract isn't obvious from name+signature. Use inline comments for why, never for what.
- Impossible states fail loud; they are never handled. Cheap cross-module assertions of real invariants are encouraged (assert, trigger
RAISE, typed error) — a loud crash is recoverable by design; silent divergence is not. Never add graceful fallbacks, neighbor re-validation, or silent absorbers (INSERT OR IGNORE, saturating decode of impossible data) for states the contracts rule out; and an assertion must check a real invariant, never an environmental assumption (clock monotonicity is the cautionary tale). Decision test and rationale:docs/invariants.md; trust boundaries: "Self-trust" indocs/threat-model/README.md.
Focus tests on:
- Signature + sender-validation edge cases.
- Nonce progression rules.
- Fee and rejection behavior.
- Included-vs-rejected commit behavior.
- Storage batch atomicity and uniqueness constraints.
- Scheduler/sequencer agreement — any invariant the two sides share should have at least one test that exercises both.
Prefer black-box tests around POST /tx and commit outcomes for integration.
Some sequencer tests use Anvil (Foundry). They run by default and fail with a clear message if anvil is not on PATH. Install Foundry or use nix develop.
See CLAUDE.md for shell setup and the full command list. In short:
cargo check
cargo test --workspace --exclude canonical-test
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warningsRun server (two phases — setup once, then run; see README.md "Running"):
# setup (L1-read-only; takes the submitter ADDRESS, not the key)
CARTESI_SEQUENCER_BLOCKCHAIN_HTTP_ENDPOINT=http://127.0.0.1:8545 \
CARTESI_SEQUENCER_BLOCKCHAIN_ID=31337 \
CARTESI_SEQUENCER_APP_ADDRESS=0x1111111111111111111111111111111111111111 \
CARTESI_SEQUENCER_BATCH_SUBMITTER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
cargo run -p wallet-sequencer -- setup
# run (keyed; reads identity from the set-up DB)
CARTESI_SEQUENCER_BLOCKCHAIN_HTTP_ENDPOINT=http://127.0.0.1:8545 \
CARTESI_SEQUENCER_AUTH_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
cargo run -p wallet-sequencer -- run- Keep inclusion-vs-rejection semantics explicit for transaction handling.
- Preserve API error shape and status code mapping unless intentionally changing the API contract.
- Add or update tests when logic changes.
- Run at least
cargo checkbefore finishing. - Read
docs/recovery/before touching recovery code, anddocs/threat-model/before touching trust-boundary code. - Check
docs/invariants.mdbefore changing anything it lists as load-bearing, and the latest review ledger underdocs/review/for known-open findings in the code you're about to touch.
- Changing tx wire format (
UserOp, SSZ payload layout, EIP-712 domain fields). - Changing DB schema or migration strategy.
- Altering rejection semantics (what consumes nonce/gas vs what is rejected).
- Introducing concurrency changes to commit ordering.
- Changing chunk/frame/batch closure or ack semantics.
- Silently weaken signature validation.
- Merge behavioral changes with unrelated refactors in one patch.
- Rely on implicit defaults for consensus-relevant values.
- Remove guardrails around queue backpressure or inclusion-lane error reporting.
At this stage it is acceptable to rewrite baseline migrations for clarity. There are no deployed environments requiring forward-only migrations. Keep schema bootstrap (initial open rows and invariants) explicit and deterministic.
Once environments are shared or deployed, switch to append-only forward migrations.
Before finishing a change, ensure:
- Code compiles (
cargo check). - Changed behavior is covered by tests, or explain why tests are pending.
- Formatting and lints are clean, or list any unresolved warnings explicitly.
- PR summary includes what changed, why it changed, and risk / compatibility notes.
README.md— product framing, user-facing trust model, API contract (endpoint shapes, caps, close codes, health semantics).CLAUDE.md— shell setup, quick reference, pointer back here.docs/protocol/— the authoritative protocol contracts:scheduler-semantics.md(the canonical acceptance algorithm, I1) andapplication-contract.md(theApplicationFFI trait contract).docs/invariants.md— register of cross-module invariants (what's load-bearing across files) + the fail-loud check policy.docs/review/— dated correctness-review ledgers; open findings, settled designs, work packages.docs/threat-model/README.md— trust boundaries, in-scope and out-of-scope threats.docs/recovery/README.md— recovery design, TLA+ formal verification, design history.docs/snapshots/— app snapshots:format.md(dump trait + wire format) andlifecycle.md(take/promote/GC/lease design + crash-safety).docs/watchdog/operator-deployment.md— production-like watchdog (Sepolia / mainnet; internal snapshot API).docs/watchdog/getting-started.md— local dev: watchdog +sequencer-devneton Anvil.docs/watchdog/README.md— watchdog architecture, compare vs advance modes, test commands.sequencer-core/— shared domain types and protocol contracts.