aere-contracts/test/xmssVerifier.test.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

142 lines
6.2 KiB
JavaScript

const { expect } = require("chai");
const { ethers } = require("hardhat");
const kat = require("./fixtures/xmss-sha2_10_256-kat.json");
// ---------------------------------------------------------------------------
// Independent in-repo oracle: RFC 8391 XMSS-SHA2_10_256 verify path in pure JS
// (ethers sha256), reimplemented from the spec, to cross-check the Solidity
// verifier against the OFFICIAL reference vector without trusting either side.
// ---------------------------------------------------------------------------
const N = 32, W = 16, LOGW = 4, LEN1 = 64, LEN2 = 3, LEN = 67, H = 10;
const cat = (...a) => ethers.concat(a);
const sha = (b) => ethers.sha256(b);
const tob = (x, l) => ethers.toBeHex(BigInt(x), l);
const xor = (a, b) => {
const A = ethers.getBytes(a), B = ethers.getBytes(b), O = new Uint8Array(A.length);
for (let i = 0; i < A.length; i++) O[i] = A[i] ^ B[i];
return ethers.hexlify(O);
};
const addr = (typ, a4, a5, a6, km) =>
tob((BigInt(typ) << 128n) | (BigInt(a4) << 96n) | (BigInt(a5) << 64n) | (BigInt(a6) << 32n) | BigInt(km), 32);
const prf = (seed, a) => sha(cat(tob(3, 32), seed, a));
const F = (seed, typ, a4, a5, a6, x) => {
const key = prf(seed, addr(typ, a4, a5, a6, 0));
const mask = prf(seed, addr(typ, a4, a5, a6, 1));
return sha(cat(tob(0, 32), key, xor(x, mask)));
};
const Hh = (seed, typ, a4, a5, a6, l, r) => {
const key = prf(seed, addr(typ, a4, a5, a6, 0));
const m0 = prf(seed, addr(typ, a4, a5, a6, 1));
const m1 = prf(seed, addr(typ, a4, a5, a6, 2));
return sha(cat(tob(1, 32), key, xor(l, m0), xor(r, m1)));
};
function chainLengths(mHex) {
const m = ethers.getBytes(mHex), d = new Array(LEN);
let csum = 0;
for (let i = 0; i < 32; i++) { const b = m[i], hi = b >> 4, lo = b & 15; d[2 * i] = hi; d[2 * i + 1] = lo; csum += (15 - hi) + (15 - lo); }
const c = csum << 4;
d[64] = (c >> 12) & 15; d[65] = (c >> 8) & 15; d[66] = (c >> 4) & 15;
return d;
}
function wotsPkFromSig(seed, idxLeaf, mhash, wots) {
const lens = chainLengths(mhash), pk = [];
for (let i = 0; i < LEN; i++) { let x = wots[i]; for (let s = lens[i]; s < W - 1; s++) x = F(seed, 0, idxLeaf, i, s, x); pk.push(x); }
return pk;
}
function lTree(seed, idxLeaf, pk) {
let nodes = pk.slice(), l = LEN, height = 0;
while (l > 1) {
const parent = l >> 1;
for (let i = 0; i < parent; i++) nodes[i] = Hh(seed, 1, idxLeaf, height, i, nodes[2 * i], nodes[2 * i + 1]);
if (l & 1) { nodes[parent] = nodes[l - 1]; l = parent + 1; } else { l = parent; }
height++;
}
return nodes[0];
}
function computeRoot(seed, leaf, leafIdx, auth) {
let left, right;
if (leafIdx & 1) { left = auth[0]; right = leaf; } else { left = leaf; right = auth[0]; }
let li = leafIdx;
for (let i = 0; i < H - 1; i++) {
li >>= 1;
const node = Hh(seed, 2, 0, i, li, left, right);
if (li & 1) { left = auth[i + 1]; right = node; } else { left = node; right = auth[i + 1]; }
}
li >>= 1;
return Hh(seed, 2, 0, H - 1, li, left, right);
}
function jsVerify(pubRoot, pubSeed, idx, R, msg, wots, auth) {
const mhash = sha(cat(tob(2, 32), R, pubRoot, tob(idx, 32), msg));
const idxLeaf = idx & ((1 << H) - 1);
const wpk = wotsPkFromSig(pubSeed, idxLeaf, mhash, wots);
const leaf = lTree(pubSeed, idxLeaf, wpk);
return computeRoot(pubSeed, leaf, idxLeaf, auth);
}
describe("AereXmssVerifier (RFC 8391 XMSS-SHA2_10_256)", function () {
async function deploy() {
const V = await ethers.getContractFactory("AereXmssVerifier");
const v = await V.deploy();
await v.waitForDeployment();
return v;
}
const V = () => [kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth];
it("independent JS oracle recomputes the official reference root", function () {
const root = jsVerify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth);
expect(root).to.equal(kat.pubRoot);
});
it("accepts the OFFICIAL reference KAT signature (verify -> true)", async function () {
const v = await deploy();
expect(await v.verify(...V())).to.equal(true);
});
it("chainLengths matches the WOTS+ checksum spec for the KAT message hash", async function () {
const v = await deploy();
// mhash as computed in verify()
const mhash = ethers.sha256(ethers.concat([ethers.toBeHex(2n, 32), kat.R, kat.pubRoot, ethers.toBeHex(BigInt(kat.idx), 32), kat.msg]));
const onchain = (await v.chainLengths(mhash)).map((x) => Number(x));
expect(onchain).to.deep.equal(chainLengths(mhash));
});
it("rejects a tampered WOTS+ signature (verify -> false)", async function () {
const v = await deploy();
const wots = kat.wots.slice();
wots[0] = xor(wots[0], "0x01" + "00".repeat(31));
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, wots, kat.auth)).to.equal(false);
});
it("rejects a tampered authentication path (verify -> false)", async function () {
const v = await deploy();
const auth = kat.auth.slice();
auth[9] = xor(auth[9], "0x01" + "00".repeat(31));
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, auth)).to.equal(false);
});
it("rejects a tampered message (verify -> false)", async function () {
const v = await deploy();
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx, kat.R, "0x26", kat.wots, kat.auth)).to.equal(false);
});
it("rejects a wrong leaf index (verify -> false)", async function () {
const v = await deploy();
expect(await v.verify(kat.pubRoot, kat.pubSeed, kat.idx + 1, kat.R, kat.msg, kat.wots, kat.auth)).to.equal(false);
});
it("rejects a wrong public root (verify -> false)", async function () {
const v = await deploy();
const badRoot = xor(kat.pubRoot, "0x01" + "00".repeat(31));
expect(await v.verify(badRoot, kat.pubSeed, kat.idx, kat.R, kat.msg, kat.wots, kat.auth)).to.equal(false);
});
it("verifyAndRecord records the result and stays under the EIP-7825 cap", async function () {
const v = await deploy();
const est = await v.verifyAndRecord.estimateGas(...V());
expect(est).to.be.lessThan(16777216n); // 2^24 Fusaka per-tx cap
await (await v.verifyAndRecord(...V())).wait();
expect(await v.lastResult()).to.equal(true);
expect(await v.verifyCount()).to.equal(1n);
});
});