crypto/kzg4844: add cell-related functions (#34766)
Some checks are pending
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run

This PR adds three cell-level kzg functions required for the sparse
blobpool (eth/72).

- VerifyCells: Verifies cells corresponding to proofs. This is used to
verify cells received from eth/72 peers.
- ComputeCells: Computes cells from blobs. This is needed because user
submissions and eth/71 transaction deliveries contain blobs, while
eth/72 peers expect cells.
- RecoverBlobs: Recovers blobs from partial cells. This is needed to
support both eth/71 and eth/72

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
Bosul Mun 2026-04-23 15:39:07 +02:00 committed by GitHub
parent 8e2107dc39
commit 526ad4f6f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 499 additions and 45 deletions

View file

@ -34,9 +34,27 @@ var (
blobT = reflect.TypeFor[Blob]()
commitmentT = reflect.TypeFor[Commitment]()
proofT = reflect.TypeFor[Proof]()
cellT = reflect.TypeFor[Cell]()
)
const CellProofsPerBlob = 128
const (
CellProofsPerBlob = 128
CellsPerBlob = 128
DataPerBlob = 64
)
// Cell represents a single cell in a blob.
type Cell [2048]byte
// UnmarshalJSON parses a cell in hex syntax.
func (c *Cell) UnmarshalJSON(input []byte) error {
return hexutil.UnmarshalFixedJSON(cellT, input, c[:])
}
// MarshalText returns the hex representation of c.
func (c *Cell) MarshalText() ([]byte, error) {
return hexutil.Bytes(c[:]).MarshalText()
}
// Blob represents a 4844 data blob.
type Blob [131072]byte
@ -189,3 +207,75 @@ func CalcBlobHashV1(hasher hash.Hash, commit *Commitment) (vh [32]byte) {
func IsValidVersionedHash(h []byte) bool {
return len(h) == 32 && h[0] == 0x01
}
// VerifyCells verifies a batch of proofs corresponding to the cells and blob commitments.
//
// For this function, it is sufficient to only provide some of the cells.
//
// The `cellIndices` specify which of the 128 cells of each blob are given.
// Indices must be given in ascending order.
//
// Note the list of indices is shared among all blobs, i.e. for a given list of indices
// [1, 2, 13], the cells slice must contain cells [1, 2, 13] of each blob.
// Thus, `len(cells)` must be a multiple of `len(cellIndices)`.
//
// One proof must be given for each cell. As such, `len(proofs)` must equal `len(cells)`.
func VerifyCells(cells []Cell, commitments []Commitment, proofs []Proof, cellIndices []uint64) error {
// commitments/proofs/cells validation
switch {
case len(commitments) == 0:
return errors.New("no commitments")
case len(proofs)%len(commitments) != 0:
return errors.New("len(proofs) must be a multiple of len(commitments)")
case len(cells) != len(proofs):
return errors.New("mismatched len(cellProofs) and len(cells)")
}
if err := validateCellIndices(cells, cellIndices); err != nil {
return err
}
if len(cells)/len(cellIndices) != len(commitments) {
return errors.New("invalid number of cells for blob count")
}
if useCKZG.Load() {
return ckzgVerifyCells(cells, commitments, proofs, cellIndices)
}
return gokzgVerifyCells(cells, commitments, proofs, cellIndices)
}
// ComputeCells computes the cells from the given blobs.
func ComputeCells(blobs []Blob) ([]Cell, error) {
if useCKZG.Load() {
return ckzgComputeCells(blobs)
}
return gokzgComputeCells(blobs)
}
// RecoverBlobs recovers blobs from the given cells and cell indices.
// In order to successfully recover, at least DataPerBlob (64) cells must be provided.
//
// For the layout of cells and cellIndices, please see [VerifyCells].
func RecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
if err := validateCellIndices(cells, cellIndices); err != nil {
return nil, err
}
if useCKZG.Load() {
return ckzgRecoverBlobs(cells, cellIndices)
}
return gokzgRecoverBlobs(cells, cellIndices)
}
func validateCellIndices(cells []Cell, cellIndices []uint64) error {
switch {
case len(cellIndices) == 0:
return errors.New("no cellIndices given")
case len(cellIndices) > len(cells):
return errors.New("less cells than cellIndices")
case len(cellIndices) > CellsPerBlob:
return errors.New("too many cellIndices")
case len(cells)%len(cellIndices) != 0:
return errors.New("len(cells) must be a multiple of len(cellIndices)")
}
// The library checks the canonical ordering of indices, so we don't have to do it here.
return nil
}

View file

@ -190,3 +190,92 @@ func ckzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProofs
}
return nil
}
// ckzgVerifyCells verifies that the cell data corresponds to the provided commitments.
func ckzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
ckzgIniter.Do(ckzgInit)
var (
proofs = make([]ckzg4844.Bytes48, len(cellProofs))
commits = make([]ckzg4844.Bytes48, 0, len(cellProofs))
indices = make([]uint64, 0, len(cellProofs))
kzgcells = make([]ckzg4844.Cell, 0, len(cellProofs))
)
for i := range cellProofs {
proofs[i] = (ckzg4844.Bytes48)(cellProofs[i])
kzgcells = append(kzgcells, (ckzg4844.Cell)(cells[i]))
}
if len(cellProofs)%len(commitments) != 0 {
return errors.New("wrong cell proofs and commitments length")
}
cellCounts := len(cellProofs) / len(commitments)
for _, commitment := range commitments {
for j := 0; j < cellCounts; j++ {
commits = append(commits, (ckzg4844.Bytes48)(commitment))
}
}
for j := 0; j < len(commitments); j++ {
indices = append(indices, cellIndices...)
}
valid, err := ckzg4844.VerifyCellKZGProofBatch(commits, indices, kzgcells, proofs)
if err != nil {
return err
}
if !valid {
return errors.New("invalid proof")
}
return nil
}
// ckzgComputeCells computes cells from blobs.
func ckzgComputeCells(blobs []Blob) ([]Cell, error) {
ckzgIniter.Do(ckzgInit)
cells := make([]Cell, 0, ckzg4844.CellsPerExtBlob*len(blobs))
for i := range blobs {
cellsI, err := ckzg4844.ComputeCells((*ckzg4844.Blob)(&blobs[i]))
if err != nil {
return []Cell{}, err
}
for _, c := range cellsI {
cells = append(cells, Cell(c))
}
}
return cells, nil
}
// ckzgRecoverBlobs recovers blobs from cells and cell indices.
func ckzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
ckzgIniter.Do(ckzgInit)
if len(cellIndices) == 0 || len(cells)%len(cellIndices) != 0 {
return []Blob{}, errors.New("cells with wrong length")
}
blobCount := len(cells) / len(cellIndices)
blobs := make([]Blob, 0, blobCount)
offset := 0
for range blobCount {
kzgcells := make([]ckzg4844.Cell, 0, len(cellIndices))
for _, cell := range cells[offset : offset+len(cellIndices)] {
kzgcells = append(kzgcells, ckzg4844.Cell(cell))
}
extCells, err := ckzg4844.RecoverCells(cellIndices, kzgcells)
if err != nil {
return []Blob{}, err
}
var blob Blob
for i, cell := range extCells[:DataPerBlob] {
copy(blob[i*len(cell):], cell[:])
}
blobs = append(blobs, blob)
offset = offset + len(cellIndices)
}
return blobs, nil
}

