// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/// @notice Minimal Succinct SP1 on-chain verifier interface.
/// @dev    The canonical `SP1VerifierGateway` / `SP1Verifier` exposes exactly
///         this. `verifyProof` REVERTS if `proofBytes` is not a valid Groth16
///         proof for `(programVKey, publicValues)`; it returns nothing on success.
interface ISP1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proofBytes
    ) external view;
}

/// @notice Destination-chain application handler. `AereOutboundVerifierV2` calls
///         this once, and only once, per proven+unused message.
interface IAereMessageHandlerV2 {
    /// @param srcChainId the AERE source chain id (2800).
    /// @param sender     the original `msg.sender` on the AERE Outbox.
    /// @param payload    the application bytes from the Outbox message.
    function handleAereMessage(uint256 srcChainId, address sender, bytes calldata payload) external;
}

/// @title AereOutboundVerifierV2
/// @notice Destination-chain verifier that completes the trust-minimised OUTBOUND
///         interop loop (AERE -> this chain). It accepts an SP1/Groth16 proof that
///         an `AereOutboundOutbox` message log was included in a QBFT-final AERE
///         block and — if the proof verifies against the pinned finality vkey AND
///         attests the ANCHORED validator set — delivers the message exactly once.
///
/// @dev ─────────────────────────────────────────────────────────────────────────
///      WHY V2 EXISTS: V1 (`0x8E893686b6B2509C5f7Fe477CE530Bc868B0074e`) WAS
///      FORGEABLE BY DESIGN. Do not reuse it; do not pin a vkey into it.
///      ─────────────────────────────────────────────────────────────────────────
///      The zk-interop guest takes its ENTIRE trust anchor as private,
///      prover-supplied input: `InteropInput { chain_id, finality: FinalityInput {
///      known_validators, .. }, .. }`. `verify_finality` checks the header's
///      `extraData` validator list against *the prover's own* `known_validators`
///      — never against anything external. V1's guest then committed
///      `(chainId, blockNumber, blockHash, root, claimKind, commitment, finalized,
///      falconVerified)`: NO validator-set commitment. V1 bound only
///      `chainId == 2800` — and `chainId` was itself the echoed prover input.
///
///      So the attack was: generate N keypairs, fabricate a header whose
///      `extraData` lists those keys and whose `receiptsRoot` is a trie the
///      attacker built containing a forged `AereCrossChainMessage` log ascribed to
///      the real Outbox address, self-seal a quorum over it, set `chain_id = 2800`,
///      and prove. Every check passes HONESTLY — the proof is cryptographically
///      valid — and V1 delivers an arbitrary message. `finalized == true` meant
///      only "final under a set the prover chose", which is no statement at all.
///      (Found by code-read 2026-07-16; never exploited — V1 shipped with
///      `programVKey == 0x0`, which is fail-closed, so every proof reverted.)
///
///      THE FIX, mirroring `AereZkQbftLightClient` (`0xCaDA54FA…6488`), which was
///      always safe precisely because it does this:
///        1. the guest COMMITS `validatorSetRoot` — keccak256 over the set the
///           finality obligation was actually discharged against — into its public
///           values, and commits `chainId` from a compile-time constant rather than
///           echoing the prover's;
///        2. this contract BINDS that committed root to `TRUSTED_VALIDATOR_SET_ROOT`,
///           an IMMUTABLE on-chain anchor, and rejects anything else.
///      The guest cannot know which set is real; the anchor decides. The guest's
///      only job is to state, unforgeably, WHICH set it used.
///
/// @dev PUBLIC-VALUES ENCODING (must match the guest's committed struct):
///      abi.encode(
///        uint64  chainId,          // must be AERE_CHAIN_ID (2800)
///        uint64  blockNumber,      // source block height; must be >= MIN_SOURCE_BLOCK
///        bytes32 blockHash,        // the QBFT-final block that carries the log
///        bytes32 root,             // receiptsRoot (informational; bound via proof)
///        uint8   claimKind,        // must be 1 (receipt/log claim)
///        bytes32 commitment,       // canonical message commitment (see below)
///        bytes32 validatorSetRoot, // MUST equal TRUSTED_VALIDATOR_SET_ROOT  <-- THE FIX
///        bool    finalized,        // must be true
///        bool    falconVerified    // must be false (mainnet consensus is ECDSA)
///      )
///      All nine fields are static 32-byte words, so this is a plain 288-byte
///      concatenation — identical to `alloy` `SolType::abi_encode` of the guest's
///      `PublicValues` struct. The length is checked exactly: a shorter/longer blob
///      is rejected rather than silently abi.decode'd.
///
///      COMMITMENT RE-DERIVATION (must match interop_core log_commitment(0)):
///        commitment = keccak256(
///          bytes32(AERE_CHAIN_ID) ‖ blockHash ‖ bytes32(outbox) ‖
///          EVENT_SIG ‖ messageId ‖ bytes32(destChainId) ‖ keccak256(data))
///      where data = abi.encode(nonce, sender, targetHandler, payload) and
///      messageId = keccak256(abi.encode(AERE_CHAIN_ID, destChainId, nonce,
///                                       sender, targetHandler, payload)).
///
/// SECURITY MODEL — trust-minimised, NOT trustless. Read before quoting.
///   * APPLICATION / INTEROP LAYER ONLY. This does not change, gate or touch AERE
///     consensus. Mainnet QBFT remains classical secp256k1 ECDSA.
///   * A valid proof attests that `>= ceil(2N/3)` of the ANCHORED validator set
///     sealed the source block. With the live N=7 set that is a 5-of-7 quorum
///     (2f+1, f=2). Delivery security == that validator set.
///   * NOT POST-QUANTUM. The committed seals are secp256k1 ECDSA and the SP1 wrap
///     is Groth16 over BN254 — both classical and quantum-vulnerable. A Falcon slot
///     is reserved but inactive; `falconVerified` MUST be `false` so this contract
///     can never launder an unbacked post-quantum claim.
///   * Security reduces to the CONJUNCTION of: >= 5-of-7 AERE validators honest
///     (all Foundation-operated today — an operator assumption, not an economic
///     one) + the pinned PROGRAM_VKEY binding the intended program + the SP1
///     gateway/Groth16 verifier being sound + TRUSTED_VALIDATOR_SET_ROOT being the
///     right set.
///   * THE ANCHOR DOES NOT TRACK ROTATION. It is immutable by design (a mutable
///     anchor would reintroduce a trusted setter). A validator-set change requires
///     a fresh deployment. Consumers MUST check TRUSTED_VALIDATOR_SET_ROOT still
///     equals keccak256 over the live `qbft_getValidatorsByBlockNumber` set before
///     trusting this contract. Treat the anchor as an expiring credential.
contract AereOutboundVerifierV2 {
    /// @notice AERE mainnet chain id the pinned vkey attests finality for.
    uint256 public constant AERE_CHAIN_ID = 2800;

    /// @notice Exact byte length of the guest's 9-word committed public values.
    uint256 public constant PUBLIC_VALUES_LENGTH = 288;

    /// @notice topic0 preimage hash of `AereOutboundOutbox.AereCrossChainMessage`.
    bytes32 public constant EVENT_SIG =
        keccak256("AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)");

    /// @notice The SP1 verifier (gateway) this contract calls. Immutable.
    ISP1Verifier public immutable sp1Verifier;

    /// @notice The pinned AERE QBFT-finality+inclusion program vkey. A proof for any
    ///         other program fails `sp1Verifier.verifyProof`. Immutable, non-zero.
    bytes32 public immutable programVKey;

    /// @notice THE FIX. Immutable commitment to the trusted AERE validator set:
    ///         keccak256 over the addresses in stored (ascending) order, identical
    ///         to the guest's `validator_set_root`. Every accepted proof must attest
    ///         THIS set. Without this bind, the prover picks the set and the bridge
    ///         is forgeable — see the V1 postmortem in the contract NatSpec above.
    bytes32 public immutable TRUSTED_VALIDATOR_SET_ROOT;

    /// @notice Lower bound of the anchored set's validity window: the first block
    ///         sealed by TRUSTED_VALIDATOR_SET_ROOT's set. Defence in depth — a
    ///         header sealed by an older set already fails the anchor bind, since
    ///         its committed root differs.
    uint64 public immutable MIN_SOURCE_BLOCK;

    /// @notice The trusted AERE-side Outbox address bound into every commitment.
    ///         A log from any other contract yields a different commitment and is
    ///         rejected. Immutable.
    address public immutable outbox;

    /// @notice Replay protection: messageId => delivered.
    mapping(bytes32 => bool) public delivered;

    /// @notice Number of messages delivered.
    uint64 public deliveryCount;

    event MessageDelivered(
        bytes32 indexed messageId,
        address indexed sender,
        address indexed targetHandler,
        uint64 sourceBlockNumber,
        bytes32 sourceBlockHash
    );

    error BadPublicValuesLength(uint256 got);
    error WrongSourceChain(uint64 got);
    error NotReceiptClaim(uint8 got);
    error NotFinal();
    error UnexpectedFalconFlag();
    error WrongDestinationChain(uint256 got);
    error CommitmentMismatch(bytes32 expected, bytes32 got);
    error AlreadyDelivered(bytes32 messageId);
    /// @notice THE FIX firing: the proof attested a validator set that is not the
    ///         anchored one (e.g. an attacker's self-nominated keys).
    error ValidatorSetMismatch(bytes32 inProof, bytes32 trusted);
    error SourceBlockTooOld(uint64 got, uint64 minimum);
    error InvalidProof();
    error ProgramNotConfigured();
    error ZeroAddress();
    error ZeroValue();
    error VerifierHasNoCode();

    /// @param _sp1Verifier       the SP1 verifier / gateway on this destination chain.
    /// @param _programVKey       the zk-interop guest vkey to pin (MUST be non-zero).
    /// @param _validatorSetRoot  keccak256 over AERE's trusted validator set, in
    ///                           stored ascending order (MUST be non-zero).
    /// @param _minSourceBlock    first block sealed by that set.
    /// @param _outbox            the AERE-side `AereOutboundOutbox` address to trust.
    constructor(
        ISP1Verifier _sp1Verifier,
        bytes32 _programVKey,
        bytes32 _validatorSetRoot,
        uint64 _minSourceBlock,
        address _outbox
    ) {
        if (address(_sp1Verifier) == address(0)) revert ZeroAddress();
        // A code-less verifier would make verifyProof (which returns no data) succeed
        // silently, accepting every "proof". Removes a deploy-time footgun.
        if (address(_sp1Verifier).code.length == 0) revert VerifierHasNoCode();
        if (_outbox == address(0)) revert ZeroAddress();
        // Fail-closed by construction: V1 was deployable with a zero vkey. A zero
        // vkey is safe (every proof reverts) but it is a trap — it invites someone
        // to "fix" it later by pinning a vkey into an unbound contract. Refuse it.
        if (_programVKey == bytes32(0) || _validatorSetRoot == bytes32(0)) revert ZeroValue();
        sp1Verifier = _sp1Verifier;
        programVKey = _programVKey;
        TRUSTED_VALIDATOR_SET_ROOT = _validatorSetRoot;
        MIN_SOURCE_BLOCK = _minSourceBlock;
        outbox = _outbox;
    }

    /// @notice Verify an AERE outbound-message proof and deliver it exactly once.
    /// @param publicValues  the guest's ABI-encoded committed public values (288 bytes).
    /// @param proofBytes    the SP1 Groth16 proof over `(programVKey, publicValues)`.
    /// @param destChainId   the message's destination chain id (must be this chain).
    /// @param nonce         the Outbox message nonce.
    /// @param sender        the original Outbox `msg.sender`.
    /// @param targetHandler the destination handler to deliver to.
    /// @param payload       the application payload.
    function deliver(
        bytes calldata publicValues,
        bytes calldata proofBytes,
        uint256 destChainId,
        uint256 nonce,
        address sender,
        address targetHandler,
        bytes calldata payload
    ) external {
        // Belt-and-suspenders: unreachable (the constructor rejects a zero vkey),
        // but keeps the fail-closed property explicit and local.
        if (programVKey == bytes32(0)) revert ProgramNotConfigured();
        if (publicValues.length != PUBLIC_VALUES_LENGTH) {
            revert BadPublicValuesLength(publicValues.length);
        }

        (
            uint64 chainId,
            uint64 blockNumber,
            bytes32 blockHash,
            , // root (informational; bound into the proof, not re-checked here)
            uint8 claimKind,
            bytes32 commitment,
            bytes32 validatorSetRoot,
            bool finalized,
            bool falconVerified
        ) = abi.decode(
                publicValues,
                (uint64, uint64, bytes32, bytes32, uint8, bytes32, bytes32, bool, bool)
            );

        // ---- Cheap public-input gates (before the expensive proof check). ----

        // THE FIX. Bind the set the proof ACTUALLY used to this contract's immutable
        // anchor. An attacker who self-nominates a validator set produces a valid
        // proof carrying a DIFFERENT root, and dies here. This single check is what
        // turns "final under some set" into "final on AERE".
        if (validatorSetRoot != TRUSTED_VALIDATOR_SET_ROOT) {
            revert ValidatorSetMismatch(validatorSetRoot, TRUSTED_VALIDATOR_SET_ROOT);
        }
        // Now meaningful: the guest commits chainId from a constant, not the prover's input.
        if (chainId != AERE_CHAIN_ID) revert WrongSourceChain(chainId);
        if (blockNumber < MIN_SOURCE_BLOCK) revert SourceBlockTooOld(blockNumber, MIN_SOURCE_BLOCK);
        if (claimKind != 1) revert NotReceiptClaim(claimKind);
        if (!finalized) revert NotFinal();
        // Honesty invariant: mainnet consensus is classical ECDSA. A `true` here
        // would be an unbacked post-quantum claim; refuse it.
        if (falconVerified) revert UnexpectedFalconFlag();
        if (destChainId != block.chainid) revert WrongDestinationChain(destChainId);

        // ---- Replay gate (cheap; re-asserted with the effect below). ----
        bytes32 messageId = keccak256(
            abi.encode(AERE_CHAIN_ID, destChainId, nonce, sender, targetHandler, payload)
        );
        if (delivered[messageId]) revert AlreadyDelivered(messageId);

        // ---- The zk check: proof valid for (pinned vkey, these publicValues). ----
        // Reverts if the proof is not a genuine QBFT-finality+inclusion proof, if
        // `publicValues` were tampered, or if it was produced for another program.
        try sp1Verifier.verifyProof(programVKey, publicValues, proofBytes) {
            // verified — did not revert
        } catch {
            revert InvalidProof();
        }

        // ---- Bind the proven commitment to the delivered message fields. ----
        bytes32 dataHash = keccak256(abi.encode(nonce, sender, targetHandler, payload));
        bytes32 expected = keccak256(
            abi.encodePacked(
                bytes32(AERE_CHAIN_ID),               // cid, left-padded
                blockHash,                            // from the proof
                bytes32(uint256(uint160(outbox))),    // trusted outbox, left-padded
                EVENT_SIG,                            // topic0
                messageId,                            // topic1 (indexed)
                bytes32(destChainId),                 // topic2 (indexed)
                dataHash                              // keccak(log data)
            )
        );
        if (expected != commitment) revert CommitmentMismatch(expected, commitment);

        // ---- Effects before interaction. ----
        delivered[messageId] = true;
        unchecked {
            deliveryCount += 1;
        }

        emit MessageDelivered(messageId, sender, targetHandler, blockNumber, blockHash);

        // ---- Deliver to the application handler. ----
        IAereMessageHandlerV2(targetHandler).handleAereMessage(AERE_CHAIN_ID, sender, payload);
    }
}
