// FINDING 4 (low): AereHistoryStateRootAnchor.proveAgainstAnchor ran the full
// SP1 Groth16 verification TWICE on identical inputs (once via VERIFIER.verify
// to read the block number, then again inside submitProofWithExpectedRoot).
//
// This suite:
//  1. Reproduces the redundant verification against the ORIGINAL V1 contract
//     using a verifier whose verify() reverts. V1's proveAgainstAnchor still
//     calls verify(), so the call reverts, witnessing the redundant path.
//  2. Proves the corrected V2 contract derives the block number locally and
//     calls ONLY submitProofWithExpectedRoot, so exactly ONE verification runs
//     (submitCalls == 1) while behaviour is unchanged: a valid proof records and
//     tampered / invalid inputs revert.
//
// The anchored root is REAL: the AERE genesis header (chain 2800) is
// reconstructed and its real state root is anchored, then proven against.

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { encodeHeader } = require("./header-rlp");
const genesis = require("./fixtures/aere-genesis-header.json");

const ZERO32 = "0x" + "00".repeat(32);
const BLOCK = 5000n; // any block number; the mock injects its canonical hash
const BLOCK2 = 6000n; // a SECOND anchored block with a DIFFERENT state root

// abi.encode(chainId, blockNumber, stateRoot, account, slot, value) -> 192 bytes
function publicValues({ chainId = 2800n, blockNumber, stateRoot, account, slot, value }) {
  return ethers.AbiCoder.defaultAbiCoder().encode(
    ["uint256", "uint256", "bytes32", "address", "bytes32", "bytes32"],
    [chainId, blockNumber, stateRoot, account, slot, value]
  );
}

