const { expect } = require("chai");
const { ethers } = require("hardhat");

/**
 * AereOutboundVerifierV2 — anchor-bind fix, with the V1 forgery as a PAIRED CONTROL.
 *
 * WHAT THIS FILE PROVES, AND WHAT IT DOES NOT
 * ───────────────────────────────────────────
 * The forgery has two independent legs:
 *
 *   LEG A (the zk layer) — "a valid SP1 proof of the forged statement exists".
 *     PROVEN LIVE, NOT HERE. Real Groth16 proofs of the forged header (7
 *     attacker-generated keys named as the validator set, self-sealed 5-of-7,
 *     attacker-built receiptsRoot) were generated on a dedicated prover and
 *     submitted by eth_call to the REAL, LIVE SP1VerifierGateway 0x9ca479C8…70628:
 *       * under the V1-design vkey 0x00123374…66f5 (256-byte, 8-word publicValues,
 *         no validatorSetRoot)      -> ACCEPTED;
 *       * under the vkey the live V2 actually pins, 0x00a71f7d…08d0 -> ALSO ACCEPTED.
 *     Tampering either the proof or the publicValues -> REJECTED, so the gateway is
 *     genuinely verifying rather than rubber-stamping. The forged statement is
 *     cryptographically VALID; only the anchor bind refuses it.
 *
 *   LEG B (the contract layer) — "given that proof, the contract's gates accept it".
 *     PROVEN HERE, with a mock verifier standing in for the (already-proven) zk leg.
 *
 * A and B together => V1's design would have delivered a forged bridge message.
 * The mock verifier is NOT a shortcut around the cryptography: leg A establishes the
 * real verifier's verdict on the real proof, and this file isolates the gate logic
 * that runs after it. We deliberately did NOT deploy a live forgeable verifier.
 *
 * HONEST SCOPE OF THE BYTES BELOW: these publicValues are built in the SAME layout
 * and shape as the real committed values, but re-derived for the LOCAL test chain id
 * (V1/V2 both require destChainId == block.chainid, and this chain is not 2800), and
 * with a placeholder blockHash/root. They exercise the GATE LOGIC, not the crypto.
 * The genuinely real committed values (chainId 2800, forged blockHash
 * 0xf210da56…b42b, attacker root 0x4f2c0cf7…23ee) were exercised live: the live V2
 * reverts ValidatorSetMismatch(0x4f2c0cf7…23ee, 0x5fa93ba7…dd79) on them. See
 * zk-light-client/OUTBOUND-VERIFIER-FIX-2026-07-16.md for every tx hash.
 */

// ── Real artifacts from the 2026-07-16/17 prover run (see the report) ──
const ANCHOR = "0x5fa93ba7730cdb599fd814c0497565f320f10d808b6ed485c3e02fb1a1e2dd79";
const OUTBOX = "0xd43FeacbbdDc5ff7cE4A72C726f3FBD204ef7936";
const MIN_SOURCE_BLOCK = 9312565;
const VKEY_V2 = "0x00a71f7d74d3a503326a93c88b0f6a6d3bfe4609e79959cc5cad56a0d5f208d0";
const VKEY_V1_CONTROL = "0x0012337429546d116941b8ac9532008a66d438b0de859002d26d6b205d4766f5"; // REAL vkey of the rebuilt V1-design guest (the mock ignores it; the live gateway accepted this vkey + the forged proof)

const AERE_CHAIN_ID = 2800n;
const EVENT_SIG = ethers.id("AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)");

const abi = ethers.AbiCoder.defaultAbiCoder();

function messageIdOf(destChainId, nonce, sender, handler, payload) {
  return ethers.keccak256(
    abi.encode(
      ["uint256", "uint256", "uint256", "address", "address", "bytes"],
      [AERE_CHAIN_ID, destChainId, nonce, sender, handler, payload]
    )
  );
}

function commitmentOf(blockHash, outbox, messageId, destChainId, nonce, sender, handler, payload) {
  const dataHash = ethers.keccak256(
    abi.encode(["uint256", "address", "address", "bytes"], [nonce, sender, handler, payload])
  );
  return ethers.keccak256(
    ethers.concat([
      ethers.zeroPadValue(ethers.toBeHex(AERE_CHAIN_ID), 32),
      blockHash,
      ethers.zeroPadValue(outbox, 32),
      EVENT_SIG,
      messageId,
      ethers.zeroPadValue(ethers.toBeHex(destChainId), 32),
      dataHash,
    ])
  );
}

