// ----------------------------------------------------------------------------- // Test helpers for the Ethereum-interop light client. // // These build a SYNTHETIC (but structurally spec-correct) Altair light-client // update using REAL BLS12-381 cryptography from @noble/curves. The BLS signatures // are genuine; hash-to-curve, aggregation and the pairing check are verified on // chain against AERE's LIVE EIP-2537 precompiles (Prague). The SSZ roots and Merkle // branches are computed here with the SAME algorithm the Solidity contract uses, so // a passing test means the two independent implementations agree byte-for-byte. // // HONEST LABEL: this is not a captured Ethereum-mainnet update. The DST used is // noble's default RFC-9380 "_NUL_" tag (mainnet sync uses "_POP_"); the genesis // validators root, fork version and headers are chosen by the test. It exercises the // exact on-chain verification pipeline; it does not assert mainnet byte-compatibility. // ----------------------------------------------------------------------------- const { bls12_381: bls } = require("@noble/curves/bls12-381"); const { sha256 } = require("@noble/hashes/sha256"); const DST = Buffer.from("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_", "ascii"); const COMMITTEE_SIZE = 512; const ZERO32 = Buffer.alloc(32); // ---- byte utils ------------------------------------------------------------- const hex = (b) => "0x" + Buffer.from(b).toString("hex"); const cat = (...a) => Buffer.concat(a.map((x) => Buffer.from(x))); function shaPair(a, b) { return Buffer.from(sha256(cat(a, b))); } // ---- EIP-2537 point encodings ---------------------------------------------- const PAD16 = Buffer.alloc(16); function f48(v) { return Buffer.from(v.toString(16).padStart(96, "0"), "hex"); } function encG1(P) { P = P.toAffine(); return cat(PAD16, f48(P.x), PAD16, f48(P.y)); // 128 bytes } function encG2(P) { P = P.toAffine(); return cat(PAD16, f48(P.x.c0), PAD16, f48(P.x.c1), PAD16, f48(P.y.c0), PAD16, f48(P.y.c1)); // 256 } // ---- SSZ --------------------------------------------------------------------- function uint64Chunk(v) { const b = Buffer.alloc(32); let x = BigInt(v); for (let i = 0; i < 8; i++) { b[i] = Number(x & 0xffn); x >>= 8n; } return b; } function beaconHeaderRoot(h) { const l01 = shaPair(uint64Chunk(h.slot), uint64Chunk(h.proposerIndex)); const l23 = shaPair(h.parentRoot, h.stateRoot); const l45 = shaPair(h.bodyRoot, ZERO32); const l67 = shaPair(ZERO32, ZERO32); const a = shaPair(l01, l23); const b = shaPair(l45, l67); return shaPair(a, b); } function merkleize(leaves) { let n = leaves.length; let layer = leaves.slice(); while (n > 1) { const next = []; for (let i = 0; i < n; i += 2) next.push(shaPair(layer[i], layer[i + 1])); layer = next; n = next.length; } return layer[0]; } function pubkeyLeaf(compressed48) { const c0 = compressed48.slice(0, 32); const c1 = cat(compressed48.slice(32, 48), Buffer.alloc(16)); return shaPair(c0, c1); } function syncCommitteeRoot(compressedPubkeys, aggCompressed) { const leaves = compressedPubkeys.map(pubkeyLeaf); const pubkeysRoot = merkleize(leaves); return shaPair(pubkeysRoot, pubkeyLeaf(aggCompressed)); } function computeDomain(domainType4, forkVersion4, gvr) { const forkVersionChunk = cat(forkVersion4, Buffer.alloc(28)); const forkDataRoot = shaPair(forkVersionChunk, gvr); return cat(domainType4, forkDataRoot.slice(0, 28)); // 32 bytes } // ---- sparse merkle tree with gindex overrides (for state-root branches) ----- // Builds a depth-`depth` binary tree; `assign` maps generalized-index -> 32-byte // value (overriding computed hashes). Returns { root, branch(gindex) }. function buildTree(depth, assign) { const size = 1 << (depth + 1); const val = new Array(size).fill(null); const leafStart = 1 << depth; for (let g = leafStart; g < size; g++) val[g] = ZERO32; for (const [g, v] of Object.entries(assign)) val[Number(g)] = Buffer.from(v); for (let g = leafStart - 1; g >= 1; g--) { if (assign[g] !== undefined) continue; // keep override val[g] = shaPair(val[2 * g], val[2 * g + 1]); } function branch(gindex) { const out = []; let g = gindex; while (g > 1) { out.push(val[g ^ 1]); // sibling g = g >> 1; } return out; } return { root: val[1], val, branch }; } // ---- committee generation ---------------------------------------------------- // Deterministic committee of 512 real BLS keypairs from a seed. function makeCommittee(seedText) { const sks = []; const pointsG1 = []; const compressed = []; const uncompressed = []; const R = bls.fields.Fr.ORDER; let seed = Buffer.from(sha256(Buffer.from(seedText, "ascii"))); for (let i = 0; i < COMMITTEE_SIZE; i++) { seed = Buffer.from(sha256(seed)); // reduce to a valid scalar in [1, r): sk = (seed mod (r-1)) + 1 const scalar = (BigInt("0x" + seed.toString("hex")) % (R - 1n)) + 1n; const sk = Buffer.from(scalar.toString(16).padStart(64, "0"), "hex"); const pk = bls.getPublicKey(sk); // compressed 48 const P = bls.G1.ProjectivePoint.fromHex(Buffer.from(pk).toString("hex")); sks.push(sk); pointsG1.push(P); compressed.push(Buffer.from(pk)); uncompressed.push(encG1(P)); } // aggregate pubkey of the whole committee let agg = bls.G1.ProjectivePoint.ZERO; for (const P of pointsG1) agg = agg.add(P); const aggCompressed = Buffer.from(agg.toRawBytes(true)); // 48 const aggUncompressed = encG1(agg); const root = syncCommitteeRoot(compressed, aggCompressed); return { sks, pointsG1, compressed, uncompressed, aggCompressed, aggUncompressed, root }; } // Sign a 32-byte message with a participant subset; returns aggregate sig (G2 point) // and the participation bitfield (64-byte SSZ Bitvector[512]). function signSubset(committee, message, participantIndices) { const bits = Buffer.alloc(COMMITTEE_SIZE / 8); let aggSig = bls.G2.ProjectivePoint.ZERO; for (const i of participantIndices) { bits[i >> 3] |= 1 << (i & 7); const sig = bls.sign(message, committee.sks[i]); // compressed G2 const S = bls.G2.ProjectivePoint.fromHex(Buffer.from(sig).toString("hex")); aggSig = aggSig.add(S); } return { bits, aggSigUncompressed: encG2(aggSig) }; } module.exports = { bls, DST, COMMITTEE_SIZE, ZERO32, hex, cat, shaPair, encG1, encG2, uint64Chunk, beaconHeaderRoot, merkleize, pubkeyLeaf, syncCommitteeRoot, computeDomain, buildTree, makeCommittee, signSubset, };