View file

@ -73,3 +73,15 @@ func ckzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, proof []Pr
func ckzgComputeCellProofs(blob *Blob) ([]Proof, error) {
panic("unsupported platform")
}
func ckzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
panic("unsupported platform")
}
func ckzgComputeCells(blobs []Blob) ([]Cell, error) {
panic("unsupported platform")
}
func ckzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
panic("unsupported platform")
}

View file

@ -148,3 +148,85 @@ func gokzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProof
}
return context.VerifyCellKZGProofBatch(commits, cellIndices, cells[:], proofs)
}
// gokzgVerifyCells verifies that the cell data corresponds to the provided commitment.
func gokzgVerifyCells(cells []Cell, commitments []Commitment, cellProofs []Proof, cellIndices []uint64) error {
gokzgIniter.Do(gokzgInit)
var (
proofs = make([]gokzg4844.KZGProof, len(cellProofs))
commits = make([]gokzg4844.KZGCommitment, 0, len(cellProofs))
indices = make([]uint64, 0, len(cellProofs))
kzgcells = make([]*gokzg4844.Cell, 0, len(cellProofs))
)
// Copy over the cell proofs and cells
for i := range cellProofs {
proofs[i] = gokzg4844.KZGProof(cellProofs[i])
gc := gokzg4844.Cell(cells[i])
kzgcells = append(kzgcells, &gc)
}
cellCounts := len(cellProofs) / len(commitments)
// Blow up the commitments to be the same length as the proofs
for _, commitment := range commitments {
for j := 0; j < cellCounts; j++ {
commits = append(commits, gokzg4844.KZGCommitment(commitment))
}
}
for j := 0; j < len(commitments); j++ {
indices = append(indices, cellIndices...)
}
return context.VerifyCellKZGProofBatch(commits, indices, kzgcells, proofs)
}
// gokzgComputeCells computes cells from blobs.
func gokzgComputeCells(blobs []Blob) ([]Cell, error) {
gokzgIniter.Do(gokzgInit)
cells := make([]Cell, 0, gokzg4844.CellsPerExtBlob*len(blobs))
for i := range blobs {
cellsI, err := context.ComputeCells((*gokzg4844.Blob)(&blobs[i]), 2)
if err != nil {
return []Cell{}, err
}
for _, c := range cellsI {
if c != nil {
cells = append(cells, Cell(*c))
}
}
}
return cells, nil
}
// gokzgRecoverBlobs recovers blobs from cells and cell indices.
func gokzgRecoverBlobs(cells []Cell, cellIndices []uint64) ([]Blob, error) {
gokzgIniter.Do(gokzgInit)
blobCount := len(cells) / len(cellIndices)
blobs := make([]Blob, 0, blobCount)
offset := 0
for range blobCount {
kzgcells := make([]*gokzg4844.Cell, 0, len(cellIndices))
for _, cell := range cells[offset : offset+len(cellIndices)] {
gc := gokzg4844.Cell(cell)
kzgcells = append(kzgcells, &gc)
}
extCells, err := context.RecoverCells(cellIndices, kzgcells, 2)
if err != nil {
return []Blob{}, err
}
var blob Blob
for i, cell := range extCells[:DataPerBlob] {
copy(blob[i*len(cell):], cell[:])
}
blobs = append(blobs, blob)
offset = offset + len(cellIndices)
}
return blobs, nil
}

