aere-contracts/test/interop/helpers.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

179 lines
6.4 KiB
JavaScript

// -----------------------------------------------------------------------------
// 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,
};