describe("AereHistoryStateRootAnchor FINDING 4 (double SP1 verify)", function () {
  const account = ethers.getAddress("0x00000000000000000000000000000000000000a1");
  const slot = "0x" + "00".repeat(31) + "05";
  const value = "0x" + "00".repeat(31) + "2a";

  let verifier;

  async function deployVerifier() {
    const V = await ethers.getContractFactory("CountingStorageProofVerifier");
    const v = await V.deploy();
    await v.waitForDeployment();
    return v;
  }

  async function deployAnchor(factoryName, verifierAddr) {
    const F = await ethers.getContractFactory(factoryName);
    const a = await F.deploy(verifierAddr);
    await a.waitForDeployment();
    return a;
  }

  // Anchor the REAL genesis state root at BLOCK on the given anchor.
  async function anchorGenesis(anchor) {
    const header = encodeHeader(genesis);
    await anchor.setHistoryBlockHash(BLOCK, genesis.hash);
    await anchor.anchorHistoricalHeader(BLOCK, header);
    expect(await anchor.anchoredStateRoot(BLOCK)).to.equal(genesis.stateRoot);
  }

  // Anchor a SECOND, distinct canonical block at BLOCK2 whose state root differs
  // from genesis. We reuse the real genesis header but overwrite only its
  // stateRoot field, so the header re-encodes to a NEW keccak (a new canonical
  // hash the mock injects) and anchors a different-but-well-formed root. This
  // gives us two anchored blocks with different roots to test cross-block
  // confusion.
  const OTHER_ROOT =
    "0x1111111111111111111111111111111111111111111111111111111111111111";
  async function anchorSecondBlock(anchor) {
    const header2 = encodeHeader({ ...genesis, stateRoot: OTHER_ROOT });
    const hash2 = ethers.keccak256(header2);
    await anchor.setHistoryBlockHash(BLOCK2, hash2);
    await anchor.anchorHistoricalHeader(BLOCK2, header2);
    expect(await anchor.anchoredStateRoot(BLOCK2)).to.equal(OTHER_ROOT);
  }

  beforeEach(async function () {
    verifier = await deployVerifier();
  });

  describe("reproduction: V1 makes the redundant verify() call", function () {
    it("proveAgainstAnchor reverts because V1 still calls verify() (double verification)", async function () {
      const anchor = await deployAnchor("MockHistoryAnchorV1", await verifier.getAddress());
      await anchorGenesis(anchor);

      const pv = publicValues({
        blockNumber: BLOCK,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });

      // The counting verifier rejects any verify() call. V1 calls verify()
      // first, so the whole tx reverts: this is the redundant verification the
      // finding describes. On a real verifier the call would instead succeed and
      // burn a full extra Groth16 pairing.
      await expect(anchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
        verifier,
        "RedundantVerifyCall"
      );

      // No verification ever reached the recording path.
      expect(await verifier.submitCalls()).to.equal(0n);
    });
  });

  describe("fix: V2 verifies exactly once", function () {
    let anchor;

    beforeEach(async function () {
      anchor = await deployAnchor("MockHistoryAnchorV2", await verifier.getAddress());
      await anchorGenesis(anchor);
    });

    it("records a valid proof running submitProofWithExpectedRoot ONCE and never verify()", async function () {
      const pv = publicValues({
        blockNumber: BLOCK,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });

      await expect(anchor.proveAgainstAnchor(pv, "0x"))
        .to.emit(anchor, "ProvenAgainstAnchor")
        .withArgs(BLOCK, genesis.stateRoot, account, slot, value);

      // Exactly one verification, via the recording path only.
      expect(await verifier.submitCalls()).to.equal(1n);
      expect(await verifier.recorded(ethers.keccak256(pv))).to.equal(true);
    });

    it("reverts when the committed root is tampered to differ from the anchored root", async function () {
      const pv = publicValues({
        blockNumber: BLOCK,
        stateRoot: ZERO32, // not the anchored genesis root
        account,
        slot,
        value,
      });

      // submit binds committed root to anchored root; mismatch reverts.
      await expect(anchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWith("StateRootMismatch");
      expect(await verifier.submitCalls()).to.equal(0n);
    });

    it("reverts NotAnchored when the proof's block number was never anchored", async function () {
      const pv = publicValues({
        blockNumber: 999999n,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });

      await expect(anchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
        anchor,
        "NotAnchored"
      );
      // Reverts before any verification.
      expect(await verifier.submitCalls()).to.equal(0n);
    });

    it("reverts BadPublicValuesLength for a non-192-byte payload", async function () {
      const short = "0x" + "11".repeat(160); // 160 bytes, one word short
      await expect(anchor.proveAgainstAnchor(short, "0x")).to.be.revertedWithCustomError(
        anchor,
        "BadPublicValuesLength"
      );
    });

    it("reverts when the underlying SP1 proof is invalid (verification still gates recording)", async function () {
      await verifier.setValidate(false);
      const pv = publicValues({
        blockNumber: BLOCK,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });
      await expect(anchor.proveAgainstAnchor(pv, "0x")).to.be.reverted;
      expect(await verifier.submitCalls()).to.equal(0n);
    });

    it("decodes block number, account, and slot locally from the 192-byte layout", async function () {
      const account2 = ethers.getAddress("0x00000000000000000000000000000000000000b2");
      const slot2 = "0x" + "00".repeat(31) + "09";
      const value2 = "0x" + "00".repeat(30) + "0100";
      const pv = publicValues({
        blockNumber: BLOCK,
        stateRoot: genesis.stateRoot,
        account: account2,
        slot: slot2,
        value: value2,
      });
      await expect(anchor.proveAgainstAnchor(pv, "0x"))
        .to.emit(anchor, "ProvenAgainstAnchor")
        .withArgs(BLOCK, genesis.stateRoot, account2, slot2, value2);
      expect(await verifier.submitCalls()).to.equal(1n);
    });
  });

  // RESIDUAL ATTACK VARIANT (cross-block root confusion).
  //
  // The V2 fix selects the anchored root using the blockNumber it decodes
  // LOCALLY from public-values word 1, and no longer cross-checks any block
  // number returned by VERIFIER.verify (that call is gone). So the sharpest
  // residual question is: can an attacker take a proof that is genuinely valid
  // for anchored block A and redirect it against a DIFFERENT anchored block B
  // (word 1 = B) to get an off-root value recorded? It must NOT: the single
  // submitProofWithExpectedRoot binds the proof's committed root (word 2) to
  // anchored[B]'s root, so the tampered block number reverts, and the exactly-
  // once property must hold (submitCalls stays 0 on the failed attack, and a
  // later honest proof still records exactly once with no verify() call).
  describe("residual attack variant: cross-block root confusion", function () {
    it("V1 (reproduction) still runs the redundant verify() on the cross-block attempt", async function () {
      const anchor = await deployAnchor("MockHistoryAnchorV1", await verifier.getAddress());
      await anchorGenesis(anchor);
      await anchorSecondBlock(anchor);

      // Proof genuinely valid for BLOCK (root = genesis root) but word 1 tampered
      // to BLOCK2 (anchored to OTHER_ROOT).
      const pv = publicValues({
        blockNumber: BLOCK2,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });

      // V1 calls verify() FIRST, so it reverts on the redundant path rather than
      // through the single submit binding. This witnesses the double-verify the
      // finding removes.
      await expect(anchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
        verifier,
        "RedundantVerifyCall"
      );
      expect(await verifier.submitCalls()).to.equal(0n);
    });

    it("V2 (closed) rejects the cross-block attempt through the single submit binding, no verify(), no record", async function () {
      const anchor = await deployAnchor("MockHistoryAnchorV2", await verifier.getAddress());
      await anchorGenesis(anchor);
      await anchorSecondBlock(anchor);

      const attackPv = publicValues({
        blockNumber: BLOCK2, // redirect a genesis-root proof at the OTHER_ROOT block
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });

      // Anchored[BLOCK2] = OTHER_ROOT, committed root = genesis root: submit binds
      // them and reverts. No fallback to verify(), and nothing recorded.
      await expect(anchor.proveAgainstAnchor(attackPv, "0x")).to.be.revertedWith(
        "StateRootMismatch"
      );
      expect(await verifier.submitCalls()).to.equal(0n);
      expect(await verifier.recorded(ethers.keccak256(attackPv))).to.equal(false);

      // The failed attack corrupts nothing: an honest proof for BLOCK2's real
      // root still records, and does so with EXACTLY ONE verification.
      const honestPv = publicValues({
        blockNumber: BLOCK2,
        stateRoot: OTHER_ROOT,
        account,
        slot,
        value,
      });
      await expect(anchor.proveAgainstAnchor(honestPv, "0x"))
        .to.emit(anchor, "ProvenAgainstAnchor")
        .withArgs(BLOCK2, OTHER_ROOT, account, slot, value);
      expect(await verifier.submitCalls()).to.equal(1n);
      expect(await verifier.recorded(ethers.keccak256(honestPv))).to.equal(true);
    });
  });
});
