Verity Architecture

Status: pre-implementation. This document captures the architectural intent derived from the Design Philosophy. No Lean or Rust source exists yet.

Verity is a provable consensus client: the verified Lean 4 Verity Consensus implementation wrapped in a Rust runtime. That two-language split makes Verity structurally different from single-language clients, so its first-class architectural axis is the verification boundary — what is inside the proven consensus implementation and what is outside it. Everything else, including the concurrency model, follows from that axis.

The architecture is organized into three concentric zones, drawn from the proven consensus implementation outward. A zone is defined by the guarantee level it holds code to — proven-pure, trusted-and-panic-free, or concurrent-IO — not by the specific components that happen to occupy it today. Which component sits in which zone is a current snapshot, expected to change as the verification frontier moves; see boundary migration.

Day-one snapshot — Rust-first. Implementation starts Rust-first (kickoff decision, 2026-07-22): at kickoff every capability contract is bound to its native-Rust implementation, the verity-consensus-sys export set is empty, and no FFI call is made. Lean-compiled logic is adopted per capability later — stable, proved, and measured-within-budget first; the state transition and fork choice last, because they track a volatile upstream spec. The zone diagrams and the inbound-block sequence below therefore show the target state, with Verity Consensus occupying the Verified Core; on day one the same functions run as native Rust in the Runtime Shell, behind the same contracts.

Zones

  • Verified Core — Verity Consensus (Lean 4, pure). The proven-pure zone: pure, total functions only — no hidden state, clocks, locks, or scheduling. This is the surface that Lean proofs defend. Its source is the formal-leanSpec Lean 4 model, compiled via Lean's C backend into a static library and exposed to Rust over a C ABI (no Aeneas) — see Formal Verification for the one-model, two-roles split between compiled-and-exported functions and proof-only propositions. Its current occupants are deliberately minimal — only the state transition and the fork-choice transition functions — but that export set is a snapshot, not a definition: it contracts if a function leaves for a zkVM artifact, and grows if a function (e.g. hash_tree_root) is verified in Lean and pulled in.

  • Runtime Shell — Rust, panic-free. The trusted, panic-free zone. Manufactures clean, typed, already-verified inputs for Verity Consensus, owns the consensus state and fork-choice view as a single writer, and threads immutable values through Verity Consensus. Proofs do not reach here, so it is held to the language-level bar instead: memory-safe, strongly typed, and panic-free. Today, SSZ / hash_tree_root and signature verification are realized here as native-Rust implementations of their capability contracts, so Verity Consensus receives precomputed roots and verified signatures rather than recomputing or trusting them itself. That is a placement, not a contract: were a Lean-verified serialization to satisfy the same contract across the FFI seam, Verified Core would compute those roots itself and Runtime Shell's consumers would not change.

  • I/O Edge — Rust, concurrent. The only place where concurrency and the outside world live: networking, the slot clock, validator duties, RPC, metrics, and node orchestration. Bounded queues provide backpressure. The choice of concurrency primitive (actor model vs. async tasks) is a later, I/O-Edge-internal decision — it is not an architectural concern, because the consensus state has a single owner in Runtime Shell and Verity Consensus is invoked sequentially regardless.

Component diagram