View file

@ -18,6 +18,8 @@ package kzg4844
import (
"crypto/rand"
mrand "math/rand"
"slices"
"testing"
"github.com/consensys/gnark-crypto/ecc/bls12-381/fr"
@ -45,14 +47,20 @@ func randBlob() *Blob {
return &blob
}
func TestCKZGWithPoint(t *testing.T) { testKZGWithPoint(t, true) }
func TestGoKZGWithPoint(t *testing.T) { testKZGWithPoint(t, false) }
func testKZGWithPoint(t *testing.T, ckzg bool) {
func switchBackend(t testing.TB, ckzg bool) (switchBack func()) {
t.Helper()
if ckzg && !ckzgAvailable {
t.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
prev := useCKZG.Load()
useCKZG.Store(ckzg)
return func() { useCKZG.Store(prev) }
}
func TestCKZGWithPoint(t *testing.T) { testKZGWithPoint(t, true) }
func TestGoKZGWithPoint(t *testing.T) { testKZGWithPoint(t, false) }
func testKZGWithPoint(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
blob := randBlob()
@ -73,11 +81,7 @@ func testKZGWithPoint(t *testing.T, ckzg bool) {
func TestCKZGWithBlob(t *testing.T) { testKZGWithBlob(t, true) }
func TestGoKZGWithBlob(t *testing.T) { testKZGWithBlob(t, false) }
func testKZGWithBlob(t *testing.T, ckzg bool) {
if ckzg && !ckzgAvailable {
t.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(t, ckzg)()
blob := randBlob()
@ -97,11 +101,7 @@ func testKZGWithBlob(t *testing.T, ckzg bool) {
func BenchmarkCKZGBlobToCommitment(b *testing.B) { benchmarkBlobToCommitment(b, true) }
func BenchmarkGoKZGBlobToCommitment(b *testing.B) { benchmarkBlobToCommitment(b, false) }
func benchmarkBlobToCommitment(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
blob := randBlob()
@ -113,11 +113,7 @@ func benchmarkBlobToCommitment(b *testing.B, ckzg bool) {
func BenchmarkCKZGComputeProof(b *testing.B) { benchmarkComputeProof(b, true) }
func BenchmarkGoKZGComputeProof(b *testing.B) { benchmarkComputeProof(b, false) }
func benchmarkComputeProof(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@ -132,11 +128,7 @@ func benchmarkComputeProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGVerifyProof(b *testing.B) { benchmarkVerifyProof(b, true) }
func BenchmarkGoKZGVerifyProof(b *testing.B) { benchmarkVerifyProof(b, false) }
func benchmarkVerifyProof(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@ -153,11 +145,7 @@ func benchmarkVerifyProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGComputeBlobProof(b *testing.B) { benchmarkComputeBlobProof(b, true) }
func BenchmarkGoKZGComputeBlobProof(b *testing.B) { benchmarkComputeBlobProof(b, false) }
func benchmarkComputeBlobProof(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@ -172,11 +160,7 @@ func benchmarkComputeBlobProof(b *testing.B, ckzg bool) {
func BenchmarkCKZGVerifyBlobProof(b *testing.B) { benchmarkVerifyBlobProof(b, true) }
func BenchmarkGoKZGVerifyBlobProof(b *testing.B) { benchmarkVerifyBlobProof(b, false) }
func benchmarkVerifyBlobProof(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
var (
blob = randBlob()
@ -192,11 +176,7 @@ func benchmarkVerifyBlobProof(b *testing.B, ckzg bool) {
func TestCKZGCells(t *testing.T) { testKZGCells(t, true) }
func TestGoKZGCells(t *testing.T) { testKZGCells(t, false) }
func testKZGCells(t *testing.T, ckzg bool) {
if ckzg && !ckzgAvailable {
t.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(t, ckzg)()
blob1 := randBlob()
blob2 := randBlob()
@ -236,11 +216,7 @@ func BenchmarkGOKZGComputeCellProofs(b *testing.B) { benchmarkComputeCellProofs(
func BenchmarkCKZGComputeCellProofs(b *testing.B) { benchmarkComputeCellProofs(b, true) }
func benchmarkComputeCellProofs(b *testing.B, ckzg bool) {
if ckzg && !ckzgAvailable {
b.Skip("CKZG unavailable in this test build")
}
defer func(old bool) { useCKZG.Store(old) }(useCKZG.Load())
useCKZG.Store(ckzg)
defer switchBackend(b, ckzg)()
blob := randBlob()
_, _ = ComputeCellProofs(blob) // for kzg initialization
@ -253,3 +229,208 @@ func benchmarkComputeCellProofs(b *testing.B, ckzg bool) {
}
}
}
// randCellIndices picks n random unique indices from [0, CellsPerBlob) in sorted order.
func randCellIndices(rng *mrand.Rand, n int) []uint64 {
perm := rng.Perm(CellsPerBlob)
indices := make([]uint64, n)
for i := 0; i < n; i++ {
indices[i] = uint64(perm[i])
}
slices.Sort(indices)
return indices
}
// randBlobAndProofs generates random blobs and precomputes their cells, proofs, and commitments.
type randBlobAndProofs struct {
blobs []Blob
commitments []Commitment
cells []Cell // flat: blobs[i] cells at [i*CellsPerBlob : (i+1)*CellsPerBlob]
proofs []Proof
}
func newBlobs(t *testing.T, blobCount int) *randBlobAndProofs {
d := &randBlobAndProofs{
blobs: make([]Blob, blobCount),
commitments: make([]Commitment, blobCount),
}
for i := range blobCount {
d.blobs[i] = *randBlob()
commitment, err := BlobToCommitment(&d.blobs[i])
if err != nil {
t.Fatalf("failed to compute commitment: %v", err)
}
d.commitments[i] = commitment
proofs, err := ComputeCellProofs(&d.blobs[i])
if err != nil {
t.Fatalf("failed to compute cell proofs: %v", err)
}
d.proofs = append(d.proofs, proofs...)
}
cells, err := ComputeCells(d.blobs)
if err != nil {
t.Fatalf("failed to compute cells: %v", err)
}
d.cells = cells
return d
}
func TestCKZGVerifyPartialCells(t *testing.T) { testVerifyPartialCells(t, true) }
func TestGoKZGVerifyPartialCells(t *testing.T) { testVerifyPartialCells(t, false) }
func testVerifyPartialCells(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
const (
iterations = 50
blobCount = 3
cellsCount = 8
)
// Precompute blobs once, vary only cell indices per iteration
d := newBlobs(t, blobCount)
for iter := range iterations {
rng := mrand.New(mrand.NewSource(int64(iter)))
indices := randCellIndices(rng, cellsCount)
var partialCells []Cell
var partialProofs []Proof
for i := range blobCount {
for _, idx := range indices {
partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
}
}
if err := VerifyCells(partialCells, d.commitments, partialProofs, indices); err != nil {
t.Fatalf("iter %d: failed to verify partial cells: %v", iter, err)
}
}
}
func TestCKZGVerifyCellsWithCorruptedCells(t *testing.T) {
testVerifyCellsWithCorruptedCells(t, true)
}
func TestGoKZGVerifyCellsWithCorruptedCells(t *testing.T) {
testVerifyCellsWithCorruptedCells(t, false)
}
func testVerifyCellsWithCorruptedCells(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
const blobCount = 3
d := newBlobs(t, blobCount)
indices := []uint64{0, 15, 63, 64, 95, 100, 120, 127}
var partialCells []Cell
var partialProofs []Proof
for i := range blobCount {
for _, idx := range indices {
partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
}
}
// Corrupt the first cell
corruptedCells := make([]Cell, len(partialCells))
copy(corruptedCells, partialCells)
corruptedCells[0][0] ^= 0xff
if err := VerifyCells(corruptedCells, d.commitments, partialProofs, indices); err == nil {
t.Fatal("expected verification failure with corrupted cell")
}
}
func TestCKZGVerifyCellsWithCorruptedProofs(t *testing.T) {
testVerifyCellsWithCorruptedProofs(t, true)
}
func TestGoKZGVerifyCellsWithCorruptedProofs(t *testing.T) {
testVerifyCellsWithCorruptedProofs(t, false)
}
func testVerifyCellsWithCorruptedProofs(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
const blobCount = 3
d := newBlobs(t, blobCount)
indices := []uint64{0, 15, 63, 64, 95, 100, 120, 127}
var partialCells []Cell
var partialProofs []Proof
for i := range blobCount {
for _, idx := range indices {
partialCells = append(partialCells, d.cells[i*CellsPerBlob+int(idx)])
partialProofs = append(partialProofs, d.proofs[i*CellProofsPerBlob+int(idx)])
}
}
// Swap first and last proof
wrongProofs := make([]Proof, len(partialProofs))
copy(wrongProofs, partialProofs)
wrongProofs[0], wrongProofs[len(wrongProofs)-1] = wrongProofs[len(wrongProofs)-1], wrongProofs[0]
if err := VerifyCells(partialCells, d.commitments, wrongProofs, indices); err == nil {
t.Fatal("expected verification failure with swapped proofs")
}
}
func TestCKZGRecoverBlob(t *testing.T) { testRecoverBlob(t, true) }
func TestGoKZGRecoverBlob(t *testing.T) { testRecoverBlob(t, false) }
func testRecoverBlob(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
// Precompute blobs once, vary only cell indices per iteration
d := newBlobs(t, 3)
for iter := range 50 {
rng := mrand.New(mrand.NewSource(int64(iter)))
numCells := DataPerBlob + rng.Intn(CellsPerBlob-DataPerBlob+1)
indices := randCellIndices(rng, numCells)
var partialCells []Cell
for bi := range 3 {
for _, idx := range indices {
partialCells = append(partialCells, d.cells[bi*CellsPerBlob+int(idx)])
}
}
recovered, err := RecoverBlobs(partialCells, indices)
if err != nil {
t.Fatalf("iter %d: failed to recover blob with %d cells: %v", iter, numCells, err)
}
if err := VerifyCellProofs(recovered, d.commitments, d.proofs); err != nil {
t.Fatalf("iter %d: recovered blobs failed verification: %v", iter, err)
}
for i := range d.blobs {
if recovered[i] != d.blobs[i] {
t.Fatalf("iter %d: recovered blob %d does not match original", iter, i)
}
}
}
}
func TestCKZGRecoverBlobWithInsufficientCells(t *testing.T) {
testRecoverBlobWithInsufficientCells(t, true)
}
func TestGoKZGRecoverBlobWithInsufficientCells(t *testing.T) {
testRecoverBlobWithInsufficientCells(t, false)
}
func testRecoverBlobWithInsufficientCells(t *testing.T, ckzg bool) {
defer switchBackend(t, ckzg)()
const blobCount = 3
d := newBlobs(t, blobCount)
// Use DataPerBlob-1 cells (one short of minimum required)
indices := make([]uint64, DataPerBlob-1)
for i := range indices {
indices[i] = uint64(i)
}
var partialCells []Cell
for bi := range blobCount {
for _, idx := range indices {
partialCells = append(partialCells, d.cells[bi*CellsPerBlob+int(idx)])
}
}
if _, err := RecoverBlobs(partialCells, indices); err == nil {
t.Fatalf("expected error with only %d cells, got none", len(indices))
}
}