Introduction

On February 16th, 2026, zkSecurity started a security audit of Zeko’s codebase. The audit lasted two weeks with two consultants. We reviewed the zeko repository on the compatible branch at commit 007b02e0cc.

Overall, we found the code to be of good quality, and the design trade-offs to be well documented and reasonable for the current stage of the project.

This is zkSecurity’s second audit of Zeko. The first audit is available here: first audit.

Scope

The audit covered the following components:

  • Circuit changes since the previous audit, specifically: the emergency DA rule, the emergency DA folder, changes to the account set insertion logic, Data Availability (DA) threshold signature verification, and changes to the commit and emergency commit rules.
  • The sequencer.
  • The Data Availability (DA) layer.
  • Deployment code for the rollup.

Threat model and security assumptions

Throughout the audit, we looked at different components of the rollup and considered the potential impact of vulnerabilities in those components under different threat models. We give a brief overview of the assumptions.

When looking at the rollup circuits, we assume that the sequencer and the prover are malicious and want to produce invalid state transitions or disrupt rollup operations. One important note is that the circuits assume that DA nodes are honest, meaning that DA nodes only sign states for which they have stored the relevant openings. Technically, this renders Zeko a validium, not a proper rollup, because the data availability layer is not strictly enforced by the L1 but relies on external assumptions.

When looking at the DA node code, we assume that these are public services that can be used by anyone, and that the sequencer is malicious and wants signatures from DA nodes without sending all relevant data to them.

Finally, when looking at the sequencer code, we assume that users submitting transactions and making queries to the sequencer are malicious and want to disrupt rollup operations, for example by sending malformed transactions or making queries that cause DoS. We note that the sequencer should be the least security-critical component of the system, as if both circuits and the DA layer work as intended, then the sequencer should be able to recover from malicious user behavior, and the worst-case scenario is a temporary service disruption.

As a final observation, the sequencer is currently able to censor transactions arbitrarily, and that is a documented design trade-off, so we did not consider censorship as a potential attack vector for the sequencer.

Changes to the zkapp circuits

We begin by describing the main changes to the zkapp circuits since the previous audit, and their security implications. We refer to the previous audit for a more general description of the circuits and their design.

Account set insertion logic

The account set is an incremental Merkle tree (IMT) that tracks the set of accounts that are currently present in the rollup. The account set is used to enforce that no duplicate accounts are created in the ledger. To do this, the insertion logic acts as both a non-inclusion proof for the new account, and an insertion proof for the updated account set root. We refer to the previous audit for a more thorough description of the account set and its insertion logic.

The main change to the account set insertion operation is that the IMT is enforced to be filled from left to right, meaning that leaves cannot be inserted at arbitrary positions in the tree and must be inserted in the order of the transactions being processed. This is done to prevent the sequencer from inserting new accounts in arbitrary positions in the tree, which would not allow users to reconstruct leaf contents from the IMT root in case of an emergency commit. The implementation of this behavior enforces that a new account leaf is inserted right next to a non-empty leaf, which, together with the assumption that the tree is initialized with at least one leaf on the far left, guarantees that the IMT is filled from left to right.

Data Availability for normal commits

When the sequencer produces a normal commit, it must post diff data to the DA layer. We describe the DA layer in more detail in the next section, but for now it suffices to say that the DA layer is composed of nodes that are assumed to be honest and sign target ledger and account-set roots only if they have stored the relevant openings to fully reconstruct those states. The public keys of all DA nodes are fixed, and a running hash of all keys is stored in the outer state as da_key. The commit rule requires DA signatures over the target ledger and account set from at least quorum DA nodes, where quorum is a configurable parameter.

Emergency DA rule and folder

The emergency commit is a special commit operation that can be performed by anyone in case of an emergency, such as a sequencer outage. The main difference from a normal commit is how data availability is handled. Indeed, the emergency commit must also enforce a DA mechanism; otherwise users can permanently lose access to their funds if a malicious user performs an emergency commit and does not publish all relevant ledger openings.

This is addressed by the emergency DA mechanism, which we now describe. The main idea is that the emergency commit also requires users to have stored all the openings required to reconstruct the target ledger state from the source ledger state as actions in a special account, called the emergency DA account. Actions in this account can be appended by anyone providing a proof for the emergency DA rule. Each action stores the following data:

module Action = struct
  type t =
    { source_ledger_hash : Ledger_hash.t
    ; target_ledger_hash : Ledger_hash.t
    ; ledger_index : Checked32.t
    ; account : Account.t
    }
  [@@deriving snarky]