flowchart TB
    subgraph C["I/O Edge — Rust, concurrent"]
        direction LR
        NET["P2P networking<br/>gossipsub · req/resp"]
        CLK["Slot clock / ticker"]
        VAL["Validator duties<br/>produce · sign · aggregate"]
        RPC["RPC / HTTP API"]
        MET["Metrics<br/>verity-metrics"]
        ORCH["Node orchestrator<br/>lifecycle · bounded queues"]
    end

    subgraph B["Runtime Shell — Rust, panic-free"]
        direction LR
        CODEC["SSZ codec + hash_tree_root<br/>wire bytes ↔ typed values"]
        CRYPTO["Signature verification<br/>verity-crypto: XMSS · leanMultisig"]
        STORE["State + fork-choice store<br/>single writer · threads immutable values"]
        DB["Database<br/>blocks · states · anchor"]
        FFI["FFI bindings layer"]
    end

    subgraph A["Verified Core · Verity Consensus — Lean 4, pure"]
        direction LR
        STF["State transition<br/>process_slots · process_block"]
        FC["Fork choice<br/>on_block · on_vote · get_head"]
    end

    NET -->|"raw bytes"| CODEC
    CODEC -->|"typed + roots"| CRYPTO
    CRYPTO -->|"verified inputs"| STORE
    CLK --> ORCH --> STORE
    STORE <-->|"immutable values"| FFI
    FFI ==>|"C ABI · Lean C backend"| STF
    FFI ==>|"C ABI"| FC
    STORE --> DB
    STORE -->|"head / state"| VAL
    STORE -->|"head / state"| RPC
    STORE -->|"head / state"| MET
    VAL -->|"signed block/vote"| CODEC

Inbound block — crossing the boundary

The boundary crossing over time, for a block arriving from a peer. Decoding, root computation, and signature verification all complete in Runtime Shell before Verity Consensus is touched, so each FFI call into Verity Consensus receives only clean, typed, verified values.

sequenceDiagram
    participant P as Peer
    participant N as Network (C)
    participant K as Codec + Crypto (B)
    participant S as Store (B)
    participant L as Verity Consensus (A)
    participant D as DB (B)
    P->>N: gossip block (bytes)
    N->>K: SSZ decode + hash_tree_root
    K->>K: verify XMSS / aggregate signatures
    K->>S: verified, typed block (+ roots)
    S->>L: process_block(state, block)  [FFI]
    L-->>S: new state
    S->>L: on_block(view, block)  [FFI]
    L-->>S: new view
    S->>L: get_head(view)  [FFI]
    L-->>S: head root
    S->>D: persist block + state

Crate layout

This layout is the target shape, not the day-one scaffold. Implementation starts with a single verity-consensus crate (kickoff decision, 2026-07-22): the zone boundaries below begin as module boundaries inside that crate, holding the same inward invariant, and split into separate crates only when a second crate earns its existence. The workspace description that follows is what that split grows into.

The Rust runtime is a Cargo workspace. Crates map onto the zones, and calls and dependencies flow inward, from higher-effect / lower-assurance toward lower-effect / higher-assurance — Verified Core never calls outward. Today that ordering reads I/O Edge → Runtime Shell → Verified Core over the current crate snapshot, and the compiler enforces it rather than discipline. The invariant is stated over guarantee levels, not crate identities, so it survives migration: if hash_tree_root moves into Verified Core, verity-types (Runtime Shell) calls inward to Verified Core for it — still Runtime Shell → Verified Core, still legal; if the state transition leaves Verified Core, its export set shrinks but nothing starts calling outward. Names follow the existing verity-* convention (verity-crypto, verity-metrics); the sole exception is the FFI bindings crate, which follows its upstream Lean library name per Rust's -sys convention.

Crates Verity must build itself

  • verity-types — consensus container definitions (Block, State, Vote, …) and constants. The Serialization capability (SSZ encode / decode, hash_tree_root) is currently satisfied by an external SSZ library behind an adapter in Runtime Shell; the contract (typed value ↔ bytes / root) is stable whether that implementation is the external Rust library or a Lean implementation reached over FFI. Foundational; depended on by every other crate.
  • verity-consensus-sys — raw FFI bindings to Verity Consensus, which is built and proven in formal-leanSpec and consumed here as a static library: Verity Consensus is the compiled, exported subset of that repository's Lean model — the intended mechanism is a dedicated export target (VerityConsensus) holding the @[export] wrappers over the model. Confines all unsafe. Named after that export target. It is the swappable backend behind the capability contracts: its exported function set is exactly whatever Verified Core currently hosts, and is expected to expand or contract as the frontier moves.
  • verity-chain — the single writer that owns the consensus state and the fork-choice store, and coordinates the State and Store aggregates under one consistency boundary. The only caller of Verity Consensus; wraps verity-consensus-sys behind a safe API. Reads and writes through verity-db.
  • verity-validator — validator duties (production only): block and vote production, signing, and aggregation.
  • verity (binary) — the executable validators run: orchestrator, slot clock, wiring, backpressure.

