ox
Version:
Ethereum Standard Library
221 lines (207 loc) • 7.29 kB
text/typescript
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { trustedSetup as fastSetup } from '@paulmillr/trusted-setups/fast-peerdas.js'
import { KZG } from 'micro-eth-signer/advanced/kzg.js'
import { Bytes, Hex, Kzg } from 'ox'
import { describe, expect, test } from 'vp/test'
import { blobData, kzg as peerdasKzg } from '../../../test/kzg.js'
import * as Blobs from '../Blobs.js'
describe('from', () => {
const blobToKzgCommitmentCases = JSON.parse(
readFileSync(
resolve(__dirname, '../../../test/kzg/blob-to-kzg-commitment.json'),
'utf8',
),
)
let kzg: Kzg.Kzg
test('defineKzg', () => {
const k = new KZG(fastSetup)
const toHex = (b: Bytes.Bytes) => Bytes.toHex(b)
const fromHex = (s: string) => Hex.toBytes(s as `0x${string}`)
kzg = Kzg.from({
blobToKzgCommitment(blob) {
return fromHex(k.blobToKzgCommitment(toHex(blob)))
},
computeCells(blob) {
return k.computeCells(toHex(blob)).map(fromHex)
},
computeCellsAndKzgProofs(blob) {
const [cells, proofs] = k.computeCellsAndProofs(toHex(blob))
return { cells: cells.map(fromHex), proofs: proofs.map(fromHex) }
},
recoverCellsAndKzgProofs(indices, cells) {
const [recoveredCells, proofs] = k.recoverCellsAndProofs(
[...indices],
cells.map(toHex),
)
return {
cells: recoveredCells.map(fromHex),
proofs: proofs.map(fromHex),
}
},
verifyCellKzgProofBatch(commitments, indices, cells, proofs) {
return k.verifyCellKzgProofBatch(
commitments.map(toHex),
[...indices],
cells.map(toHex),
proofs.map(toHex),
)
},
})
expect(kzg).toMatchInlineSnapshot(`
{
"blobToKzgCommitment": [Function],
"computeCells": [Function],
"computeCellsAndKzgProofs": [Function],
"recoverCellsAndKzgProofs": [Function],
"verifyCellKzgProofBatch": [Function],
}
`)
})
describe('blobToKzgCommitment', () => {
for (const data of blobToKzgCommitmentCases) {
test(data.name, () => {
if (data.output === null)
expect(() =>
Uint8Array.from(
kzg.blobToKzgCommitment(Hex.toBytes(data.input.blob)),
),
).toThrowError()
else
expect(
Uint8Array.from(
kzg.blobToKzgCommitment(Hex.toBytes(data.input.blob)),
),
).toEqual(Hex.toBytes(data.output))
})
}
})
})
test('exports', () => {
expect(Object.keys(Kzg)).toMatchInlineSnapshot(`
[
"versionedHashVersion",
"from",
]
`)
})
describe('peerdas (EIP-7594)', () => {
// Single blob for cell-level round-trip tests.
const blob = Blobs.from(Hex.fromString(blobData), { as: 'Bytes' }).slice(
0,
1,
)[0]!
// KZG cell-proof computations are CPU-heavy and can exceed the default
// 20s testTimeout on busy CI runners; give each peerdas test 60s headroom.
test('computeCellsAndKzgProofs returns 128 cells + 128 proofs', () => {
const { cells, proofs } = peerdasKzg.computeCellsAndKzgProofs(blob)
expect(cells.length).toBe(128)
expect(proofs.length).toBe(128)
for (const cell of cells) expect(cell.length).toBe(2048)
for (const proof of proofs) expect(proof.length).toBe(48)
}, 60_000)
test('computeCells matches the cells from computeCellsAndKzgProofs', () => {
const cellsOnly = peerdasKzg.computeCells(blob)
const { cells } = peerdasKzg.computeCellsAndKzgProofs(blob)
expect(cellsOnly.length).toBe(cells.length)
for (let i = 0; i < cells.length; i++)
expect(cellsOnly[i]).toEqual(cells[i])
}, 60_000)
test('verifyCellKzgProofBatch round-trips', () => {
const commitment = peerdasKzg.blobToKzgCommitment(blob)
const { cells, proofs } = peerdasKzg.computeCellsAndKzgProofs(blob)
const indices = cells.map((_, i) => i)
const commitments = cells.map(() => commitment)
expect(
peerdasKzg.verifyCellKzgProofBatch(commitments, indices, cells, proofs),
).toBe(true)
}, 60_000)
test('verifyCellKzgProofBatch detects a corrupted cell', () => {
const commitment = peerdasKzg.blobToKzgCommitment(blob)
const { cells, proofs } = peerdasKzg.computeCellsAndKzgProofs(blob)
const tampered = cells.map((c, i) => {
if (i !== 7) return c
const copy = Uint8Array.from(c)
copy[0] = copy[0]! ^ 0x01
return copy
})
const indices = cells.map((_, i) => i)
const commitments = cells.map(() => commitment)
expect(
peerdasKzg.verifyCellKzgProofBatch(
commitments,
indices,
tampered,
proofs,
),
).toBe(false)
}, 60_000)
test('recoverCellsAndKzgProofs reconstructs the full set from half the cells', () => {
const { cells, proofs } = peerdasKzg.computeCellsAndKzgProofs(blob)
// Erase every other cell (keep 64 of 128).
const keptIndices: number[] = []
const keptCells: Bytes.Bytes[] = []
for (let i = 0; i < cells.length; i += 2) {
keptIndices.push(i)
keptCells.push(cells[i]!)
}
const recovered = peerdasKzg.recoverCellsAndKzgProofs(
keptIndices,
keptCells,
)
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])
}
}, 60_000)
})
test('from: preserves `this` binding for method-style implementations', () => {
// Stateful implementation whose methods rely on `this` -- a destructured
// wrapper would lose the binding and throw.
class StatefulKzg {
suffix: Uint8Array
constructor(suffix: Uint8Array) {
this.suffix = suffix
}
blobToKzgCommitment(blob: Uint8Array): Uint8Array {
return new Uint8Array([...blob.slice(0, 1), ...this.suffix])
}
computeCells(blob: Uint8Array): readonly Uint8Array[] {
return [new Uint8Array([...blob.slice(0, 1), ...this.suffix])]
}
computeCellsAndKzgProofs(blob: Uint8Array): {
cells: readonly Uint8Array[]
proofs: readonly Uint8Array[]
} {
const cell = new Uint8Array([...blob.slice(0, 1), ...this.suffix])
return { cells: [cell], proofs: [cell] }
}
recoverCellsAndKzgProofs(
_indices: readonly number[],
cells: readonly Uint8Array[],
): {
cells: readonly Uint8Array[]
proofs: readonly Uint8Array[]
} {
return {
cells: cells.map((c) => new Uint8Array([...c, ...this.suffix])),
proofs: cells.map((c) => new Uint8Array([...c, ...this.suffix])),
}
}
verifyCellKzgProofBatch(): boolean {
return this.suffix.length > 0
}
}
const stateful = new StatefulKzg(new Uint8Array([0xab, 0xcd]))
const kzg = Kzg.from(stateful)
expect(() => kzg.blobToKzgCommitment(new Uint8Array([0x11]))).not.toThrow()
expect(kzg.blobToKzgCommitment(new Uint8Array([0x11]))).toEqual(
new Uint8Array([0x11, 0xab, 0xcd]),
)
expect(kzg.computeCells(new Uint8Array([0x11]))).toEqual([
new Uint8Array([0x11, 0xab, 0xcd]),
])
expect(kzg.verifyCellKzgProofBatch([], [], [], [])).toBe(true)
})