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

144 lines
6.7 KiB
JavaScript

const { expect } = require("chai");
const { ethers } = require("hardhat");
const fs = require("fs");
const path = require("path");
// ---------------------------------------------------------------------------
// AereDaFraudProofVerifier — on-chain adjudication of the aere-da crate's
// bad-encoding fraud proofs and availability samples.
//
// Every fixture in test/fixtures/da/ was emitted by the Rust crate itself
// (aerenew/da, `cargo run --bin aere-da-export-fixtures`) using the SAME code
// path its own tests exercise: DataSquareCommitment::commit, try_prove_bad_row
// / try_prove_bad_col, and sample(). Each fixture also carries `rustVerdict` =
// the crate's own verify_bad_encoding / verify_sample result. This suite is a
// DIFFERENTIAL test: the Solidity verifier's answer must equal BOTH the
// expected outcome AND the Rust crate's answer, for identical bytes.
//
// SHA-256 hasher: the on-chain verifier recomputes the NMT with the EVM sha256
// precompile (0x02), so the fixtures commit to the crate's SHA-256 hasher.
// ---------------------------------------------------------------------------
const FIX_DIR = path.join(__dirname, "fixtures", "da");
const load = (name) => JSON.parse(fs.readFileSync(path.join(FIX_DIR, name + ".json"), "utf8"));
// Fixture JSON -> ethers struct tuples (ordered to match the Solidity structs).
const nodeT = (nd) => [nd.min, nd.max, nd.hash];
const proofT = (pr) => [pr.index, pr.siblings.map(nodeT)];
const shareT = (sh) => [sh.pos, sh.ns, sh.data, proofT(sh.proof)];
const badT = (p) => [p.axis, p.index, nodeT(p.root), proofT(p.outerProof), p.basis.map(shareT), shareT(p.disputed)];
const sampleT = (s) => [s.row, s.col, s.ns, s.data, proofT(s.rowProof), nodeT(s.rowRoot), proofT(s.outerProof)];
describe("AereDaFraudProofVerifier", function () {
let verifier;
before(async function () {
const F = await ethers.getContractFactory("AereDaFraudProofVerifier");
verifier = await F.deploy();
await verifier.waitForDeployment();
});
// Sanity: the fixtures directory must actually exist and be populated by the
// Rust exporter (never silently skip — that would fake a green run).
it("fixture directory is populated by the Rust crate", function () {
const files = fs.readdirSync(FIX_DIR).filter((f) => f.endsWith(".json"));
expect(files.length).to.be.greaterThanOrEqual(7);
for (const f of ["bad_row", "bad_col", "forged_basis", "spurious_wellencoded", "wrong_dataroot", "sample_ok", "sample_tampered"]) {
expect(files).to.include(f + ".json");
}
});
describe("bad-encoding fraud proofs (differential vs Rust)", function () {
const cases = [
{ name: "bad_row" },
{ name: "bad_col" },
{ name: "forged_basis" },
{ name: "spurious_wellencoded" },
{ name: "wrong_dataroot" },
];
for (const c of cases) {
it(`${c.name}: Solidity verdict == expectConvict == rustVerdict`, async function () {
const fx = load(c.name);
// The fixture's own expected outcome and the crate's verdict must agree
// (guards against a stale/miswired fixture).
expect(fx.expectConvict).to.equal(fx.rustVerdict);
const got = await verifier.verifyBadEncoding(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof));
expect(got).to.equal(fx.expectConvict, fx.description);
expect(got).to.equal(fx.rustVerdict, "Solidity must match the Rust crate bit-for-bit");
});
}
});
describe("challenge() state-changing path", function () {
it("CONVICTS a badly-encoded row: emits BadBlockConvicted", async function () {
const fx = load("bad_row");
await expect(verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof)))
.to.emit(verifier, "BadBlockConvicted")
.withArgs(fx.dataRoot.hash, fx.proof.axis, fx.proof.index, fx.proof.disputed.pos);
});
it("CONVICTS a badly-encoded column: emits BadBlockConvicted", async function () {
const fx = load("bad_col");
await expect(verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof)))
.to.emit(verifier, "BadBlockConvicted")
.withArgs(fx.dataRoot.hash, fx.proof.axis, fx.proof.index, fx.proof.disputed.pos);
});
it("REJECTS a spurious challenge on a well-encoded block: reverts", async function () {
const fx = load("spurious_wellencoded");
await expect(
verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
});
it("REJECTS a doctored-basis proof: reverts", async function () {
const fx = load("forged_basis");
await expect(
verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
});
it("REJECTS a proof against the wrong data root: reverts", async function () {
const fx = load("wrong_dataroot");
await expect(
verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
});
});
describe("availability samples (NMT inclusion against the data root)", function () {
it("sample_ok: honest sample verifies (matches Rust)", async function () {
const fx = load("sample_ok");
expect(fx.expectAvailable).to.equal(fx.rustVerdict);
const got = await verifier.verifySample(nodeT(fx.dataRoot), fx.n, sampleT(fx.sample));
expect(got).to.equal(true);
});
it("sample_tampered: flipped byte fails inclusion (matches Rust)", async function () {
const fx = load("sample_tampered");
expect(fx.expectAvailable).to.equal(fx.rustVerdict);
const got = await verifier.verifySample(nodeT(fx.dataRoot), fx.n, sampleT(fx.sample));
expect(got).to.equal(false);
});
});
describe("robustness: local tamper must not fool the verifier", function () {
it("flipping a disputed byte on the spurious proof still does not falsely convict a good root", async function () {
// Take the spurious (well-encoded) fixture and corrupt ONLY the disputed
// share's data in the calldata (not re-committed). Its inclusion proof no
// longer matches the honest root, so the verifier must still REJECT: you
// cannot manufacture a conviction against an honest block by lying about a
// cell's value.
const fx = load("spurious_wellencoded");
const p = JSON.parse(JSON.stringify(fx.proof));
// flip first byte of disputed data
const b = ethers.getBytes(p.disputed.data);
b[0] ^= 0xff;
p.disputed.data = ethers.hexlify(b);
const got = await verifier.verifyBadEncoding(nodeT(fx.dataRoot), fx.k, fx.n, badT(p));
expect(got).to.equal(false);
});
});
});