Thin glue over existing libraries

  • verity-p2p — gossip and req/resp over libp2p.
  • verity-crypto — adapter over leanMultisig (XMSS verify / sign / aggregate).
  • verity-db — persistence (Repository): blocks, states, and the finalized anchor, over an embedded key-value store. Keeps the storage concern out of the single-writer aggregate coordinator.
  • verity-rpc — HTTP API surface.
  • verity-metrics — implementation of the leanMetrics contract.

Layer mapping: Verified Core = Verity Consensus (the compiled export subset of formal-leanSpec, not a Cargo crate); Runtime Shell = verity-consensus-sys, verity-types, verity-chain, verity-crypto, verity-db; I/O Edge = verity-p2p, verity-validator, verity-rpc, verity-metrics, verity (binary).

flowchart TB
    subgraph ZC["I/O Edge"]
        BIN["verity (bin)"]
        VAL["verity-validator"]
        RPC["verity-rpc"]
        MET["verity-metrics"]
        P2P["verity-p2p"]
    end
    subgraph ZB["Runtime Shell"]
        CHAIN["verity-chain"]
        CRYPTO["verity-crypto"]
        DB["verity-db"]
        TYPES["verity-types"]
        SYS["verity-consensus-sys"]
    end
    subgraph ZA["Verified Core · Verity Consensus"]
        LEAN["Verity Consensus<br/>(Lean repo)"]
    end
    BIN --> VAL
    BIN --> RPC
    BIN --> MET
    BIN --> P2P
    BIN --> CHAIN
    VAL --> CHAIN
    VAL --> CRYPTO
    RPC --> CHAIN
    MET --> CHAIN
    P2P --> CHAIN
    CHAIN --> SYS
    SYS ==> LEAN
    CHAIN --> DB
    CHAIN --> TYPES
    CRYPTO --> TYPES
    DB --> TYPES

Capability contracts

The Verified Core ↔ Runtime Shell boundary is expressed not as a fixed list of FFI functions but as a small set of capability contracts — Rust-side interfaces (traits), one per consensus capability that could be realized on either side of the proof boundary:

  • StateTransitionstate_transition(pre_state, verified_block) -> Result<post_state>
  • ForkChoiceDecision — the pure decision: fork_choice_decision(view) -> head / safe_target / updated view
  • Serialization / HashTreeRoothash_tree_root(value) -> root, encode / decode
  • SignatureVerification — verify aggregate (Type-1 / Type-2) proofs

Each contract admits two implementations: a native-Rust implementation (the capability lives in Runtime Shell) or an FFI-into-Lean implementation provided by verity-consensus-sys (the capability lives in Verified Core). Consumers such as verity-chain depend only on the contract and never learn whether it is Lean-backed. Which side hosts a capability is therefore the combination of: (a) which implementation is bound — a wiring decision in the verity binary, constrained by what is actually proven; (b) where the proof obligation sits; and (c) whether that capability's functions appear in the verity-consensus-sys export set.

Error model. Failure is part of the contract, in two strictly separated layers:

  • Protocol rejection (an invalid block) is a value. In the Lean model it is a pure Except-style result; in the contract it is the Err arm of the shared Result. The error type is a plain enum (ProcessingError), defined in the contract crate alongside the traits, so the native-Rust and FFI-into-Lean implementations return the same type and a migration leaves the error path untouched. At the C ABI the FFI implementation uses the conventional shape — an int32 status code plus an out-parameter for the result — with the status codes in one-to-one correspondence with the Lean model's rejection reasons; that correspondence table is kept next to the Lean definition it mirrors, and the code→enum conversion is confined to verity-consensus-sys. Rejection reasons are a small closed set (the Runtime Shell delivers already-verified inputs, so FFI-level rejection is rare by design), which is why a code enum suffices and no structured error payload crosses the ABI.
  • Runtime failure (Lean runtime allocation failure) is not a value and is not modeled in the contract. The Lean runtime can abort the process on allocation failure, and Verity designs on the assumption that this cannot be hooked. Such an abort is classed with a Rust-side OOM abort: an availability failure, not a safety failure. The panic-freedom claim is precise on this point — it asserts that no code path returns an incorrect consensus value, not that a linked runtime can never abort; the residual abort condition is listed in the trust base.

