UNPKG

micro-eth-signer

Version:

Minimal library for Ethereum transactions, addresses and smart contracts

609 lines (593 loc) 25.4 kB
// prettier-ignore import { type PolyFn, type Polynomial, bitReversalPermutation, FFT, log2, poly, reverseBits, rootsOfUnity, } from '@noble/curves/abstract/fft'; import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/abstract/utils'; import { bls12_381 as bls } from '@noble/curves/bls12-381'; import { sha256 } from '@noble/hashes/sha2'; import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'; import { add0x, hexToNumber, strip0x } from './utils.ts'; /* KZG for [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844). Docs: - https://github.com/ethereum/c-kzg-4844 - https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/polynomial-commitments.md TODO(high-level): - data converted into blob by prepending 0x00 prefix on each chunk and ends with 0x80 terminator - Unsure how generic is this - There are up to 6 blob per tx - Terminator only added to the last blob - sidecar: {blob, commitment, proof} - Calculate versionedHash from commitment, which is included inside of tx - if 'sidecars' inside of tx enabled: - envelope turns into 'wrapper' - rlp([tx, blobs, commitments, proofs]) - this means there are two eip4844 txs: with sidecars and without */ const { Fr, Fp12 } = bls.fields; const G1 = bls.G1.ProjectivePoint; const G2 = bls.G2.ProjectivePoint; type G1Point = typeof bls.G1.ProjectivePoint.BASE; type G2Point = typeof bls.G2.ProjectivePoint.BASE; type Scalar = string | bigint; type Blob = string | string[] | bigint[]; const BLOB_REGEX = /.{1,64}/g; // TODO: is this valid? function parseScalar(s: Scalar): bigint { if (typeof s === 'string') { s = strip0x(s); if (s.length !== 2 * Fr.BYTES) throw new Error('parseScalar: wrong format'); s = BigInt(`0x${s}`); } if (!Fr.isValid(s)) throw new Error('parseScalar: invalid field element'); return s; } function formatScalar(n: bigint) { return add0x(bytesToHex(numberToBytesBE(n, Fr.BYTES))); } function pairingVerify(a1: G1Point, a2: G2Point, b1: G1Point, b2: G2Point) { // Filter-out points at infinity, because pairingBatch will throw an error const pairs = [ { g1: a1.negate(), g2: a2 }, { g1: b1, g2: b2 }, ].filter(({ g1, g2 }) => !G1.ZERO.equals(g1) && !G2.ZERO.equals(g2)); const f = bls.pairingBatch(pairs, true); return Fp12.eql(f, Fp12.ONE); } function chunks<T>(arr: T[], len: number): T[][] { if (len <= 0) throw new Error('chunks: chunkSize must be > 0'); const res: T[][] = []; for (let i = 0; i < arr.length; i += len) res.push(arr.slice(i, i + len)); return res; } function chunkBytes(u8a: Uint8Array, len: number): Uint8Array[] { if (len <= 0) throw new Error('chunkBytes: chunk size must be > 0'); const res: Uint8Array[] = []; for (let i = 0; i < u8a.length; i += len) res.push(u8a.subarray(i, i + len)); return res; } function strideExtend(src: bigint[], stride: number, outLen: number): bigint[] { const dst = new Array<bigint>(outLen).fill(Fr.ZERO); for (let i = 0, pos = 0; i < src.length && pos < outLen; i++, pos += stride) dst[pos] = src[i]; return dst; } // Official JSON format export type SetupData = { g1_lagrange: string[]; g2_monomial: string[]; g1_monomial?: string[]; // Optional, for PeerDAS only! fk20?: string[]; }; // PEERDAS Constants const FE_PER_EXT_BLOB = 8192; const FE_PER_BLOB = 4096; const FE_PER_CELL = 64; const CELLS_PER_BLOB = FE_PER_BLOB / FE_PER_CELL; // 64 const CELLS_PER_EXT_BLOB = FE_PER_EXT_BLOB / FE_PER_CELL; // 128 const BYTES_PER_CELL = 2048; const CIRCULANT_DOMAIN_SIZE = CELLS_PER_BLOB * 2; // 128 const FK20_STRIDE = FE_PER_EXT_BLOB / CIRCULANT_DOMAIN_SIZE; // RBL = Reverse Bits Limited table const CELL_INDICES_RBL: Readonly<number[]> = bitReversalPermutation( Array.from({ length: 128 }, (_, j) => j) ); const Cell = { encode(fields: bigint[]): string { if (fields.length !== FE_PER_CELL) throw new Error(`Cell.encode: Expected ${FE_PER_CELL} field elements`); return add0x(bytesToHex(concatBytes(...fields.map(Fr.toBytes)))); }, decode(hex: string): bigint[] { const bytes = hexToBytes(strip0x(hex)); if (bytes.length !== BYTES_PER_CELL) throw new Error(`Cell.decode: Expected ${BYTES_PER_CELL} bytes after decoding hex`); const fields = chunkBytes(bytes, Fr.BYTES).map(Fr.fromBytes); for (const f of fields) if (!Fr.isValid(f)) throw new Error('invalid fr'); return fields; }, }; /** * KZG from [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844). * @example * const kzg = new KZG(trustedSetupData); */ export class KZG { private readonly POLY_NUM: number; private readonly G1LB: G1Point[]; // lagrange brp private readonly G2M: G2Point[]; private readonly G1M?: G1Point[]; private readonly ROOTS_OF_UNITY_BRP: bigint[]; private readonly ROOTS_CACHE: ReturnType<typeof rootsOfUnity>; private readonly fftFr: ReturnType<typeof FFT<bigint>>; private readonly fftG1: ReturnType<typeof FFT<G1Point>>; private readonly polyFr: PolyFn<bigint[], bigint>; // Should they be configurable? private readonly FIAT_SHAMIR_PROTOCOL_DOMAIN = utf8ToBytes('FSBLOBVERIFY_V1_'); private readonly RANDOM_CHALLENGE_KZG_BATCH_DOMAIN = utf8ToBytes('RCKZGBATCH___V1_'); private readonly POLY_NUM_BYTES: Uint8Array; // PeerDAS private fk20Columns?: G1Point[][]; constructor(setup: SetupData & { encoding?: 'fast_v1' }) { if (setup == null || typeof setup !== 'object') throw new Error('expected valid setup data'); if (!Array.isArray(setup.g1_lagrange) || !Array.isArray(setup.g2_monomial)) throw new Error('expected valid setup data'); // The slowest part let fastSetup = false; if ('encoding' in setup) { fastSetup = setup.encoding === 'fast_v1'; if (!fastSetup) throw new Error('unknown encoding ' + setup.encoding); } const G1L = setup.g1_lagrange.map(fastSetup ? this.parseG1Unchecked : this.parseG1); this.POLY_NUM = G1L.length; this.G2M = setup.g2_monomial.map(fastSetup ? this.parseG2Unchecked : this.parseG2); this.G1LB = bitReversalPermutation(G1L); this.ROOTS_CACHE = rootsOfUnity(Fr, 7n); this.ROOTS_OF_UNITY_BRP = this.ROOTS_CACHE.brp(log2(this.POLY_NUM)); this.fftFr = FFT(this.ROOTS_CACHE, Fr); this.fftG1 = FFT<G1Point>(rootsOfUnity(Fr, 7n), { add: (a, b) => a.add(b), sub: (a, b) => a.subtract(b), mul: (a, scalar) => a.multiplyUnsafe(scalar), inv: Fr.inv, }); this.polyFr = poly(Fr, this.ROOTS_CACHE); this.POLY_NUM_BYTES = numberToBytesBE(this.POLY_NUM, 8); if (setup.g1_monomial) { this.G1M = setup.g1_monomial.map(fastSetup ? this.parseG1Unchecked : this.parseG1); } if (setup.fk20) { this.fk20Columns = chunks( setup.fk20.map(fastSetup ? this.parseG1Unchecked : this.parseG1), FE_PER_CELL ); } } // Internal private parseG1(p: string | G1Point) { if (typeof p === 'string') p = G1.fromHex(strip0x(p)); return p; } private parseG1Unchecked(p: string) { if (typeof p !== 'string') throw new Error('string expected'); const [x, y] = p.split(' ').map(hexToNumber); return G1.fromAffine({ x, y }); } private parseG2(p: string) { return G2.fromHex(strip0x(p)); } private parseG2Unchecked(p: string) { const xy = strip0x(p) .split(' ') .map((c) => c.split(',').map((c) => BigInt('0x' + c))) as unknown as [bigint, bigint][]; const x = bls.fields.Fp2.fromBigTuple(xy[0]); const y = bls.fields.Fp2.fromBigTuple(xy[1]); return G2.fromAffine({ x, y }); } private parseBlob(blob: Blob) { if (typeof blob === 'string') { blob = strip0x(blob); if (blob.length !== this.POLY_NUM * Fr.BYTES * 2) throw new Error('Wrong blob length'); const m = blob.match(BLOB_REGEX); if (!m) throw new Error('Wrong blob'); blob = m; } return blob.map(parseScalar); } private invSafe(inverses: bigint[]) { inverses = Fr.invertBatch(inverses); for (const i of inverses) if (i === undefined) throw new Error('invSafe: division by zero'); return inverses; } private G1msm(points: G1Point[], scalars: bigint[]) { // Filters zero scalars, non-const time, but improves computeProof up to x93 for empty blobs const _points = []; const _scalars = []; for (let i = 0; i < scalars.length; i++) { const s = scalars[i]; if (Fr.is0(s)) continue; _points.push(points[i]); _scalars.push(s); } return G1.msm(_points, _scalars); } private computeChallenge(blob: bigint[], commitment: G1Point): bigint { const h = sha256 .create() .update(this.FIAT_SHAMIR_PROTOCOL_DOMAIN) .update(numberToBytesBE(0, 8)) .update(this.POLY_NUM_BYTES); for (const b of blob) h.update(numberToBytesBE(b, Fr.BYTES)); h.update(commitment.toRawBytes(true)); const res = Fr.create(bytesToNumberBE(h.digest())); h.destroy(); return res; } private evalPoly(poly: bigint[], x: bigint) { return this.polyFr.lagrange.eval(poly, x, true); } // Basic computeProof(blob: Blob, z: bigint | string): [string, string] { z = parseScalar(z); blob = this.parseBlob(blob); const y = this.evalPoly(blob, z); const batch = []; let rootOfUnityPos: undefined | number; const poly = new Array(this.POLY_NUM).fill(Fr.ZERO); for (let i = 0; i < this.POLY_NUM; i++) { if (Fr.eql(z, this.ROOTS_OF_UNITY_BRP[i])) { rootOfUnityPos = i; batch.push(Fr.ONE); continue; } poly[i] = Fr.sub(blob[i], y); batch.push(Fr.sub(this.ROOTS_OF_UNITY_BRP[i], z)); } const inverses = this.invSafe(batch); for (let i = 0; i < this.POLY_NUM; i++) poly[i] = Fr.mul(poly[i], inverses[i]); if (rootOfUnityPos !== undefined) { poly[rootOfUnityPos] = Fr.ZERO; for (let i = 0; i < this.POLY_NUM; i++) { if (i === rootOfUnityPos) continue; batch[i] = Fr.mul(Fr.sub(z, this.ROOTS_OF_UNITY_BRP[i]), z); } const inverses = this.invSafe(batch); for (let i = 0; i < this.POLY_NUM; i++) { if (i === rootOfUnityPos) continue; poly[rootOfUnityPos] = Fr.add( poly[rootOfUnityPos], Fr.mul(Fr.mul(Fr.sub(blob[i], y), this.ROOTS_OF_UNITY_BRP[i]), inverses[i]) ); } } const proof = add0x(this.G1msm(this.G1LB, poly).toHex(true)); return [proof, formatScalar(y)]; } verifyProof(commitment: string, z: Scalar, y: Scalar, proof: string): boolean { try { z = parseScalar(z); y = parseScalar(y); const g2x = Fr.is0(z) ? G2.ZERO : G2.BASE.multiply(z); const g1y = Fr.is0(y) ? G1.ZERO : G1.BASE.multiply(y); const XminusZ = this.G2M[1].subtract(g2x); const PminusY = this.parseG1(commitment).subtract(g1y); return pairingVerify(PminusY, G2.BASE, this.parseG1(proof), XminusZ); } catch (e) { return false; } } private getRPowers(r: bigint, n: number) { const rPowers = []; if (n !== 0) { rPowers.push(Fr.ONE); for (let i = 1; i < n; i++) rPowers[i] = Fr.mul(rPowers[i - 1], r); } return rPowers; } // There are no test vectors for this private verifyProofBatch(commitments: G1Point[], zs: bigint[], ys: bigint[], proofs: string[]) { const n = commitments.length; const p: G1Point[] = proofs.map((i) => this.parseG1(i)); const h = sha256 .create() .update(this.RANDOM_CHALLENGE_KZG_BATCH_DOMAIN) .update(this.POLY_NUM_BYTES) .update(numberToBytesBE(n, 8)); for (let i = 0; i < n; i++) { h.update(commitments[i].toRawBytes(true)); h.update(Fr.toBytes(zs[i])); h.update(Fr.toBytes(ys[i])); h.update(p[i].toRawBytes(true)); } const r = Fr.create(bytesToNumberBE(h.digest())); h.destroy(); const rPowers = this.getRPowers(r, n); const proofPowers = this.G1msm(p, rPowers); const CminusY = commitments.map((c, i) => c.subtract(Fr.is0(ys[i]) ? G1.ZERO : G1.BASE.multiply(ys[i])) ); const RtimesZ = rPowers.map((p, i) => Fr.mul(p, zs[i])); const rhs = this.G1msm(p.concat(CminusY), RtimesZ.concat(rPowers)); return pairingVerify(proofPowers, this.G2M[1], rhs, G2.BASE); } // Blobs blobToKzgCommitment(blob: Blob): string { return add0x(this.G1msm(this.G1LB, this.parseBlob(blob)).toHex(true)); } computeBlobProof(blob: Blob, commitment: string): string { blob = this.parseBlob(blob); const challenge = this.computeChallenge(blob, this.parseG1(commitment)); const [proof, _] = this.computeProof(blob, challenge); return proof; } verifyBlobProof(blob: Blob, commitment: string, proof: string): boolean { try { blob = this.parseBlob(blob); const c = this.parseG1(commitment); const challenge = this.computeChallenge(blob, c); const y = this.evalPoly(blob, challenge); return this.verifyProof(commitment, challenge, y, proof); } catch (e) { return false; } } verifyBlobProofBatch(blobs: string[], commitments: string[], proofs: string[]): boolean { if (!Array.isArray(blobs) || !Array.isArray(commitments) || !Array.isArray(proofs)) throw new Error('invalid arguments'); if (blobs.length !== commitments.length || blobs.length !== proofs.length) return false; if (blobs.length === 1) return this.verifyBlobProof(blobs[0], commitments[0], proofs[0]); try { const b = blobs.map((i) => this.parseBlob(i)); const c = commitments.map(this.parseG1); const challenges = b.map((b, i) => this.computeChallenge(b, c[i])); const ys = b.map((_, i) => this.evalPoly(b[i], challenges[i])); return this.verifyProofBatch(c, challenges, ys, proofs); } catch (e) { return false; } } // PeerDAS (https://eips.ethereum.org/EIPS/eip-7594) private Fk20Precomputes = (): G1Point[][] => { if (!this.G1M) throw new Error('PeerDAS requires full kzg setup (with G1 monomial)'); if (this.fk20Columns) return this.fk20Columns; // This is very slow and takes 38s on first run! const columns: G1Point[][] = Array.from( { length: CIRCULANT_DOMAIN_SIZE }, () => new Array(FE_PER_CELL) ); const G1Mrev_chunks = chunks(Array.from(this.G1M).reverse(), FE_PER_CELL); const xExt = new Array<G1Point>(CIRCULANT_DOMAIN_SIZE).fill(G1.ZERO); for (let offset = 0; offset < FE_PER_CELL; offset++) { for (let i = 0; i < CELLS_PER_BLOB - 1; i++) xExt[i] = G1Mrev_chunks[i + 1][offset]; // FFT call is 600ms here, while in Rust it's 45ms. Issue is mostly with Point#multiply: // Rust // RUST mul=47951 add=2 sub=3 (microseconds) // JS: mul=597947 add=2613 sub=2653 // It could be optimized by copying optimizations from C: // - GLV endomorphism // - Windowed booth encode multiplication (wnaf-like stuff) const res = this.fftG1.direct(xExt); for (let row = 0; row < CIRCULANT_DOMAIN_SIZE; row++) columns[row][offset] = res[row]; } this.fk20Columns = columns; return columns; }; private Fk20Proof = (poly: Polynomial<bigint>): string[] => { const precomputes = this.Fk20Precomputes(); // 128x64 if (poly.length !== FE_PER_BLOB) throw new Error('Fk20Proof: wrong poly'); const coeffs: bigint[][] = Array.from({ length: CIRCULANT_DOMAIN_SIZE }, () => new Array(FE_PER_CELL).fill(Fr.ZERO) ); for (let i = 0; i < FE_PER_CELL; i++) { const toeplitz = new Array<bigint>(CIRCULANT_DOMAIN_SIZE).fill(Fr.ZERO); toeplitz[0] = poly[FE_PER_BLOB - 1 - i]; for (let j = 0; j < CELLS_PER_EXT_BLOB - CELLS_PER_BLOB - 2; j++) { toeplitz[CELLS_PER_BLOB + 2 + j] = poly[CELLS_PER_EXT_BLOB - i - 1 + j * FE_PER_CELL]; } const res = this.fftFr.direct(toeplitz); for (let j = 0; j < CIRCULANT_DOMAIN_SIZE; j++) coeffs[j][i] = res[j]; } const hExtFFT = []; for (let i = 0; i < CIRCULANT_DOMAIN_SIZE; i++) hExtFFT.push(this.G1msm(precomputes[i], coeffs[i])); const h = this.fftG1.inverse(hExtFFT); for (let i = CELLS_PER_BLOB; i < CIRCULANT_DOMAIN_SIZE; i++) h[i] = G1.ZERO; return this.fftG1.direct(h, false, true).map((p) => add0x(p.toHex(true))); }; private getCells(blob: string) { if (!this.G1M) throw new Error('PeerDAS requires full kzg setup (with G1 monomial)'); // Convert compact poly into extended const blobParsed = this.parseBlob(blob); const polyMShort = this.fftFr.inverse(blobParsed, true); const extendedEvalBRP = this.fftFr.direct( strideExtend(polyMShort, 1, FE_PER_EXT_BLOB), false, true ); const cells = chunks(extendedEvalBRP, FE_PER_CELL).map(Cell.encode); return { cells, polyMShort }; } computeCells(blob: string): string[] { return this.getCells(blob).cells; } computeCellsAndProofs(blob: string): [string[], string[]] { const { cells, polyMShort } = this.getCells(blob); const proofs = this.Fk20Proof(polyMShort); return [cells, proofs]; } private recoverCell(indices: number[], recoveredCellsNulls: (bigint | null)[]) { const PEERDAS_RECOVERY_SHIFT = 7n; const PEERDAS_RECOVERY_SHIFT_INV = Fr.inv(7n); const cellsBRP: (bigint | null)[] = new Array(FE_PER_EXT_BLOB); for (let i = 0; i < FE_PER_EXT_BLOB; i++) cellsBRP[reverseBits(i, log2(FE_PER_EXT_BLOB))] = recoveredCellsNulls[i]; const cellsBRPFull = bitReversalPermutation(recoveredCellsNulls); const missingIndicesBRP: number[] = []; const indicesSet = new Set(indices); for (let i = 0; i < CELLS_PER_EXT_BLOB; i++) if (!indicesSet.has(i)) missingIndicesBRP.push(reverseBits(i, log2(CELLS_PER_EXT_BLOB))); if (missingIndicesBRP.length === 0 || missingIndicesBRP.length >= CELLS_PER_EXT_BLOB) throw new Error('Invalid number of missing cells for vanishing polynomial'); const roots = []; const extRoots = this.ROOTS_CACHE.roots(log2(FE_PER_EXT_BLOB)); for (let i = 0; i < missingIndicesBRP.length; i++) roots.push(extRoots[missingIndicesBRP[i] * FK20_STRIDE]); const shortVanishing = this.polyFr.vanishing(roots); const vanishing = strideExtend(shortVanishing, FE_PER_CELL, FE_PER_EXT_BLOB); const vanishingEval = this.fftFr.direct(vanishing); const extendedEvalZero = []; for (let i = 0; i < FE_PER_EXT_BLOB; i++) { const v = cellsBRPFull[i]; extendedEvalZero.push(v === null ? Fr.ZERO : Fr.mul(v, vanishingEval[i])); } const extendedCoset = this.fftFr.direct( this.polyFr.shift(this.fftFr.inverse(extendedEvalZero), PEERDAS_RECOVERY_SHIFT) ); const vanishingShifted = this.polyFr.shift(vanishing, PEERDAS_RECOVERY_SHIFT); const vanishingCoset = this.fftFr.direct(vanishingShifted); const reconstructedCoset = []; for (let i = 0; i < FE_PER_EXT_BLOB; i++) reconstructedCoset.push(Fr.div(extendedCoset[i], vanishingCoset[i])); return this.fftFr.direct( this.polyFr.shift(this.fftFr.inverse(reconstructedCoset), PEERDAS_RECOVERY_SHIFT_INV), false, true ); } recoverCellsAndProofs(indices: number[], cells: string[]): [string[], string[]] { if (cells.length !== indices.length) throw new Error('Indices and cells array lengths mismatch'); if (indices.length > CELLS_PER_EXT_BLOB) throw new Error(`Too many cells provided (${indices.length} > ${CELLS_PER_EXT_BLOB})`); if (indices.length < CELLS_PER_BLOB) throw new Error( `Not enough cells provided (${indices.length} < ${CELLS_PER_BLOB}) for recovery` ); const uniqueIndices = new Set<number>(); for (const idx of indices) { if (idx >= CELLS_PER_EXT_BLOB || idx < 0) throw new Error(`Invalid cell index found: ${idx}`); if (uniqueIndices.has(idx)) throw new Error(`Duplicate cell index found: ${idx}`); uniqueIndices.add(idx); } const recoveredCellsNulls: (bigint | null)[] = new Array(FE_PER_EXT_BLOB).fill(null); for (let i = 0; i < indices.length; i++) { const idx = indices[i]; const fields = Cell.decode(cells[i]); for (let j = 0; j < FE_PER_CELL; j++) recoveredCellsNulls[idx * FE_PER_CELL + j] = fields[j]; } let recoveredCells: bigint[]; if (indices.length === CELLS_PER_EXT_BLOB) { // TODO: this should not happen. Need to think about better construction that will enforce this // perhaps uniqueIndices.size() == indices.length? if (recoveredCellsNulls.some((f) => f === null)) throw new Error('Internal error: Null found even when all cells provided'); recoveredCells = recoveredCellsNulls as bigint[]; } else { recoveredCells = this.recoverCell(indices, recoveredCellsNulls); } const allCells = chunks(recoveredCells, FE_PER_CELL).map(Cell.encode); const proofs = this.Fk20Proof(this.fftFr.inverse(recoveredCells, true).slice(0, FE_PER_BLOB)); return [allCells, proofs]; } verifyCellKzgProofBatch( commitments: string[], indices: number[], cells: string[], proofs: string[] ): boolean { if (!this.G1M) throw new Error('PeerDAS requires full kzg setup (with G1 monomial)'); if ( commitments.length !== cells.length || indices.length !== cells.length || proofs.length !== cells.length ) { throw new Error('verifyCellKzgProofBatch: input array lengths mismatch'); } if (cells.length === 0) return true; for (const idx of indices) { if (idx >= CELLS_PER_EXT_BLOB) throw new Error('verifyCellKzgProofBatch: invalid cell index: ' + idx); } // Deduplicate commitments (0ms) const uniqueMap = new Map<string, number>(); const uniqueCommitments: string[] = []; const commitmentIndicesMap = []; for (let i = 0; i < commitments.length; i++) { const commitHex = commitments[i]; if (uniqueMap.has(commitHex)) commitmentIndicesMap.push(uniqueMap.get(commitHex)!); else { const newIndex = uniqueCommitments.length; uniqueMap.set(commitHex, newIndex); uniqueCommitments.push(commitHex); commitmentIndicesMap.push(newIndex); } } // Compute challenge r (5ms) const h = sha256.create(); h.update(utf8ToBytes('RCKZGCBATCH__V1_')); h.update(numberToBytesBE(FE_PER_CELL, 8)); // uint64 h.update(numberToBytesBE(uniqueCommitments.length, 8)); // uint64 h.update(numberToBytesBE(cells.length, 8)); // uint64 for (const c of uniqueCommitments) h.update(hexToBytes(strip0x(c))); for (const idx of commitmentIndicesMap) h.update(numberToBytesBE(idx, 8)); // uint64 for (const idx of indices) h.update(numberToBytesBE(idx, 8)); // uint64 for (const c of cells) h.update(hexToBytes(strip0x(c))); for (const p of proofs) h.update(hexToBytes(strip0x(p))); const r = Fr.create(bytesToNumberBE(h.digest())); // Proofs lincomb (175ms) const rPowers = this.getRPowers(r, cells.length); // const proofsG1 = proofs.map((hex) => this.parseG1(hex)); // 120ms const proofLincomb = this.G1msm(proofsG1, rPowers); // 51ms // Weighted sum of commitments (4ms) const uniqueCommitmentsG1 = uniqueCommitments.map(this.parseG1); const weights: bigint[] = new Array(uniqueCommitments.length).fill(Fr.ZERO); for (let i = 0; i < commitmentIndicesMap.length; i++) { const idx = commitmentIndicesMap[i]; weights[idx] = Fr.add(weights[idx], rPowers[i]); } const CAgg = this.G1msm(uniqueCommitmentsG1, weights); // Compute commitment to aggregated interpolation polynomial (47 ms) const columns = Array.from({ length: CELLS_PER_EXT_BLOB }, () => new Array(FE_PER_CELL).fill(Fr.ZERO) ); const usedRows = new Set<number>(); for (let k = 0; k < cells.length; k++) { const row = indices[k]; usedRows.add(row); const weight = rPowers[k]; const cell = Cell.decode(cells[k]); for (let j = 0; j < FE_PER_CELL; j++) columns[row][j] = Fr.add(columns[row][j], Fr.mul(cell[j], weight)); } const ROOTS_EXT = this.ROOTS_CACHE.roots(log2(FE_PER_EXT_BLOB)); const aggInterp = new Array(FE_PER_CELL).fill(Fr.ZERO); for (const i of usedRows) { const idx = (FE_PER_EXT_BLOB - CELL_INDICES_RBL[i]) % FE_PER_EXT_BLOB; const cosetR = ROOTS_EXT[idx]; const shifted = this.polyFr.shift(this.fftFr.inverse(columns[i], true), cosetR); for (let k = 0; k < FE_PER_CELL; k++) aggInterp[k] = Fr.add(aggInterp[k], shifted[k]); } const IAgg = this.G1msm(this.G1M.slice(0, FE_PER_CELL), aggInterp); // Weighted sum of proofs (0ms) const weightedR = []; for (let k = 0; k < proofsG1.length; k++) { const idx = indices[k]; if (idx >= CELLS_PER_EXT_BLOB) throw new Error(`Invalid cell index ${idx}`); const hkPow = (CELL_INDICES_RBL[idx] * FE_PER_CELL) % FE_PER_EXT_BLOB; if (hkPow >= ROOTS_EXT.length) throw new Error(`hkPow out of bounds`); weightedR.push(Fr.mul(rPowers[k], ROOTS_EXT[hkPow])); } const PiAgg = this.G1msm(proofsG1, weightedR); return pairingVerify( CAgg.add(IAgg.negate()).add(PiAgg), // CAgg - IAgg + PiAgg G2.BASE, proofLincomb, this.G2M[FE_PER_CELL] ); } // High-level method // commitmentToVersionedHash(commitment: Uint8Array) { // const VERSION = 1; // Currently only 1 version is supported // // commitment is G1 point in hex? // return concatBytes(new Uint8Array([VERSION]), sha256(commitment)); // } }