ox
Version:
Ethereum Standard Library
277 lines (258 loc) • 7.8 kB
text/typescript
import { BlobCells, Blobs, Bytes, Hex } from 'ox'
import { describe, expect, test } from 'vp/test'
import { blobData, kzg } from '../../../test/kzg.js'
// KZG cell-proof computations are CPU-heavy and can exceed the default
// 20s testTimeout on busy CI runners; give every test that touches the
// KZG backend (fromBlob / verify / recover / toDataColumns) 60s headroom.
const timeout = 60_000
const [blobHex] = Blobs.from(Hex.fromString(blobData))
const [blobBytes] = Blobs.from(Bytes.fromString(blobData))
describe('fromBlob', () => {
test(
'default (Hex in → Hex out)',
() => {
const { cells, proofs } = BlobCells.fromBlob(blobHex!, { kzg })
expect(cells.length).toBe(128)
expect(proofs.length).toBe(128)
for (const cell of cells) {
expect(typeof cell).toBe('string')
expect(Hex.size(cell as Hex.Hex)).toBe(2048)
}
for (const proof of proofs) {
expect(typeof proof).toBe('string')
expect(Hex.size(proof as Hex.Hex)).toBe(48)
}
},
timeout,
)
test(
'Bytes in → Bytes out',
() => {
const { cells, proofs } = BlobCells.fromBlob(blobBytes!, { kzg })
expect(cells.length).toBe(128)
expect(proofs.length).toBe(128)
for (const cell of cells) {
expect(cell).toBeInstanceOf(Uint8Array)
expect((cell as Bytes.Bytes).length).toBe(2048)
}
for (const proof of proofs) {
expect(proof).toBeInstanceOf(Uint8Array)
expect((proof as Bytes.Bytes).length).toBe(48)
}
},
timeout,
)
test(
'as: "Bytes" overrides Hex input',
() => {
const { cells, proofs } = BlobCells.fromBlob(blobHex!, {
kzg,
as: 'Bytes',
})
expect(cells[0]).toBeInstanceOf(Uint8Array)
expect(proofs[0]).toBeInstanceOf(Uint8Array)
},
timeout,
)
test(
'as: "Hex" overrides Bytes input',
() => {
const { cells, proofs } = BlobCells.fromBlob(blobBytes!, {
kzg,
as: 'Hex',
})
expect(typeof cells[0]).toBe('string')
expect(typeof proofs[0]).toBe('string')
},
timeout,
)
test(
'Hex and Bytes outputs are equivalent',
() => {
const fromHex = BlobCells.fromBlob(blobHex!, { kzg })
const fromBytes = BlobCells.fromBlob(blobBytes!, { kzg })
for (let i = 0; i < fromHex.cells.length; i++) {
expect(Hex.toBytes(fromHex.cells[i] as Hex.Hex)).toEqual(
fromBytes.cells[i],
)
expect(Hex.toBytes(fromHex.proofs[i] as Hex.Hex)).toEqual(
fromBytes.proofs[i],
)
}
},
timeout,
)
})
describe('verify', () => {
test(
'returns true for a well-formed batch',
() => {
const [commitment] = Blobs.toCommitments([blobBytes!], { kzg })
const { cells, proofs } = BlobCells.fromBlob(blobBytes!, { kzg })
expect(
BlobCells.verify({
kzg,
commitments: cells.map(() => commitment!),
cellIndices: cells.map((_, i) => i),
cells,
proofs,
}),
).toBe(true)
},
timeout,
)
test(
'returns false when a cell is tampered',
() => {
const [commitment] = Blobs.toCommitments([blobBytes!], { kzg })
const { cells, proofs } = BlobCells.fromBlob(blobBytes!, { kzg })
const tampered = cells.map((c, i) => {
if (i !== 3) return c
const copy = Uint8Array.from(c as Bytes.Bytes)
copy[0] = copy[0]! ^ 0x01
return copy
})
expect(
BlobCells.verify({
kzg,
commitments: cells.map(() => commitment!),
cellIndices: cells.map((_, i) => i),
cells: tampered,
proofs,
}),
).toBe(false)
},
timeout,
)
test(
'accepts Hex cells/proofs/commitments',
() => {
const [commitment] = Blobs.toCommitments([blobBytes!], { kzg, as: 'Hex' })
const { cells, proofs } = BlobCells.fromBlob(blobHex!, { kzg })
expect(
BlobCells.verify({
kzg,
commitments: cells.map(() => commitment!),
cellIndices: cells.map((_, i) => i),
cells,
proofs,
}),
).toBe(true)
},
timeout,
)
})
describe('recover', () => {
test(
'reconstructs the full set from 64 cells (Bytes)',
() => {
const { cells, proofs } = BlobCells.fromBlob(blobBytes!, { kzg })
const keptIndices: number[] = []
const keptCells: Bytes.Bytes[] = []
for (let i = 0; i < cells.length; i += 2) {
keptIndices.push(i)
keptCells.push(cells[i] as Bytes.Bytes)
}
const recovered = BlobCells.recover(keptIndices, keptCells, { kzg })
expect(recovered.cells.length).toBe(128)
expect(recovered.proofs.length).toBe(128)
for (let i = 0; i < cells.length; i++) {
expect(recovered.cells[i]).toEqual(cells[i])
expect(recovered.proofs[i]).toEqual(proofs[i])
}
},
timeout,
)
test(
'reconstructs the full set from 64 cells (Hex)',
() => {
const { cells } = BlobCells.fromBlob(blobHex!, { kzg })
const keptIndices: number[] = []
const keptCells: Hex.Hex[] = []
for (let i = 0; i < cells.length; i += 2) {
keptIndices.push(i)
keptCells.push(cells[i] as Hex.Hex)
}
const recovered = BlobCells.recover(keptIndices, keptCells, { kzg })
expect(typeof recovered.cells[0]).toBe('string')
expect(typeof recovered.proofs[0]).toBe('string')
for (let i = 0; i < cells.length; i++)
expect(recovered.cells[i]).toEqual(cells[i])
},
timeout,
)
test('throws when fewer than 64 cells are provided', () => {
expect(() =>
BlobCells.recover(
[0, 1, 2],
[new Uint8Array(2048), new Uint8Array(2048), new Uint8Array(2048)],
{ kzg },
),
).toThrow(BlobCells.InsufficientCellsError)
})
test('throws when cellIndices and cells lengths differ', () => {
expect(() =>
BlobCells.recover([0, 1], [new Uint8Array(2048)], { kzg }),
).toThrow(BlobCells.MismatchedLengthsError)
})
})
describe('toDataColumns', () => {
test(
'returns 128 columns, one per column index',
() => {
const blobs = Blobs.from(Hex.fromString(blobData))
const columns = BlobCells.toDataColumns(blobs, { kzg })
expect(columns.length).toBe(128)
columns.forEach((col, i) => {
expect(col.index).toBe(i)
expect(col.cells.length).toBe(blobs.length)
expect(col.proofs.length).toBe(blobs.length)
expect(col.commitments.length).toBe(blobs.length)
})
},
timeout,
)
test(
'each column verifies via BlobCells.verify',
() => {
const blobs = Blobs.from(Bytes.fromString(blobData)).slice(0, 1)
const columns = BlobCells.toDataColumns(blobs, { kzg })
// Sample one column for the verify smoke test (full sweep ~30s).
const col = columns[7]!
expect(
BlobCells.verify({
kzg,
commitments: col.commitments,
cellIndices: col.cells.map(() => col.index),
cells: col.cells,
proofs: col.proofs,
}),
).toBe(true)
},
timeout,
)
test(
'respects `as: "Bytes"`',
() => {
const blobs = Blobs.from(Hex.fromString(blobData))
const columns = BlobCells.toDataColumns(blobs, { kzg, as: 'Bytes' })
expect(columns[0]!.cells[0]).toBeInstanceOf(Uint8Array)
},
timeout,
)
})
test('exports', () => {
expect(Object.keys(BlobCells)).toMatchInlineSnapshot(`
[
"fieldElementsPerCell",
"bytesPerCell",
"cellsPerExtBlob",
"fromBlob",
"verify",
"recover",
"toDataColumns",
"InsufficientCellsError",
"MismatchedLengthsError",
]
`)
})