The contracts' "already-verified inputs" clause has concrete, named content: formal-leanSpec's theorems are proved relative to explicit well-formedness predicates — Store.WellFormed for the fork-choice store, AnchorWF (discharged by Reachable) for the state, and ValidatorRegistry.WellFormed for validator keys. Maintaining those predicates across every mutation is Runtime Shell's half of the contract: Verified Core's theorems speak only about inputs that satisfy them, so the single writer must preserve them, and the boundary harnesses target exactly them (see the Model-Checking Strategy).

The contracts must be defined inner to both their consumers and their implementations — otherwise verity-consensus-sys implementing a contract defined in verity-chain would force a sys → chain edge and break the inward invariant. The recommended home is a thin contract crate (e.g. verity-consensus-api) holding only the trait definitions — the minimal expression of a movable boundary; folding them into verity-types is the alternative but mixes container shape with capability behavior. The final crate placement is an implementation-time decision; what matters architecturally is that the boundary is a contract, not a hardcoded call site.

Settled (kickoff decision, 2026-07-22). Proposer selection lives chain-side — a pure function next to the state transition and fork choice, not a verity-validator concern. Like everything else it starts as native Rust, and its pure-function shape keeps it a candidate for later adoption into the Verified Core.

Open for discussion. Whether duty scheduling, signing, and aggregation should be separate crates rather than folded into verity-validator once the workspace split happens.

The FFI seam — marshalling cost and verification

When a contract is bound FFI-into-Lean, every call marshals its inputs across the C ABI: the Rust value is promoted into the Lean object representation on the way in and the result lowered back on the way out. For StateTransition and ForkChoiceDecision that means the full state or fork-choice view crosses the seam per call. This layer deserves explicit attention, because it is the weakest trusted link in the whole chain: the Lean theorems stop at Lean values, so a conversion bug (a transposed field, an endianness slip, a truncated list) makes the proven function compute correctly on the wrong input — and no proof, on either side, can see it.

Two obligations follow:

  • Verification. The promote/lower code is boundary code in the Runtime Shell and is the primary target of the boundary harnesses (round-trip properties, no-panic-on-any-input, range enforcement — see the Model-Checking Strategy). Cross-language behavioral equivalence is additionally evidenced by shared leanSpec vectors run on both sides; verifiable-stf demonstrates the strongest form of that evidence — the compiled-Lean and compiled-Rust STF produce byte-identical outputs on the same inputs.

  • Measurement. Adopting a Lean implementation behind a contract is gated on measured cost, not assumed cost. Two data sets exist today:

    • leanSSZ's C ABI PoC (Rust-caller round-trip and hash_tree_root match): STF+HTR 27.5 ms at V=4096, within budget; per-op on a ~526 KB state, serialize 33 ms / hash_tree_root 58 ms / deserialize 54 ms (list-based codec, uncached merkleization).
    • verifiable-stf's compiled-Lean vs compiled-Rust STF comparison (RISC-V zkVM cycles, a proxy for relative native cost): 26.1 M vs 12.5 M cycles at N=10 and 35.3 M vs 14.4 M at N=100 — the Lean runtime's one-time Init accounts for ~15 M of the Lean side, so the steady-state Lean overhead is roughly 1.4× Rust once initialization is amortized across a long-lived process.

    These numbers are inputs to the migration triggers below: a capability moves into the Verified Core only when its measured seam cost fits the slot-time budget.

