Introduction

On July 21st, 2025, the Ethereum Foundation engaged zkSecurity to perform a security assessment of the KZG libraries used by PeerDAS. These libraries include blst, c-kzg-4844, rust-eth-kzg, and go-eth-kzg. The goal of this assessment was to conduct an in-depth analysis of the various KZG polynomial commitment implementations (C, Rust, and Go) used for Ethereum’s EIP-4844 and EIP-7594, as well as some of their dependencies in the blst BLS12-381 library. Over the course of the engagement, we evaluated compliance with relevant specifications, compared cross-implementation behaviors, and identified potential safety issues. Specified bindings and assembly routines were excluded from the scope as defined.

The engagement lasted four weeks and was conducted by two consultants. During this period, several observations and findings were identified and communicated to the respective library development teams. The detailed findings and their implications are discussed in the subsequent sections of this report.

Scope

  • blst library at target commit v0.3.15: The scope includes implementations for blst_p1s_mult_pippenger_scratch_sizeof and blst_p1s_mult_pippenger, covering lower-level elliptic curve operations. However, the ASM implementations of finite field arithmetic are excluded.
  • c-kzg-4844 library at target commit 669e7484: The scope includes code associated with EIP-7594, such as common/lincomb.c and all of src/eip7594. It also includes bindings, except for bindings/python, bindings/elixir, and bindings/node.js.
  • rust-eth-kzg library at target commit v0.7.1: The scope covers code related to both EIP-4844 and EIP-7594, including bindings, except for bindings/golang.
  • go-eth-kzg library at target commit v1.3.0: The scope includes code associated with EIP-7594, excluding internal/kzg, internal/multiexp, internal/utils, tests, and all _test.go files.

Overview

EIP-7594

Following EIP-4844, a blob-carrying transaction in Ethereum can publish a blob containing 4096 field elements along with its KZG commitment. Validators must retrieve the entire blob and verify the commitment against it. EIP-7594 introduces 1-D peer DAS (Data Availability Sampling) for Ethereum. The blob is first extended using erasure coding (doubling its size). The extended blob is then divided into 128 cells, each containing 64 field elements. The blob is associated with a KZG commitment, and each cell has a KZG multi-proof to verify that its 64 field elements are consistent with the blob commitment. To ensure data availability, each validator only needs to receive and verify a small subset (e.g., 8) of the cells from the network, instead of downloading and verifying the entire blob as required in EIP-4844.

Cell Proof Generation

During block proposal, the proposer splits the blob into cells and generates a proof for each cell. First, the blob is extended with erasure coding to double its size, and then the extended blob is divided into cells, each containing 64 field elements. Next, the KZG multi-point proof for each cell is computed. The FK20 paper describes an efficient algorithm for computing all cell proofs of a blob. Below is the function signature for cell proof generation in c-kzg-4844:

C_KZG_RET compute_cells_and_kzg_proofs(
    Cell *cells, // The output cells
    KZGProof *proofs, // The output proofs for each cell
    const Blob *blob, // The input blob data
    const KZGSettings *s // The settings, including the trusted setup and precomputation
);

Cell Proof Verification

Validators retrieve a subset of cells and proofs from the network and verify these KZG multi-point proofs against the blob commitment. Since a block can contain multiple blobs, validators use a universal verification method to efficiently verify cells from different blobs in a single batch. Below is the function signature for cell proof verification in c-kzg-4844:

C_KZG_RET verify_cell_kzg_proof_batch(
    bool *ok, // The verification result
    const Bytes48 *commitments_bytes, // The commitment of the entire blob that the cell belongs to
    const uint64_t *cell_indices, // The indices of the cells in the blob
    const Cell *cells, // The array of cell data
    const Bytes48 *proofs_bytes, // The proofs for the cells
    uint64_t num_cells, // The number of cells in this batch, specifying the length of the above arrays
    const KZGSettings *s // The settings, including the trusted setup and precomputation
);

Blob Recovery

To recover the original blob, other nodes (e.g., index nodes) can retrieve cell data from the network. Once more than half of the cell data is retrieved, the entire blob can be reconstructed using erasure coding. Below is the function signature for blob recovery in c-kzg-4844:

C_KZG_RET recover_cells_and_kzg_proofs(
    Cell *recovered_cells, // The output recovered cells
    KZGProof *recovered_proofs, // The output recovered proofs for each cell
    const uint64_t *cell_indices, // The input indices of each cell
    const Cell *cells, // The input cell data
    uint64_t num_cells, // The number of input cells
    const KZGSettings *s  // The settings, including the trusted setup and precomputation
);

Multi-Scalar Multiplication (MSM) in blst

The blst library implements the BLS12-381 elliptic curve and pairing operations in C and assembly. Our scope focuses on the MSM (Multi-Scalar Multiplication) implementation, which primarily uses the Pippenger algorithm. Note that blst is designed for cryptographic signatures and implements constant-time operations to prevent side-channel attacks. However, for data availability sampling use cases, constant-time operations are not required and may reduce efficiency.

Given a list of base points [P0,P1,P2,...,Pn1] and scalars (all of fixed bit length, e.g., 256) [s0,s1,s2,...,sn1], the Pippenger algorithm efficiently computes i=0n1siPi.

The main idea is to divide the scalars into smaller windows. Within each window, the scalars are grouped into buckets based on their values, allowing for efficient computation. For example, consider four points [P0,P1,P2,P3] and four 4-bit scalars [11,9,3,7]. If the window size is 2 bits, the scalars are split into two parts: the lower half [3,1,3,3] and the upper half [2,2,0,1]. The computation is then performed separately for each window:

  • W0=3P0+1P1+3P2+3P3
  • W1=2P0+2P1+0P2+1P3

The final result is obtained by summing the contributions from each window: W0+4W1. Within each window, points with the same scalar value are grouped into the same bucket to reduce the number of operations. For example, W0 can be computed as 1P1+3(P0+P2+P3), and W1 as 2(P0+P1)+1P3.

Booth Encoding is an optimization technique that reduces the bucket size by half. Instead of using scalar values in the range [0,1,2,3], Booth encoding maps them to [2,1,0,1,2]. This optimization leverages the fact that the negative of a point P (denoted P) can be easily computed by negating its y coordinate.

For example, if a scalar is 2, the algorithm negates the point and places it in the bucket for 2, as 2P=2(P). Given a scalar s and a window of n bits, Booth encoding splits the scalar into smaller components wi within the range [2n1,2n1], while ensuring that 2niwi=s. This approach reduces the number of buckets required and improves efficiency.

The ptype##s_mult_pippenger function computes the multiplication for each window dynamically, while the ptype##s_mult_wbits function precomputes the multiplications for each window (e.g., [1P,2P,3P,4P]) and caches them. This allows for faster selection of points during computation but consumes more memory.