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.
144 lines
6.7 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|