Introduction
On October 6th, 2025, Lighter tasked zkSecurity with auditing the Plonky 2 wrapper circuits. The audit lasted one week with two consultants. During the engagement, the zkSecurity team was granted access to the relevant parts of the Lighter codebase. A number of observations and findings have been reported to the Lighter team. The findings are detailed in the latter section of this report. The codebase was found to be of high quality, accompanied by unit tests and an integration test for the wrapper circuit.
Scope
The primary scope of this audit was the Wrapper layer of the Lighter circuits. More specifically, it served as a follow-up to two engagements conducted one and two months earlier. The first audit focused extensively on Lighter’s top-level circuits in the Prover’s circuit/src directory as well as all files in circuit/src/types/ and circuit/src/transactions/, along with circuit/src/bigint/unsafe_bit/mod.rs. The second audit focused on changes introduced by the Lighter team and included the directories: src/delta, src/recursion, src/bigint, src/hints, and src/comparison.
This audit focused explicitly on the Wrapper circuits of Lighter. Specifically, it covered the commit 0028b73b1e38de7e7c7e4ad33c7680a39e2c6c90 and the following components:
WrapperInnerCircuit,WrapperOuterCircuit,WrapperCircuitfromsrc/recursion/wrapper_circuit.rsBlobEvaluationCircuitfromsrc/blob/blob_constraints.rsEvaluateBitstreamGatefromsrc/blob/evaluate_bitstream.rsBlobPolynomialTargetfromsrc/blob/blob_polynomials.rs
Importantly, the BLS12-381 code and the Gnark outer circuit were not covered in this engagement.
Overview
The reports from our three previous audit engagements already provide extensive explanations of Lighter’s protocol. To avoid redundancy, we will not repeat those details here. Instead, we refer the reader to the first, the second and the third reports, as well as the whitepaper and documentation, for an introduction to the protocol.
Accordingly, this report does not include a general overview. Rather, we briefly highlight the wrapper circuits that were the focus of this particular audit and explain their logic.
WrapperInnerCircuit
This section describes what the Wrapper Inner Circuit verifies, how each sub‑step works, and how the blob bytes are structured, read, and connected into the various checks.
The following figure depicts the Wrapper Inner Circuit logic.

