// AereStorageProofVerifierV2 — regression tests for CANONICAL RECORD POISONING.
//
// KNOWN AND ALREADY DOCUMENTED (not the bug): AereStorageProofVerifier 0xF9a1A183…9362E proves
// a value against a GIVEN, prover-supplied state root and cannot itself show that root is
// canonical. That is the honest, intended scope of a storage-proof primitive.
//
// THE FINDING (2026-07-17 sweep): V1 wrote EVERY submission — anchored or not — into ONE shared
// mapping keyed only by (blockNumber, account, slot). So a record established through the
// trust-minimized path (AereStateRootAnchor.proveAgainstAnchor, which forces the proof's root to
// equal a root recovered from the BLOCKHASH opcode) can be SILENTLY OVERWRITTEN by anyone
// calling the unbound submitProof() with a proof against a state root of their OWN making for
// the SAME key.
//
// NOT THEORETICAL: the live V1 holds exactly such a record — AereTreasury 0x687933…6119 slot 0
// (owner) == Foundation 0x0243A4…f3C3 at block 8,915,939, against canonical stateRoot
// 0x515fce04…4a92f3 (verified live 2026-07-17, == eth_getBlockByNumber(8915939).stateRoot).
//
// THE FIX: anchored and unbound records live in SEPARATE mappings. Anchored is keyed by
// (blockNumber, account, slot) and writable ONLY by the immutable CANONICAL_ANCHOR; unbound is
// keyed by (SUBMITTER, blockNumber, account, slot) so every submitter has their own namespace.
// getProven() — the V1-shaped read — returns the ANCHORED record only: fail-closed.
//
// Each attack test is PAIRED with a control on the REAL V1 contract showing the poisoning
// SUCCEEDS there. Both sides use the REAL AereStateRootAnchor logic (via MockBlockhashAnchor,
// the existing test subclass that injects a canonical hash so a REAL AERE header outside the
// live 256-block window can be anchored) and the REAL AERE genesis header — the anchor's
// keccak-match + RLP state-root decode is genuine.
//
// The SP1 verifier is MOCKED (MockSp1Verifier: reverts unless the exact (vkey, publicValues,
// proof) triple was pre-registered — mirroring the real gateway). That is the CORRECT model of
// this attack: the attacker's storage proof is CRYPTOGRAPHICALLY VALID — they really did build
// a trie in which the slot holds their value, and the guest honestly proved it. The guest cannot
// know which root is canonical; that is exactly its documented scope. The bug was never a broken
// proof — it was letting an honestly-proven-but-uncanonical record land on a canonical key.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { encodeHeader } = require("./header-rlp");
const genesis = require("./fixtures/aere-genesis-header.json");

const VKEY = "0x" + "ab".repeat(32);
const PROOF = "0x4388a21c99";
const ATTACK_PROOF = "0x4388a21cAA"; // distinct bytes: the attacker's own valid proof

// The REAL live poisoning target (chain 2800, read 2026-07-17).
const TREASURY = "0x687933AE7ea4927867AC227F1b60d476003e6119";
const FOUNDATION = "0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3";
const SLOT0 = ethers.ZeroHash;
const word = (addr) => ethers.zeroPadValue(ethers.getAddress(addr), 32);

function marker(vkey, pv, proof) {
  return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof]));
}