// V2 guest: 9 committed words, chainId from a constant, validatorSetRoot present.
function encodeV2(chainId, blockNumber, blockHash, root, claimKind, commitment, vsr, finalized, falcon) {
  return abi.encode(
    ["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bytes32", "bool", "bool"],
    [chainId, blockNumber, blockHash, root, claimKind, commitment, vsr, finalized, falcon]
  );
}

// V1 guest: 8 committed words, chainId ECHOED from prover input, NO validatorSetRoot.
function encodeV1(chainId, blockNumber, blockHash, root, claimKind, commitment, finalized, falcon) {
  return abi.encode(
    ["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bool", "bool"],
    [chainId, blockNumber, blockHash, root, claimKind, commitment, finalized, falcon]
  );
}

describe("AereOutboundVerifierV2 — validator-set anchor bind (the V1 forgery fix)", () => {
  let mock, recorder, v2, v1, deployer;
  const FORGED_BLOCK_HASH = "0x" + "ab".repeat(32);
  const FORGED_ROOT = "0x" + "cd".repeat(32);
  // keccak over an attacker's self-nominated set — any value that is not the anchor.
  const ATTACKER_VSR = ethers.keccak256("0x" + "de".repeat(140));

  before(async () => {
    [deployer] = await ethers.getSigners();

    const Mock = await ethers.getContractFactory("MockSP1VerifierAlwaysAccepts");
    mock = await Mock.deploy();
    await mock.waitForDeployment();

    const Rec = await ethers.getContractFactory("AereOutboundDeliveryRecorder");
    recorder = await Rec.deploy();
    await recorder.waitForDeployment();

    const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
    v2 = await V2.deploy(await mock.getAddress(), VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX);
    await v2.waitForDeployment();

    // The ORIGINAL V1 contract, wired to the same mock — the control.
    const V1 = await ethers.getContractFactory("AereOutboundVerifier");
    v1 = await V1.deploy(await mock.getAddress(), VKEY_V1_CONTROL, OUTBOX);
    await v1.waitForDeployment();
  });

  it("CONTROL: the V1 design ACCEPTS the forgery and delivers a message nobody authorised", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const nonce = 999999n;
    const sender = ethers.getAddress("0x00000000000000000000000000000000deadbeef");
    const payload = ethers.hexlify(ethers.toUtf8Bytes("FORGED"));

    const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
    const commitment = commitmentOf(
      FORGED_BLOCK_HASH,
      OUTBOX,
      messageId,
      destChainId,
      nonce,
      sender,
      handler,
      payload
    );

    // The attacker simply declares chainId = 2800 — it was an echoed prover input.
    const pv = encodeV1(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, true, false);

    const before = await recorder.deliveryCount();
    await v1.deliver(pv, "0x", destChainId, nonce, sender, handler, payload);
    const after = await recorder.deliveryCount();

    // THE BUG, REPRODUCED: a message the real Outbox never emitted was delivered.
    expect(after - before).to.equal(1n);
    expect(await v1.delivered(messageId)).to.equal(true);
    const d = await recorder.deliveries(Number(after) - 1);
    expect(d.sender).to.equal(sender);
    expect(ethers.toUtf8String(await recorder.payloadAt(Number(after) - 1))).to.equal("FORGED");
  });

  it("FIX: V2 REJECTS the identical forgery — committed root != anchored root", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const nonce = 999999n;
    const sender = ethers.getAddress("0x00000000000000000000000000000000deadbeef");
    const payload = ethers.hexlify(ethers.toUtf8Bytes("FORGED"));

    const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
    const commitment = commitmentOf(
      FORGED_BLOCK_HASH,
      OUTBOX,
      messageId,
      destChainId,
      nonce,
      sender,
      handler,
      payload
    );

    // Same forgery. The V2 guest is FORCED to commit which set it used.
    const pv = encodeV2(
      2800,
      9999999,
      FORGED_BLOCK_HASH,
      FORGED_ROOT,
      1,
      commitment,
      ATTACKER_VSR, // <- the attacker's own set
      true,
      false
    );

    const before = await recorder.deliveryCount();
    await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload))
      .to.be.revertedWithCustomError(v2, "ValidatorSetMismatch")
      .withArgs(ATTACKER_VSR, ANCHOR);
    expect(await recorder.deliveryCount()).to.equal(before); // nothing delivered
    expect(await v2.delivered(messageId)).to.equal(false);
  });

  it("FIX: V2 ACCEPTS a well-formed message that attests the ANCHORED set", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const nonce = 7n;
    const sender = deployer.address;
    const payload = ethers.hexlify(ethers.toUtf8Bytes("legit"));

    const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
    const commitment = commitmentOf(
      FORGED_BLOCK_HASH,
      OUTBOX,
      messageId,
      destChainId,
      nonce,
      sender,
      handler,
      payload
    );
    const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);

    await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload)).to.emit(v2, "MessageDelivered");
    expect(await v2.delivered(messageId)).to.equal(true);
  });

  it("replay of the same delivered message reverts", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const nonce = 7n;
    const sender = deployer.address;
    const payload = ethers.hexlify(ethers.toUtf8Bytes("legit"));
    const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
    const commitment = commitmentOf(
      FORGED_BLOCK_HASH,
      OUTBOX,
      messageId,
      destChainId,
      nonce,
      sender,
      handler,
      payload
    );
    const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);

    await expect(v2.deliver(pv, "0x", destChainId, nonce, sender, handler, payload))
      .to.be.revertedWithCustomError(v2, "AlreadyDelivered")
      .withArgs(messageId);
  });

  it("rejects a wrong-length publicValues blob instead of silently abi.decoding it", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const short = encodeV1(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, true, false);
    expect(ethers.dataLength(short)).to.equal(256); // the V1 8-word layout
    await expect(v2.deliver(short, "0x", destChainId, 1n, deployer.address, handler, "0x"))
      .to.be.revertedWithCustomError(v2, "BadPublicValuesLength")
      .withArgs(256);
  });

  it("rejects a source block below the anchored set's validity window", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const pv = encodeV2(2800, 1000, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, ANCHOR, true, false);
    await expect(v2.deliver(pv, "0x", destChainId, 1n, deployer.address, handler, "0x"))
      .to.be.revertedWithCustomError(v2, "SourceBlockTooOld")
      .withArgs(1000, MIN_SOURCE_BLOCK);
  });

  it("rejects an unbacked post-quantum claim (falconVerified = true)", async () => {
    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, ethers.ZeroHash, ANCHOR, true, true);
    await expect(v2.deliver(pv, "0x", destChainId, 1n, deployer.address, handler, "0x")).to.be.revertedWithCustomError(
      v2,
      "UnexpectedFalconFlag"
    );
  });

  it("constructor refuses a zero vkey (V1's fail-closed trap is not repeatable)", async () => {
    const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
    await expect(
      V2.deploy(await mock.getAddress(), ethers.ZeroHash, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX)
    ).to.be.revertedWithCustomError(V2, "ZeroValue");
    await expect(
      V2.deploy(await mock.getAddress(), VKEY_V2, ethers.ZeroHash, MIN_SOURCE_BLOCK, OUTBOX)
    ).to.be.revertedWithCustomError(V2, "ZeroValue");
  });

  it("constructor refuses a code-less verifier", async () => {
    const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
    await expect(
      V2.deploy(deployer.address, VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX)
    ).to.be.revertedWithCustomError(V2, "VerifierHasNoCode");
  });

  it("rejects a proof the SP1 verifier does not accept", async () => {
    const Rejecting = await ethers.getContractFactory("MockSP1VerifierAlwaysReverts");
    const rej = await Rejecting.deploy();
    await rej.waitForDeployment();
    const V2 = await ethers.getContractFactory("AereOutboundVerifierV2");
    const v2r = await V2.deploy(await rej.getAddress(), VKEY_V2, ANCHOR, MIN_SOURCE_BLOCK, OUTBOX);
    await v2r.waitForDeployment();

    const handler = await recorder.getAddress();
    const destChainId = (await ethers.provider.getNetwork()).chainId;
    const nonce = 42n;
    const sender = deployer.address;
    const payload = "0x";
    const messageId = messageIdOf(destChainId, nonce, sender, handler, payload);
    const commitment = commitmentOf(
      FORGED_BLOCK_HASH,
      OUTBOX,
      messageId,
      destChainId,
      nonce,
      sender,
      handler,
      payload
    );
    const pv = encodeV2(2800, 9999999, FORGED_BLOCK_HASH, FORGED_ROOT, 1, commitment, ANCHOR, true, false);
    await expect(v2r.deliver(pv, "0x", destChainId, nonce, sender, handler, payload)).to.be.revertedWithCustomError(
      v2r,
      "InvalidProof"
    );
  });
});
