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