describe("AereStorageProofVerifierV2 — canonical record-poisoning fix (2026-07-17)", function () {
  let mock, v2, v2Anchor, v1, v1Anchor, deployer, attacker, chainId;
  let CANONICAL_ROOT, GENESIS_HEADER;
  const BN = 0n; // the real AERE genesis block

  // abi.encode(chainId, blockNumber, stateRoot, account, slot, value) -> 192 bytes
  function pv({ blockNumber = BN, stateRoot, account = TREASURY, slot = SLOT0, value, cid = chainId }) {
    return ethers.AbiCoder.defaultAbiCoder().encode(
      ["uint256", "uint256", "bytes32", "address", "bytes32", "bytes32"],
      [cid, blockNumber, stateRoot, account, slot, value]
    );
  }
  async function accept(publicValues, proof = PROOF) {
    await mock.setValid(marker(VKEY, publicValues, proof), true);
  }

  beforeEach(async function () {
    [deployer, attacker] = await ethers.getSigners();
    chainId = (await ethers.provider.getNetwork()).chainId;
    GENESIS_HEADER = encodeHeader(genesis);
    CANONICAL_ROOT = genesis.stateRoot;

    mock = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    await mock.waitForDeployment();
    const mockAddr = await mock.getAddress();

    // --- V2 + its anchor. The two are mutually referential, so the verifier is deployed with
    //     the anchor's PREDICTED CREATE address and both directions are asserted afterwards.
    const nonce = await ethers.provider.getTransactionCount(deployer.address);
    const predictedAnchor = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });
    v2 = await (await ethers.getContractFactory("AereStorageProofVerifierV2")).deploy(mockAddr, VKEY, predictedAnchor);
    await v2.waitForDeployment();
    v2Anchor = await (await ethers.getContractFactory("MockBlockhashAnchor")).deploy(await v2.getAddress());
    await v2Anchor.waitForDeployment();
    expect(await v2Anchor.getAddress()).to.equal(predictedAnchor); // prediction held

    // --- V1 + its anchor (the control).
    v1 = await (await ethers.getContractFactory("AereStorageProofVerifier")).deploy(mockAddr, VKEY);
    await v1.waitForDeployment();
    v1Anchor = await (await ethers.getContractFactory("MockBlockhashAnchor")).deploy(await v1.getAddress());
    await v1Anchor.waitForDeployment();

    // Anchor the REAL AERE genesis header on both (its real canonical hash is injected because
    // genesis is far outside the live 256-block BLOCKHASH window; the keccak match + RLP decode
    // that follow are the genuine production logic).
    for (const a of [v2Anchor, v1Anchor]) {
      await a.setBlockHash(BN, genesis.hash);
      await a.anchorRecentHeader(BN, GENESIS_HEADER);
    }
  });

  /* ------------------------- fixture / wiring sanity ------------------------------ */

  describe("wiring + real-data sanity", function () {
    it("the REAL AERE genesis header reconstructs its canonical hash and state root", async function () {
      expect(ethers.keccak256(GENESIS_HEADER)).to.equal(genesis.hash);
      const a = await v2Anchor.getAnchoredRoot(BN);
      expect(a[0]).to.equal(true);
      expect(a[1]).to.equal(CANONICAL_ROOT); // decoded from the real header by the anchor
      expect(a[2]).to.equal(genesis.hash);
    });

    it("the anchor <-> verifier binding is immutable and verified BOTH directions", async function () {
      expect(await v2.CANONICAL_ANCHOR()).to.equal(await v2Anchor.getAddress());
      expect(await v2Anchor.VERIFIER()).to.equal(await v2.getAddress());
      expect(await v2.SP1_VERIFIER()).to.equal(await mock.getAddress());
      expect(await v2.PROGRAM_VKEY()).to.equal(VKEY);
    });

    it("rejects a zero anchor / zero vkey / code-less verifier at construction", async function () {
      const F = await ethers.getContractFactory("AereStorageProofVerifierV2");
      const m = await mock.getAddress();
      await expect(F.deploy(m, VKEY, ethers.ZeroAddress)).to.be.revertedWithCustomError(F, "ZeroAddress");
      await expect(F.deploy(ethers.ZeroAddress, VKEY, deployer.address)).to.be.revertedWithCustomError(F, "ZeroAddress");
      await expect(F.deploy(m, ethers.ZeroHash, deployer.address)).to.be.revertedWithCustomError(F, "ZeroVKey");
      await expect(
        F.deploy("0x000000000000000000000000000000000000dEaD", VKEY, deployer.address)
      ).to.be.revertedWithCustomError(F, "VerifierHasNoCode");
    });
  });

  /* ================================================================================
     THE ATTACK: an unbound proof overwrites a canonically-anchored record.
     ================================================================================ */

  describe("ATTACK — unbound submitProof() poisons a canonically-anchored record", function () {
    // The honest, anchored fact: Treasury slot0 == Foundation, proven against the CANONICAL root.
    const honestPv = () => pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
    // The attacker's fact: same key, but proven against a root of THEIR OWN making, in which the
    // Treasury owner is the attacker. The proof is genuinely valid for that root.
    const ATTACKER_ROOT = "0x" + "de".repeat(32);
    const attackPv = (who) => pv({ stateRoot: ATTACKER_ROOT, value: word(who) });

    async function anchorHonest(anchor) {
      const p = honestPv();
      await accept(p);
      await anchor.proveAgainstAnchor(p, PROOF);
      return p;
    }

    it("PAIRED CONTROL: on V1 the attacker OVERWRITES the anchored record", async function () {
      await anchorHonest(v1Anchor);
      // the honest, anchored truth is on the books
      let rec = await v1.getProven(BN, TREASURY, SLOT0);
      expect(rec[0]).to.equal(true);
      expect(rec[1]).to.equal(CANONICAL_ROOT);
      expect(rec[2]).to.equal(word(FOUNDATION));

      // ...until the attacker submits an unbound proof for the SAME key.
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await v1.connect(attacker).submitProof(ap, ATTACK_PROOF);

      rec = await v1.getProven(BN, TREASURY, SLOT0);
      expect(rec[0]).to.equal(true); // still "exists" — nothing looks wrong
      expect(rec[1]).to.equal(ATTACKER_ROOT); // canonical root REPLACED
      expect(rec[2]).to.equal(word(attacker.address)); // <-- POISONED: attacker is now "the owner"
      expect(rec[2]).to.not.equal(word(FOUNDATION));
    });

    it("V2: the anchored record SURVIVES the identical attack — poisoning is closed", async function () {
      await anchorHonest(v2Anchor);
      let rec = await v2.getProven(BN, TREASURY, SLOT0);
      expect(rec[2]).to.equal(word(FOUNDATION));

      // the attacker's unbound submission SUCCEEDS (the API is not dropped)...
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await expect(v2.connect(attacker).submitProof(ap, ATTACK_PROOF)).to.emit(v2, "SlotProvenUnbound");

      // ...but it lands in the ATTACKER'S OWN namespace and touches nothing canonical.
      rec = await v2.getProven(BN, TREASURY, SLOT0);
      expect(rec[0]).to.equal(true);
      expect(rec[1]).to.equal(CANONICAL_ROOT); // untouched
      expect(rec[2]).to.equal(word(FOUNDATION)); // <-- STILL the honest, anchored truth
      expect(await v2.anchoredValue(BN, TREASURY, SLOT0)).to.equal(word(FOUNDATION));
      expect(await v2.provenanceOf(BN, TREASURY, SLOT0)).to.equal(2); // Provenance.Anchored

      // the attacker's record is readable, clearly non-canonical, and namespaced under them
      const u = await v2.getUnboundProven(attacker.address, BN, TREASURY, SLOT0);
      expect(u[0]).to.equal(true);
      expect(u[1]).to.equal(ATTACKER_ROOT);
      expect(u[2]).to.equal(word(attacker.address));
    });

    it("V2: submitProofWithExpectedRoot from a NON-anchor caller is ALSO non-canonical", async function () {
      // The expected-root check is NOT self-securing: the caller picks the root, so it passes
      // trivially. Provenance must therefore come from WHO calls, not from WHICH function.
      await anchorHonest(v2Anchor);
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await v2.connect(attacker).submitProofWithExpectedRoot(ap, ATTACK_PROOF, ATTACKER_ROOT); // passes!

      // ...and still cannot reach the canonical domain.
      const rec = await v2.getProven(BN, TREASURY, SLOT0);
      expect(rec[1]).to.equal(CANONICAL_ROOT);
      expect(rec[2]).to.equal(word(FOUNDATION));
      const u = await v2.getUnboundProven(attacker.address, BN, TREASURY, SLOT0);
      expect(u[2]).to.equal(word(attacker.address));
    });

    it("PAIRED CONTROL: on V1 submitProofWithExpectedRoot poisons it just as well", async function () {
      await anchorHonest(v1Anchor);
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await v1.connect(attacker).submitProofWithExpectedRoot(ap, ATTACK_PROOF, ATTACKER_ROOT);
      const rec = await v1.getProven(BN, TREASURY, SLOT0);
      expect(rec[2]).to.equal(word(attacker.address)); // POISONED
    });

    it("V2: two different attackers cannot overwrite EACH OTHER either", async function () {
      const a1 = attackPv(attacker.address);
      const a2 = pv({ stateRoot: "0x" + "cc".repeat(32), value: word(deployer.address) });
      await accept(a1, ATTACK_PROOF);
      await accept(a2, PROOF);
      await v2.connect(attacker).submitProof(a1, ATTACK_PROOF);
      await v2.connect(deployer).submitProof(a2, PROOF);

      const u1 = await v2.getUnboundProven(attacker.address, BN, TREASURY, SLOT0);
      const u2 = await v2.getUnboundProven(deployer.address, BN, TREASURY, SLOT0);
      expect(u1[2]).to.equal(word(attacker.address)); // each keeps their own
      expect(u2[2]).to.equal(word(deployer.address));
    });

    it("V2: an unbound record is INVISIBLE to the canonical read when nothing is anchored", async function () {
      // fail-closed: no anchored record => getProven reports absent, even though an unbound
      // record for that key exists. V1 would have reported the unbound record as if canonical.
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await v2.connect(attacker).submitProof(ap, ATTACK_PROOF);

      const fresh = await v2.getProven(12345n, TREASURY, SLOT0);
      expect(fresh[0]).to.equal(false);
      const rec = await v2.getProven(BN, TREASURY, SLOT0); // BN was never anchored in this test
      expect(rec[0]).to.equal(false); // <-- absent, NOT the attacker's value
      expect(await v2.isAnchoredProven(BN, TREASURY, SLOT0)).to.equal(false);
      await expect(v2.anchoredValue(BN, TREASURY, SLOT0)).to.be.revertedWithCustomError(v2, "NotAnchored");
    });

    it("PAIRED CONTROL: on V1 that same unbound record READS AS CANONICAL", async function () {
      const ap = attackPv(attacker.address);
      await accept(ap, ATTACK_PROOF);
      await v1.connect(attacker).submitProof(ap, ATTACK_PROOF);
      const rec = await v1.getProven(BN, TREASURY, SLOT0);
      expect(rec[0]).to.equal(true); // indistinguishable from an anchored record
      expect(rec[2]).to.equal(word(attacker.address));
    });
  });

  /* ---------------- V2 does not over-reject: the anchored path works --------------- */

  describe("V2 does not over-reject — the anchored path still works end to end", function () {
    it("proveAgainstAnchor records a canonical, trust-minimized fact", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await accept(p);
      await expect(v2Anchor.proveAgainstAnchor(p, PROOF)).to.emit(v2, "SlotProvenAnchored");

      const d = await v2.getAnchoredProven(BN, TREASURY, SLOT0);
      expect(d[0]).to.equal(true);
      expect(d[1]).to.equal(CANONICAL_ROOT);
      expect(d[2]).to.equal(word(FOUNDATION));
      expect(d[5]).to.equal(await v2Anchor.getAddress()); // submitter == the anchor
      expect(await v2.provenanceOf(BN, TREASURY, SLOT0)).to.equal(2); // Anchored
    });

    it("the anchor REFUSES a proof whose root is not the canonical one", async function () {
      const bad = pv({ stateRoot: "0x" + "de".repeat(32), value: word(FOUNDATION) });
      await accept(bad);
      await expect(v2Anchor.proveAgainstAnchor(bad, PROOF)).to.be.revertedWithCustomError(
        v2Anchor,
        "RootNotCanonical"
      );
      expect((await v2.getProven(BN, TREASURY, SLOT0))[0]).to.equal(false);
    });

    it("the anchor REFUSES a proof for a block it has not anchored", async function () {
      const p = pv({ blockNumber: 999n, stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await accept(p);
      await expect(v2Anchor.proveAgainstAnchor(p, PROOF)).to.be.revertedWithCustomError(v2Anchor, "NotAnchored");
    });

    it("re-anchoring the same canonical fact is idempotent", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await accept(p);
      await v2Anchor.proveAgainstAnchor(p, PROOF);
      await v2Anchor.proveAgainstAnchor(p, PROOF); // no conflict: same canonical root
      expect((await v2.getProven(BN, TREASURY, SLOT0))[2]).to.equal(word(FOUNDATION));
    });
  });

  /* ---------------- proof + chain integrity still enforced ------------------------- */

  describe("proof + chain integrity (unchanged from V1)", function () {
    it("an invalid proof reverts InvalidProof on every entry point", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await expect(v2.submitProof(p, PROOF)).to.be.revertedWithCustomError(v2, "InvalidProof");
      await expect(v2.submitProofWithExpectedRoot(p, PROOF, CANONICAL_ROOT)).to.be.revertedWithCustomError(
        v2,
        "InvalidProof"
      );
      await expect(v2.verify(p, PROOF)).to.be.revertedWithCustomError(v2, "InvalidProof");
    });

    it("a wrong chainId reverts WrongChain", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION), cid: 1n });
      await accept(p);
      await expect(v2.submitProof(p, PROOF)).to.be.revertedWithCustomError(v2, "WrongChain").withArgs(1n, chainId);
    });

    it("a wrong-length publicValues reverts BadPublicValuesLength", async function () {
      await expect(v2.submitProof("0x1234", PROOF)).to.be.revertedWithCustomError(v2, "BadPublicValuesLength").withArgs(2);
    });

    it("submitProofWithExpectedRoot still enforces the expected root", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await accept(p);
      await expect(v2.submitProofWithExpectedRoot(p, PROOF, "0x" + "11".repeat(32)))
        .to.be.revertedWithCustomError(v2, "StateRootMismatch")
        .withArgs(CANONICAL_ROOT, "0x" + "11".repeat(32));
    });

    it("verify() is unchanged and still returns the decoded tuple (the anchor depends on it)", async function () {
      const p = pv({ stateRoot: CANONICAL_ROOT, value: word(FOUNDATION) });
      await accept(p);
      const out = await v2.verify(p, PROOF);
      expect(out[0]).to.equal(BN);
      expect(out[1]).to.equal(CANONICAL_ROOT);
      expect(out[2]).to.equal(TREASURY);
      expect(out[4]).to.equal(word(FOUNDATION));
    });
  });
});