High‑Level Flow
handle_segment_proofs- Verifies the first segment and enforces the initial invariants (first segment is empty).
- Conditionally verifies the remaining segment proofs while
i < segment_countand merges them into a singleBatchTargetwith chained continuity (block numbers, timestamps, state roots, pubdata hash, priority op prefixes).
verify_batch_commitment- Computes
blob_commitment_hash = keccak(opening_x || opening_y || kzg_versioned_hash). - Computes
batch_commitment = keccak(batch fields || blob_commitment_hash)and connects it to the public batch commitment.
- Computes
verify_version_and_reserved_data- Enforces
blob_bytes[0..34] = 0(2‑byte version + 32‑byte reserved).
- Enforces
verify_latest_market_data- Decodes big‑endian mark prices and funding prefix sums from the blob header and connects them to
batch.new_public_market_details.
- Decodes big‑endian mark prices and funding prefix sums from the blob header and connects them to
handle_delta_chain_proof- Verifies the delta recursion proof and parses
AggregatedDeltaTarget.
- Verifies the delta recursion proof and parses
verify_aggregated_delta- Recomputes the account‑delta root from the delta proof and must enforce equality with
batch.new_account_delta_tree_root. - Derives the delta evaluation point
z = Poseidon2(pubdata_hash(blob_bytes[BLOB_ACCOUNT_OFFSET..]), account_delta_root)and asserts it equals the delta proof’sevaluation_point.
- Recomputes the account‑delta root from the delta proof and must enforce equality with
handle_blob_evaluation_proof. Verifies the blob‑evaluation proof and connects:batch.new_account_delta_tree_root == blob.account_delta_tree_root(kzg_versioned_hash, opening_x, opening_y)blob_bytes[i]batch.new_public_market_details == blob.public_market_details
verify_delta_polynomial_evaluation- Replays the pubdata polynomial over
blob_bytes[BLOB_ACCOUNT_OFFSET..]at the samezand matches the delta proof’sevaluation.
- Replays the pubdata polynomial over
Blob Bytes Layout (126,976 bytes total = 4096 × 31)
Offsets are inclusive ranges (0‑based), sizes in bytes.
- [0..1] Version (2B, big‑endian) – enforced zero in wrapper.
- [2..33] Reserved (32B) – enforced zero in wrapper.
- [34..1053] Mark Prices (255 x 4B, big‑endian per u32)
- For market
iin [0..254], mark_price[i] at[34 + 4i .. 34 + 4i + 3].
- For market
- [1054..3348] Funding Rate Prefix Sum (255 × 9B)
- Per market
i:[0]=sign_flag (1 if negative, else 0); [1..4]=abs limb hi (u32 BE); [5..8]=abs limb lo (u32 BE).
- Per market
- [3349..126,975] Account‑Delta Pubdata Region
- Encoded as base‑16 numbers: each number is prefixed by a length nibble; then that many nibbles follow.
- The wrapper splits each byte into 2 nibbles (little‑endian nibble order per byte) and feeds them to
EvaluateBitstreamGate.
How these bytes feed other components:
- Polynomial coefficients (blob‑eval circuit): the entire blob is chunked into 4096 groups of 31B. Each group is parsed as a big‑endian 32B integer by prepending a 0x00 byte and then reduced into BLS12-381 scalars, ensuring
< modulus(seeblob_polynomial.rs: from_bytes). - Pubdata hash for delta FS: wrapper packs
[3349..end]into 7‑byte big‑endian limbs and Poseidon2‑hashes the sequence to computepubdata_hash. - Bitstream: wrapper splits
[3349..end]into nibbles and feeds them intoEvaluateBitstreamGateto reconstruct the polynomial evaluation atz.
Delta Polynomial Evaluation Fiat-Shamir Challenges
The delta polynomial evaluation is performed as follows:
- The coefficients of the delta polynomial are extracted from the account-delta pubdata region of the blob bytes, specifically from
blob_bytes[BLOB_ACCOUNT_OFFSET..]. - The evaluation point
zis derived from both the account-delta pubdata and the account_delta_root. This is calculated asz = Poseidon2(pubdata_hash, account_delta_root), where:pubdata_hashis obtained by hashing the blob bytes from the account offset to the end using Poseidon2 (blob_bytes[BLOB_ACCOUNT_OFFSET..]).account_delta_rootis sourced from the delta recursion proof.
This approach ensures that the delta polynomial evaluation is intrinsically linked to both the actual blob data and the resulting account delta tree root, thereby providing strong integrity and consistency guarantees.
BlobEvaluationCircuit
The BlobEvaluationCircuit is a component used in the WrapperInnerCircuit. It is responsible for evaluating the KZG polynomial commitments over the blob data using the BLS12-381 elliptic curve. The circuit takes as inputs the blob data, the corresponding KZG commitments, and the evaluation points. It then computes the evaluations of the polynomials at the specified points and verifies that these evaluations are consistent with the provided KZG commitments.
The BlobEvaluationCircuit performs the following key functions:
blob_data_hash = hash(version_and_reserved || market_details || account_delta_tree_root)blob_polynomial_opening_x == hash(blob_data_hash || kzg_versioned_hash)blob_polynomial_opening_y == evaluate_polynomial(blob_polynomial, blob_polynomial_opening_x)
Where:
blob_polynomialis the polynomial computed from the blob bytes (the whole blob bytes, not just a part like inverify_delta_polynomial_evaluation).version_and_reservedis a fixed 32-byte prefix of the blob bytes.market_detailsis the market details of the batch (not part of the blob bytes, but they are checked to be the same in theWrapperInnerCircuit).account_delta_tree_rootis the new account delta tree root of the batch.kzg_versioned_hashis the versioned hash of the KZG commitment of the blob polynomial.blob_polynomial_opening_xandblob_polynomial_opening_yare the and of the KZG opening proof .
This ensures that the blob data is correctly committed to the KZG polynomial and that the evaluations are valid.
EvaluateBitstreamGate
The EvaluateBitstreamGate is a custom gate used within the WrapperInnerCircuit. It is responsible for evaluating the blob bytes using a bitstream representation. It takes as inputs an evaluation point and the blob half-bytes, and computes the evaluation of the blob polynomial at the given point. The gate ensures that the evaluation is consistent with the bitstream representation of the blob data.
The evaluation is performed using a sequence of state updates using an Horner-like method state(i+1) = state(i) * x + next_half_byte, where x is the evaluation point and next_half_byte is the next half-byte from the blob data.
WrapperOuterCircuit and Wrapper Circuit
The second wrapper circuit is the WrapperOuterCircuit, which is responsible for verifying the proof generated by the WrapperInnerCircuit. It takes as public inputs the proof of the inner circuit along with its public inputs, and verifies the proof using Plonky2’s proof verification mechanism. Together, the WrapperInnerCircuit and WrapperOuterCircuit are encapsulated within the structure WrapperCircuit.
The WrapperOuterCircuit uses Poseidon Goldilocks BN128 (also known as BN254) as its hash function, which is Ethereum-friendly. This ensures compatibility with Ethereum smart contracts, which may be used to verify the proofs generated by the WrapperOuterCircuit.