// 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. `AereOutboundVerifier` calls
///         this once, and only once, per proven+unused message.
interface IAereMessageHandler {
    /// @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 AereOutboundVerifier
/// @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 AERE finality
///         vkey — delivers the message to its target handler exactly once.
///
/// @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 (informational)
///           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)
///           bool    finalized,      // must be true
///           bool    falconVerified  // must be false (mainnet consensus is ECDSA)
///         )
///         All eight fields are static 32-byte words, so this is a plain 256-byte
///         concatenation — identical to `alloy` `SolType::abi_encode` of the
///         guest's `PublicValues` struct.
///
///         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.
///   * A valid proof attests only that a `>= quorum` of AERE's 5 QBFT validators
///     sealed the source block. Delivery security == that validator set. It is a
///     zk light-client bridge, not a magic-trustless one.
///   * Those validator seals are classical ECDSA today and thus QUANTUM-VULNERABLE.
///     A Falcon seal slot is reserved but inactive; `falconVerified` is required to
///     be `false` so this contract can never launder an unbacked post-quantum claim.
///   * The SP1 Groth16 wrapper is over BN254, which is ALSO quantum-vulnerable.
///   * This is the honest replacement for the earlier flawed/inert multisig bridge:
///     it removes the bridge signers, it does not remove trust in AERE consensus.
contract AereOutboundVerifier {
    /// @notice AERE mainnet chain id the pinned vkey attests finality for.
    uint256 public constant AERE_CHAIN_ID = 2800;

    /// @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 program vkey. A proof for any other
    ///         program simply fails `sp1Verifier.verifyProof`. Immutable.
    bytes32 public immutable programVKey;

    /// @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;

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

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

    /// @param _sp1Verifier the SP1 verifier / gateway on this destination chain.
    /// @param _programVKey the AERE QBFT-finality guest vkey to pin.
    /// @param _outbox      the AERE-side `AereOutboundOutbox` address to trust.
    constructor(ISP1Verifier _sp1Verifier, bytes32 _programVKey, address _outbox) {
        require(address(_sp1Verifier) != address(0), "verifier: zero sp1");
        require(_outbox != address(0), "verifier: zero outbox");
        sp1Verifier = _sp1Verifier;
        programVKey = _programVKey;
        outbox = _outbox;
    }

    /// @notice Verify an AERE outbound-message proof and deliver it exactly once.
    /// @param publicValues the guest's ABI-encoded committed public values.
    /// @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 {
        (
            uint64 chainId,
            uint64 blockNumber,
            bytes32 blockHash,
            , // root (informational; bound into the proof, not re-checked here)
            uint8 claimKind,
            bytes32 commitment,
            bool finalized,
            bool falconVerified
        ) = abi.decode(
                publicValues,
                (uint64, uint64, bytes32, bytes32, uint8, bytes32, bool, bool)
            );

        // ---- Cheap public-input gates (before the expensive proof check). ----
        if (chainId != AERE_CHAIN_ID) revert WrongSourceChain(chainId);
        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);

        // ---- The zk check: proof valid for (pinned vkey, these publicValues). ----
        // Reverts if the proof is not a genuine QBFT-finality+inclusion proof, or
        // if `publicValues` were tampered (the commitment would no longer match a
        // provable statement), or if it was produced for a different program vkey.
        sp1Verifier.verifyProof(programVKey, publicValues, proofBytes);

        // ---- Bind the proven commitment to the delivered message fields. ----
        bytes32 messageId = keccak256(
            abi.encode(AERE_CHAIN_ID, destChainId, nonce, sender, targetHandler, payload)
        );
        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);

        // ---- Replay protection (effects before interaction). ----
        if (delivered[messageId]) revert AlreadyDelivered(messageId);
        delivered[messageId] = true;

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

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