Interchange shape — a conditional design, not an adoption decision. Nothing here decides whether any capability is bound to Lean — that remains gated per capability (stable, proved, measured-within-budget). What is fixed now is only the shape the seam takes if a binding happens, so that a future adoption is a re-binding rather than a redesign:

  • Long-lived values stay resident. The consensus state and fork-choice view do not round-trip per call. The Rust side holds an opaque handle to a Lean-resident value (process_block(state_handle, block) -> new_state_handle), which fits Lean's immutable, reference-counted values and eliminates the per-call state marshalling cost entirely. The single-writer discipline makes ownership simple: the store is the only holder. Persistence and crash recovery are defined by SSZ export/import at the DB, not by the handle.
  • Inputs cross as SSZ bytes, decoded by the callee. Per-call inputs (blocks, votes) are passed in their SSZ wire form and decoded on the Lean side. This deliberately avoids constructing Lean objects field-by-field from Rust (lean_alloc_ctor-style), which would couple the shell to the Lean object layout and concentrate unsafe exactly where a conversion bug is least detectable. Bytes-as-interchange means the conversion is the consensus-critical wire format itself — already fixture-tested on both sides — and, if the Lean side ever ships a proven decoder, the Lean half of the seam becomes proven code.

Field-by-field construction is not banned outright; it is the last resort, admitted only where measurement shows the byte path cannot fit the budget.

Boundary migration

Because a zone is a guarantee level and placement is a snapshot, components are expected to cross the Verified Core ↔ Runtime Shell boundary over the life of the project — the verification boundary moves. The capability contracts are what make this affordable: a migration is a re-binding plus a move of the proof obligation, not a redesign.

Cost model — what a migration touches, and what it must not. A migration may change:

  • which implementation is bound behind the capability contract (native-Rust ↔ FFI-into-Lean);
  • where the proof obligation sits (a Lean proof vs. a language-level / external-library guarantee);
  • the verity-consensus-sys export set (it grows or shrinks);
  • which crate the implementation lives in.

A migration must not change:

  • consumer code (verity-chain, verity-validator) — it depends on the contract, not the placement;
  • consensus container shapes (the verity-types shared model) — shape is separable from the serialization behavior that may move (see the Domain Model);
  • the zone definitions (the guarantee levels);
  • the inward invariant (calls still flow toward higher assurance; Verified Core still never calls outward).

Anticipated migrations. Two are foreseen, in opposite directions, alongside two partial placements already in the design:

CapabilityTodayAnticipated moveTriggerEffect
State transitionVerified CoreVerified Core → Runtime ShellAn upstream spec for SNARK-proving the consensus STF materializes (none published as of 2026-07; see Ethlambda notes)Verified Core export set shrinks; FFI surface contracts; the StateTransition contract is bound to a zkVM-friendly (Rust / leanVM) implementation
SSZ / hash_tree_rootRuntime ShellRuntime Shell → Verified CoreA Lean-verified merkleization becomes availableVerified Core computes its own roots; "Verity Consensus receives precomputed roots" no longer holds; verity-types calls inward to Verified Core for the Serialization contract
Fork choiceVerified Core (decision) + Runtime Shell (Store)The worked example of a capability split across the boundary: a pure decision in Verified Core over a mutable Store owned in Runtime Shell
Proposer selectionRuntime Shell (pure function, chain-side)Runtime Shell → Verified Core (candidate)Verified in Lean and pulled into the export setSame pattern as SSZ: a pure decision whose shape is already what the core requires

The STF row is not a decision to move it — the working position is that the STF stays in Verity Consensus (Lean 4). It is recorded so the design is shown to withstand the move if the trigger fires; the full tension is in the Ethlambda notes.

Notes

  • What "proven" means — the artifact chain, the proposition catalog, and the trust base — is defined in Formal Verification.
  • Function names in the diagrams (process_block, on_block, get_head, …) are indicative and will be reconciled with leanSpec (lstar HEAD) when implementation begins.