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); }); });