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.
179 lines
6.4 KiB
JavaScript
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,
|
|
};
|