micro-eth-signer
Version:
Minimal library for Ethereum transactions, addresses and smart contracts
580 lines (578 loc) • 26.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KZG = void 0;
// prettier-ignore
const fft_1 = require("@noble/curves/abstract/fft");
const utils_1 = require("@noble/curves/abstract/utils");
const bls12_381_1 = require("@noble/curves/bls12-381");
const sha2_1 = require("@noble/hashes/sha2");
const utils_2 = require("@noble/hashes/utils");
const utils_ts_1 = require("./utils.js");
/*
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 } = bls12_381_1.bls12_381.fields;
const G1 = bls12_381_1.bls12_381.G1.ProjectivePoint;
const G2 = bls12_381_1.bls12_381.G2.ProjectivePoint;
const BLOB_REGEX = /.{1,64}/g; // TODO: is this valid?
function parseScalar(s) {
if (typeof s === 'string') {
s = (0, utils_ts_1.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) {
return (0, utils_ts_1.add0x)((0, utils_2.bytesToHex)((0, utils_1.numberToBytesBE)(n, Fr.BYTES)));
}
function pairingVerify(a1, a2, b1, b2) {
// 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 = bls12_381_1.bls12_381.pairingBatch(pairs, true);
return Fp12.eql(f, Fp12.ONE);
}
function chunks(arr, len) {
if (len <= 0)
throw new Error('chunks: chunkSize must be > 0');
const res = [];
for (let i = 0; i < arr.length; i += len)
res.push(arr.slice(i, i + len));
return res;
}
function chunkBytes(u8a, len) {
if (len <= 0)
throw new Error('chunkBytes: chunk size must be > 0');
const res = [];
for (let i = 0; i < u8a.length; i += len)
res.push(u8a.subarray(i, i + len));
return res;
}
function strideExtend(src, stride, outLen) {
const dst = new Array(outLen).fill(Fr.ZERO);
for (let i = 0, pos = 0; i < src.length && pos < outLen; i++, pos += stride)
dst[pos] = src[i];
return dst;
}
// 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 = (0, fft_1.bitReversalPermutation)(Array.from({ length: 128 }, (_, j) => j));
const Cell = {
encode(fields) {
if (fields.length !== FE_PER_CELL)
throw new Error(`Cell.encode: Expected ${FE_PER_CELL} field elements`);
return (0, utils_ts_1.add0x)((0, utils_2.bytesToHex)((0, utils_2.concatBytes)(...fields.map(Fr.toBytes))));
},
decode(hex) {
const bytes = (0, utils_2.hexToBytes)((0, utils_ts_1.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);
*/
class KZG {
constructor(setup) {
// Should they be configurable?
this.FIAT_SHAMIR_PROTOCOL_DOMAIN = (0, utils_2.utf8ToBytes)('FSBLOBVERIFY_V1_');
this.RANDOM_CHALLENGE_KZG_BATCH_DOMAIN = (0, utils_2.utf8ToBytes)('RCKZGBATCH___V1_');
// PeerDAS (https://eips.ethereum.org/EIPS/eip-7594)
this.Fk20Precomputes = () => {
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 = 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(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;
};
this.Fk20Proof = (poly) => {
const precomputes = this.Fk20Precomputes(); // 128x64
if (poly.length !== FE_PER_BLOB)
throw new Error('Fk20Proof: wrong poly');
const coeffs = 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(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) => (0, utils_ts_1.add0x)(p.toHex(true)));
};
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 = (0, fft_1.bitReversalPermutation)(G1L);
this.ROOTS_CACHE = (0, fft_1.rootsOfUnity)(Fr, 7n);
this.ROOTS_OF_UNITY_BRP = this.ROOTS_CACHE.brp((0, fft_1.log2)(this.POLY_NUM));
this.fftFr = (0, fft_1.FFT)(this.ROOTS_CACHE, Fr);
this.fftG1 = (0, fft_1.FFT)((0, fft_1.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 = (0, fft_1.poly)(Fr, this.ROOTS_CACHE);
this.POLY_NUM_BYTES = (0, utils_1.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
parseG1(p) {
if (typeof p === 'string')
p = G1.fromHex((0, utils_ts_1.strip0x)(p));
return p;
}
parseG1Unchecked(p) {
if (typeof p !== 'string')
throw new Error('string expected');
const [x, y] = p.split(' ').map(utils_ts_1.hexToNumber);
return G1.fromAffine({ x, y });
}
parseG2(p) {
return G2.fromHex((0, utils_ts_1.strip0x)(p));
}
parseG2Unchecked(p) {
const xy = (0, utils_ts_1.strip0x)(p)
.split(' ')
.map((c) => c.split(',').map((c) => BigInt('0x' + c)));
const x = bls12_381_1.bls12_381.fields.Fp2.fromBigTuple(xy[0]);
const y = bls12_381_1.bls12_381.fields.Fp2.fromBigTuple(xy[1]);
return G2.fromAffine({ x, y });
}
parseBlob(blob) {
if (typeof blob === 'string') {
blob = (0, utils_ts_1.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);
}
invSafe(inverses) {
inverses = Fr.invertBatch(inverses);
for (const i of inverses)
if (i === undefined)
throw new Error('invSafe: division by zero');
return inverses;
}
G1msm(points, scalars) {
// 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);
}
computeChallenge(blob, commitment) {
const h = sha2_1.sha256
.create()
.update(this.FIAT_SHAMIR_PROTOCOL_DOMAIN)
.update((0, utils_1.numberToBytesBE)(0, 8))
.update(this.POLY_NUM_BYTES);
for (const b of blob)
h.update((0, utils_1.numberToBytesBE)(b, Fr.BYTES));
h.update(commitment.toRawBytes(true));
const res = Fr.create((0, utils_1.bytesToNumberBE)(h.digest()));
h.destroy();
return res;
}
evalPoly(poly, x) {
return this.polyFr.lagrange.eval(poly, x, true);
}
// Basic
computeProof(blob, z) {
z = parseScalar(z);
blob = this.parseBlob(blob);
const y = this.evalPoly(blob, z);
const batch = [];
let rootOfUnityPos;
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 = (0, utils_ts_1.add0x)(this.G1msm(this.G1LB, poly).toHex(true));
return [proof, formatScalar(y)];
}
verifyProof(commitment, z, y, proof) {
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;
}
}
getRPowers(r, n) {
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
verifyProofBatch(commitments, zs, ys, proofs) {
const n = commitments.length;
const p = proofs.map((i) => this.parseG1(i));
const h = sha2_1.sha256
.create()
.update(this.RANDOM_CHALLENGE_KZG_BATCH_DOMAIN)
.update(this.POLY_NUM_BYTES)
.update((0, utils_1.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((0, utils_1.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) {
return (0, utils_ts_1.add0x)(this.G1msm(this.G1LB, this.parseBlob(blob)).toHex(true));
}
computeBlobProof(blob, commitment) {
blob = this.parseBlob(blob);
const challenge = this.computeChallenge(blob, this.parseG1(commitment));
const [proof, _] = this.computeProof(blob, challenge);
return proof;
}
verifyBlobProof(blob, commitment, proof) {
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, commitments, proofs) {
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;
}
}
getCells(blob) {
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) {
return this.getCells(blob).cells;
}
computeCellsAndProofs(blob) {
const { cells, polyMShort } = this.getCells(blob);
const proofs = this.Fk20Proof(polyMShort);
return [cells, proofs];
}
recoverCell(indices, recoveredCellsNulls) {
const PEERDAS_RECOVERY_SHIFT = 7n;
const PEERDAS_RECOVERY_SHIFT_INV = Fr.inv(7n);
const cellsBRP = new Array(FE_PER_EXT_BLOB);
for (let i = 0; i < FE_PER_EXT_BLOB; i++)
cellsBRP[(0, fft_1.reverseBits)(i, (0, fft_1.log2)(FE_PER_EXT_BLOB))] = recoveredCellsNulls[i];
const cellsBRPFull = (0, fft_1.bitReversalPermutation)(recoveredCellsNulls);
const missingIndicesBRP = [];
const indicesSet = new Set(indices);
for (let i = 0; i < CELLS_PER_EXT_BLOB; i++)
if (!indicesSet.has(i))
missingIndicesBRP.push((0, fft_1.reverseBits)(i, (0, fft_1.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((0, fft_1.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, cells) {
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();
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 = 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;
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;
}
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, indices, cells, proofs) {
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();
const uniqueCommitments = [];
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 = sha2_1.sha256.create();
h.update((0, utils_2.utf8ToBytes)('RCKZGCBATCH__V1_'));
h.update((0, utils_1.numberToBytesBE)(FE_PER_CELL, 8)); // uint64
h.update((0, utils_1.numberToBytesBE)(uniqueCommitments.length, 8)); // uint64
h.update((0, utils_1.numberToBytesBE)(cells.length, 8)); // uint64
for (const c of uniqueCommitments)
h.update((0, utils_2.hexToBytes)((0, utils_ts_1.strip0x)(c)));
for (const idx of commitmentIndicesMap)
h.update((0, utils_1.numberToBytesBE)(idx, 8)); // uint64
for (const idx of indices)
h.update((0, utils_1.numberToBytesBE)(idx, 8)); // uint64
for (const c of cells)
h.update((0, utils_2.hexToBytes)((0, utils_ts_1.strip0x)(c)));
for (const p of proofs)
h.update((0, utils_2.hexToBytes)((0, utils_ts_1.strip0x)(p)));
const r = Fr.create((0, utils_1.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 = 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();
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((0, fft_1.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]);
}
}
exports.KZG = KZG;
//# sourceMappingURL=kzg.js.map