From 0d97d9a7847e3a1cb18ba36f48c7e5b98332952c Mon Sep 17 00:00:00 2001 From: Jun Song Date: Wed, 29 Jul 2026 16:44:47 +0900 Subject: [PATCH 1/2] [PeerDAS] Expose compute_cells (cells without proofs) through the C ABI compute_cells already existed as a Nim-only API. Consensus clients on the PeerDAS hot path receive cell proofs from the execution layer (engine_getBlobsV2 / BlobsBundleV2) and only need the erasure-extended cells: one size-4096 IFFT + one size-4096 FFT (~1.5 ms/blob) instead of the 128 FK20 MSMs of compute_cells_and_kzg_proofs (~80-140 ms/blob), a ~50x difference. Align its signature with the other PeerDAS exports (ptr UncheckedArray[Cell] + FFI nil-pointer validation of ctx and cells) and export it as ctt_eth_kzg_compute_cells, declared in ethereum_eip7594_peerdas.h. As with the sibling exports, the blob parameter is passed by reference and must be non-NULL from C. Matches the compute_cells function of the consensus spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/fulu/polynomial-commitments-sampling.md Co-Authored-By: Claude Fable 5 --- benchmarks/bench_eth_eip7594_peerdas.nim | 2 +- benchmarks/eth_eip7594/perf_compute_cells.nim | 3 ++- constantine/eth_eip7594_peerdas.nim | 10 +++++++--- .../protocols/ethereum_eip7594_peerdas.h | 13 +++++++++++++ .../t_cells_and_kzg_proofs_opt.nim | 2 +- tests/eth_eip7594_peerdas/t_compute_cells_opt.nim | 4 ++-- tests/eth_eip7594_peerdas/t_peerdas_recovery.nim | 12 ++++++------ tests/t_eth_eip7594_peerdas.nim | 2 +- 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/benchmarks/bench_eth_eip7594_peerdas.nim b/benchmarks/bench_eth_eip7594_peerdas.nim index ad60a0cb4..7bf958bc5 100644 --- a/benchmarks/bench_eth_eip7594_peerdas.nim +++ b/benchmarks/bench_eth_eip7594_peerdas.nim @@ -173,7 +173,7 @@ proc benchComputeCells(b: BenchSet, ctx: ptr EthereumKZGContext, iters: int) = var cells: ref array[CELLS_PER_EXT_BLOB, Cell] new(cells) bench("compute_cells (half-FFT optimization)", iters): - doAssert cttEthKzg_Success == ctx.compute_cells(cells[], b.blobs[0]) + doAssert cttEthKzg_Success == ctx.compute_cells(cells[].asUnchecked(), b.blobs[0]) proc benchComputeCellsAndKZGProofsNoPrecomp(b: BenchSet, ctx: ptr EthereumKZGContext, iters: int) = ## Compute cells and proofs together using FK20 algorithm diff --git a/benchmarks/eth_eip7594/perf_compute_cells.nim b/benchmarks/eth_eip7594/perf_compute_cells.nim index 76f9d185f..6ad53740b 100644 --- a/benchmarks/eth_eip7594/perf_compute_cells.nim +++ b/benchmarks/eth_eip7594/perf_compute_cells.nim @@ -9,6 +9,7 @@ import benchset_serialization, constantine/eth_eip7594_peerdas, + constantine/platforms/primitives, constantine/ethereum_eip4844_kzg_parallel, ../bench_blueprint, std/[os, strutils, monotimes] @@ -29,7 +30,7 @@ proc benchComputeCells(b: BenchSet, ctx: ptr EthereumKZGContext, iters: int) = var cells: ref array[CELLS_PER_EXT_BLOB, Cell] new(cells) bench("compute_cells", iters): - doAssert cttEthKzg_Success == ctx.compute_cells(cells[], b.blobs[0]) + doAssert cttEthKzg_Success == ctx.compute_cells(cells[].asUnchecked(), b.blobs[0]) proc main() = echo "PeerDAS (EIP-7594) - compute_cells Benchmark" diff --git a/constantine/eth_eip7594_peerdas.nim b/constantine/eth_eip7594_peerdas.nim index 64385648b..ac3692b20 100644 --- a/constantine/eth_eip7594_peerdas.nim +++ b/constantine/eth_eip7594_peerdas.nim @@ -206,8 +206,8 @@ func compute_cells_impl( func compute_cells*( ctx: ptr EthereumKZGContext, - cells: var array[CELLS_PER_EXT_BLOB, Cell], - blob: Blob): cttEthKzgStatus = + cells: ptr UncheckedArray[Cell], + blob: Blob): cttEthKzgStatus {.libPrefix: prefix_eth_kzg, raises: [].} = ## Compute all cells for an extended blob using the half-FFT optimization. ## This is the MOST efficient known method for computing cells. ## @@ -251,6 +251,10 @@ func compute_cells*( ## e. Bit-reverse to match cell ordering ## 4. Convert cells to bytes [Serialization] + # Validate FFI pointers before dereferencing + if ctx.isNil or cells.isNil: + return cttEthKzg_InputsLengthsMismatch + const N = FIELD_ELEMENTS_PER_BLOB # Deserialize blob to polynomial (evaluation form, bit-reversed) @@ -265,7 +269,7 @@ func compute_cells*( defer: freeHeapAligned(poly_coef_nat) poly_coef_nat[].lagrangeInterpolate(poly_eval_brp[], ctx.fft_desc_ext) - return compute_cells_impl(ctx, cells, poly_eval_brp[], poly_coef_nat[]) + return compute_cells_impl(ctx, cast[ptr array[CELLS_PER_EXT_BLOB, Cell]](cells)[], poly_eval_brp[], poly_coef_nat[]) func compute_cells_and_kzg_proofs*( ctx: ptr EthereumKZGContext, diff --git a/include/constantine/protocols/ethereum_eip7594_peerdas.h b/include/constantine/protocols/ethereum_eip7594_peerdas.h index a1d6a6cf8..a50d31c51 100644 --- a/include/constantine/protocols/ethereum_eip7594_peerdas.h +++ b/include/constantine/protocols/ethereum_eip7594_peerdas.h @@ -31,6 +31,19 @@ typedef struct { byte raw[CTT_BYTES_PER_CELL]; } ctt_eth_kzg_cell; // Ethereum EIP-7594 PeerDAS Interface // ------------------------------------------------------------------------------------------------ +/** Compute all cells for an extended blob, without KZG proofs. + * + * @param ctx KZG context (trusted setup) + * @param cells Output: array of 128 cells (caller-allocated) + * @param blob Input: the blob to compute cells for + * @return cttEthKzg_Success on success, error status otherwise + */ +ctt_eth_kzg_status ctt_eth_kzg_compute_cells( + const ctt_eth_kzg_context* ctx, + ctt_eth_kzg_cell* cells, + const ctt_eth_kzg_blob* blob +) __attribute__((warn_unused_result)); + /** Compute all cells and KZG proofs for an extended blob using the FK20 algorithm. * * @param ctx KZG context (trusted setup) diff --git a/tests/eth_eip7594_peerdas/t_cells_and_kzg_proofs_opt.nim b/tests/eth_eip7594_peerdas/t_cells_and_kzg_proofs_opt.nim index 489d3a78f..213a06f6f 100644 --- a/tests/eth_eip7594_peerdas/t_cells_and_kzg_proofs_opt.nim +++ b/tests/eth_eip7594_peerdas/t_cells_and_kzg_proofs_opt.nim @@ -87,7 +87,7 @@ func compute_cells_and_kzg_proofs_naive( poly_monomial.lagrangeInterpolate(poly_lagrange, ctx.fft_desc_ext) # Compute cells using the public API - let cells_status = compute_cells(ctx, cells, blob) + let cells_status = compute_cells(ctx, cells.asUnchecked(), blob) if cells_status != cttEthKzg_Success: return cells_status diff --git a/tests/eth_eip7594_peerdas/t_compute_cells_opt.nim b/tests/eth_eip7594_peerdas/t_compute_cells_opt.nim index 627933118..c0542403e 100644 --- a/tests/eth_eip7594_peerdas/t_compute_cells_opt.nim +++ b/tests/eth_eip7594_peerdas/t_compute_cells_opt.nim @@ -23,7 +23,7 @@ import constantine/eth_eip7594_peerdas {.all.}, constantine/ethereum_eip4844_kzg, constantine/serialization/codecs, - constantine/platforms/allocs, + constantine/platforms/[allocs, primitives], # Shared test utilities ../testutils/eth_consensus_utils @@ -179,7 +179,7 @@ suite "EIP-7594 PeerDAS - compute_cells [" & test_case & "]": var cells_opt: array[CELLS_PER_EXT_BLOB, Cell] - let status = compute_cells(ctx, cells_opt, blob[]) + let status = compute_cells(ctx, cells_opt.asUnchecked(), blob[]) doAssert status == cttEthKzg_Success, "compute_cells failed: " & $status let expectedCells = testData["output"].parseCells() diff --git a/tests/eth_eip7594_peerdas/t_peerdas_recovery.nim b/tests/eth_eip7594_peerdas/t_peerdas_recovery.nim index c8170a287..143bb7927 100644 --- a/tests/eth_eip7594_peerdas/t_peerdas_recovery.nim +++ b/tests/eth_eip7594_peerdas/t_peerdas_recovery.nim @@ -47,7 +47,7 @@ proc test_recover_from_64_cells*(ctx: ptr EthereumKZGContext) = echo " Blob created" var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) echo " compute_cells status: ", status doAssert status == cttEthKzg_Success @@ -84,7 +84,7 @@ proc test_recover_from_65_cells*(ctx: ptr EthereumKZGContext) = let blob = build_test_blob() var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) doAssert status == cttEthKzg_Success var available_indices: seq[CellIndex] @@ -120,7 +120,7 @@ proc test_recover_from_all_cells*(ctx: ptr EthereumKZGContext) = let blob = build_test_blob() var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) doAssert status == cttEthKzg_Success var available_indices: seq[CellIndex] @@ -153,7 +153,7 @@ proc test_recover_alternate_indices*(ctx: ptr EthereumKZGContext) = let blob = build_test_blob() var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) doAssert status == cttEthKzg_Success var available_indices: seq[CellIndex] @@ -187,7 +187,7 @@ proc test_too_few_cells_error*(ctx: ptr EthereumKZGContext) = let blob = build_test_blob() var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) doAssert status == cttEthKzg_Success var available_indices: seq[CellIndex] @@ -216,7 +216,7 @@ proc test_duplicate_indices_error*(ctx: ptr EthereumKZGContext) = let blob = build_test_blob() var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = ctx.compute_cells(cells, blob) + let status = ctx.compute_cells(cells.asUnchecked(), blob) doAssert status == cttEthKzg_Success var available_indices: seq[CellIndex] diff --git a/tests/t_eth_eip7594_peerdas.nim b/tests/t_eth_eip7594_peerdas.nim index a1d1b417c..5de2a2459 100644 --- a/tests/t_eth_eip7594_peerdas.nim +++ b/tests/t_eth_eip7594_peerdas.nim @@ -31,7 +31,7 @@ TestVectorsDir.testGen(compute_cells, "kzg-mainnet", testVector): parseAssign(testVector, blob, BYTES_PER_BLOB, testVector["input"]["blob"].content) var cells: array[CELLS_PER_EXT_BLOB, Cell] - let status = compute_cells(ctx, cells, blob[]) + let status = compute_cells(ctx, cells.asUnchecked(), blob[]) stdout.write "[" & $status & "]\n" if status == cttEthKzg_Success: From 3b51e477d7734e8544f8d0a87f043eeb91993a99 Mon Sep 17 00:00:00 2001 From: Jun Song Date: Wed, 29 Jul 2026 16:44:48 +0900 Subject: [PATCH 2/2] [PeerDAS] constantine-go: add EthKzgContext.ComputeCells Cells-only Go binding over ctt_eth_kzg_compute_cells, mirroring ComputeCellsAndKzgProofs, with a test over the consensus-spec-tests compute_cells vectors. Co-Authored-By: Claude Fable 5 --- constantine-go/constantine.go | 21 ++++++++ constantine-go/eth_kzg7594_peerdas_test.go | 56 ++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/constantine-go/constantine.go b/constantine-go/constantine.go index 4446a356b..79969f346 100644 --- a/constantine-go/constantine.go +++ b/constantine-go/constantine.go @@ -359,6 +359,27 @@ func (ctx EthKzgContext) VerifyBlobKzgProofBatchParallel(blobs []EthBlob, commit type EthKzgCell [2048]byte +func (ctx EthKzgContext) ComputeCells( + blob *EthBlob, +) (cells *[128]EthKzgCell, err error) { + if blob == nil { + return nil, errors.New("ComputeCells: blob is nil") + } + cells = new([128]EthKzgCell) + status := C.ctt_eth_kzg_compute_cells( + ctx.cCtx, + (*C.ctt_eth_kzg_cell)(unsafe.Pointer(cells)), + (*C.ctt_eth_kzg_blob)(unsafe.Pointer(blob)), + ) + if status != C.cttEthKzg_Success { + err = errors.New( + C.GoString(C.ctt_eth_kzg_status_to_string(status)), + ) + return nil, err + } + return cells, nil +} + func (ctx EthKzgContext) ComputeCellsAndKzgProofs( blob *EthBlob, ) (cells *[128]EthKzgCell, proofs *[128]EthKzgProof, err error) { diff --git a/constantine-go/eth_kzg7594_peerdas_test.go b/constantine-go/eth_kzg7594_peerdas_test.go index 72a74f919..729de6f1d 100644 --- a/constantine-go/eth_kzg7594_peerdas_test.go +++ b/constantine-go/eth_kzg7594_peerdas_test.go @@ -27,6 +27,7 @@ import ( var ( peerdasTestDir = "../tests/protocol_ethereum_eip7594_fulu_peerdas" + computeCellsTests = filepath.Join(peerdasTestDir, "compute_cells/kzg-mainnet/*/data.yaml") computeCellsAndProofsTests = filepath.Join(peerdasTestDir, "compute_cells_and_kzg_proofs/kzg-mainnet/*/data.yaml") verifyCellKzgProofTests = filepath.Join(peerdasTestDir, "verify_cell_kzg_proof_batch/kzg-mainnet/*/data.yaml") recoverCellsAndProofsTests = filepath.Join(peerdasTestDir, "recover_cells_and_kzg_proofs/kzg-mainnet/*/data.yaml") @@ -36,6 +37,61 @@ func (dst *EthKzgCell) UnmarshalText(input []byte) error { return fromHexImpl(dst[:], input) } +// ---- compute_cells (cells only, no proofs) ---- + +type computeCellsOnlyTest struct { + Input *computeTestInput `yaml:"input"` + Output *[]string `yaml:"output"` // [cells...] +} + +func TestComputeCells(t *testing.T) { + ctx, tsErr := EthKzgContextNew(trustedSetupFile) + require.NoError(t, tsErr) + defer ctx.Delete() + + tests, err := filepath.Glob(computeCellsTests) + require.NoError(t, err) + require.NotEmpty(t, tests) + + for _, tf := range tests { + testName := filepath.Base(filepath.Dir(tf)) + raw, rErr := os.ReadFile(tf) + require.NoError(t, rErr) + + var test computeCellsOnlyTest + require.NoError(t, yaml.Unmarshal(raw, &test)) + + // Invalid input -> no output + if test.Input == nil || test.Input.Blob == nil { + require.Nil(t, test.Output, "expected no output for missing input in %s", testName) + continue + } + + var blob EthBlob + if err := fromHexImpl(blob[:], []byte(*test.Input.Blob)); err != nil { + require.Nil(t, test.Output, "expected no output for invalid blob in %s", testName) + continue + } + + cells, err := ctx.ComputeCells(&blob) + if err != nil { + require.Nil(t, test.Output, "expected failure for %s", testName) + continue + } + + require.NotNil(t, test.Output, "expected output for %s", testName) + + expCells := *test.Output + require.Len(t, expCells, 128) + + for i := 0; i < 128; i++ { + expCell, err := hex.DecodeString(expCells[i][2:]) + require.NoError(t, err, "failed to decode expected cell %d in %s", i, testName) + require.Equal(t, expCell, cells[i][:], "cell %d mismatch in %s", i, testName) + } + } +} + // ---- compute_cells_and_kzg_proofs ---- type computeTestInput struct {