end

The witness of this rule also includes an old_account and a Merkle path. The rule enforces that the implied root reconstructed from the old_account and the Merkle path matches the source_ledger_hash, and that the new root reconstructed from the account and the same Merkle path matches the target_ledger_hash. In particular, the rule enforces that the new account leaf, stored into account, is a correct opening for the new ledger hash. This ensures that users have access to all the data required to perform one ledger “step” from the source ledger to the target ledger.

This process can be extended to a chain of actions: if there exists an action with source A and target B, and another action with source B and target C, then users can reconstruct the path from A to C by following the chain of actions. This is exactly the idea behind the emergency DA folder circuit. This circuit is an instantiation of a “folder” circuit, meaning it is a circuit that recursively builds a claim. The claim has the following shape:

module Stmt = struct
  type t =
    { source_ledger : Ledger_hash.t
    ; target_ledger : Ledger_hash.t
    ; target_action_state : F.t
    }
  [@@deriving snarky]
end

The semantics are: “there exists a chain of actions, whose running hash is equal to target_action_state, that leads from source_ledger to target_ledger”. The emergency commit rule requires a proof for the emergency DA folder, with source and target ledger equal to the source and target ledger of the commit, and target_action_state equal to the current action state of the emergency DA account. This ensures that users have access to all the data required to reconstruct the path from the source ledger to the target ledger, and thus to reconstruct the target ledger itself.

DA layer

The DA layer is built as a committee of standalone nodes. Each node exposes an Async.Rpc interface and signs a ledger hash only after receiving enough data to reconstruct the corresponding state transition. In this model, a DA signature is an attestation that the node can reproduce the transition from a source ledger hash to a target ledger hash.

Each posted diff contains a source ledger hash, a target ledger hash, a list of account updates indexed by position, and an optional command payload. Operationally, the critical write path is Post_diff: this is where the node verifies consistency and decides whether to sign. The remaining RPCs (Get_diff, Has_diff, Get_diff_source, Get_signature, and the chain endpoints) are read and retrieval utilities that let clients reconstruct history and fetch attestations.

The core operation is Post_diff, which accepts a client-supplied diff and, if it passes validation, stores it and returns a signature over the target ledger hash and account-set root. The node’s validation pipeline for this operation is as follows:

  1. It checks that the provided openings match the claimed source ledger root.
  2. It checks that the source is known locally (or allowed as an empty-ledger start).
  3. It checks that diff indices are unique.
  4. It applies each account update, enforcing index consistency, and recomputes the target ledger root.
  5. It signs a message that commits to both the recomputed target ledger root and the account-set root.
  6. If a command is present, it recomputes receipt-chain updates and checks they match the target ledger accounts.
  7. It checks that newly created ledger accounts appear in the same order as their account-set entries.
  8. It attaches metadata (timestamp and account-set root) to the stored diff.
  9. It stores the diff under the target ledger hash, unless that target is already present in the database.

This flow is the main defense against a malicious sequencer trying to obtain signatures for states that cannot be reconstructed from posted data. In particular, the signature is gated by local replay of all the account updates. The optional command payload is checked through receipt-chain validation, which ties command execution to the resulting account receipt-chain hashes in the target ledger.

Sequencer design

The sequencer is the main off-chain component that accepts user transactions, updates local state, pushes diffs to the DA layer, and proves commits that are posted on the L1. Operationally, its role is to maintain a consistent pipeline from transaction intake to final commit while keeping enough local and DA-backed history to recover after failures.

A transaction first enters through GraphQL, where the sequencer checks pool constraints, fee requirements, proof/signature validity, and slot-range admissibility. It is important that this is the only place where fees are checked, as the circuits never enforce any fee requirement, and thus the sequencer is responsible for choosing the right fee policy and enforcing it. Notice that even a malicious sequencer will try to enqueue as many transactions as possible to collect as much fee revenue as possible. The slot acceptance window is a safety buffer: it reduces the risk of proving transactions that expire before they can be committed. Accepted transactions are applied to the local ledger and account-set state, producing witnesses for proving.

For DA, the sequencer builds a diff per applied command and posts diffs to DA nodes in strict order. At commit time, the sequencer collects signatures for the latest target hash to satisfy the DA multisig condition enforced onchain. Proving is decoupled through a message queue and a parallel merger. Base proofs are generated asynchronously, merged in the background, and committed in order by closing batches into commit-ready units. The sequencer periodically closes the parallel merging queue to produce a final commit proof, which is posted on the L1.