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

/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
///      NOTHING and REVERTS on an invalid proof. On chain 2800 the deployed
///      SP1VerifierGateway is 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 and the
///      SP1 Groth16 route is selected by the proof's leading 4-byte selector
///      0x4388a21c (SP1 v6 groth16), exactly as AereEVMValidity uses it.
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereEVMValidityV2 - CANONICAL-BOUND on-chain anchor for REAL FULL-EVM
 *        validity proofs of live chain-2800 blocks.
 *
 * @dev STAGED / DO-NOT-DEPLOY-UNTIL-FORK: this contract, its sibling
 *      AereEVMValidityBatchV2, and the re-proved SP1 guest are STAGED and MUST NOT
 *      be deployed until (1) the guest is re-proved and its new vkey wired into
 *      PROGRAM_VKEY, and (2) the AerePQC futureEips SUPERVISED MAINNET FORK is live
 *      on chain 2800 so EIP-2935 history storage backs canonicalHash() for
 *      older-than-256-block binding. See aerenew/rollup-evm-validity/NOTE.md.
 *
 * @notice Successor to AereEVMValidity. It fixes the canonical-binding gap in V1:
 *         V1 recorded an SP1 postStateRoot as block N with NO blockHash/parentHash
 *         in the public values and NO on-chain blockhash() check, so a genuine SP1
 *         proof of a FABRICATED execution could be anchored as block N and an
 *         attacker could squat slot N via first-write-wins. V2 binds every record
 *         to the real canonical chain.
 *
 *         The guest (bin/aere-client) re-executes a real chain-2800 (AERE mainnet,
 *         QBFT / Besu) block with revm INSIDE the SP1 zkVM, re-roots the sparse
 *         trie, and only produces a valid proof when the recomputed post-state root
 *         equals the block's header.stateRoot. The V2 guest ALSO commits the block's
 *         own blockHash and its parentHash and enforces, inside the circuit, that
 *         prevStateRoot == parent.stateRoot.
 *
 *         PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact
 *         V2 guest ELF. Its vkey differs from the V1 guest's, so a V1 proof cannot
 *         be recorded here and vice-versa.
 *
 *         PUBLIC-VALUES CONVENTION (exactly 288 bytes = 9 x 32):
 *           abi.encode(
 *             uint256 chainId,        // must equal block.chainid (2800)
 *             uint256 blockNumber,    // the proven chain-2800 block height
 *             bytes32 parentHash,     // header.parentHash of the proven block
 *             bytes32 blockHash,      // the proven block's own canonical hash
 *             bytes32 prevStateRoot,  // parent block state root (pre-state anchor)
 *             bytes32 postStateRoot,  // proven post-execution state root
 *             bytes32 txRoot,         // header.transactionsRoot (binds the txs)
 *             uint256 gasUsed,        // total gas used by the block
 *             uint256 numTxns         // number of transactions executed
 *           )
 *
 *         CANONICAL BINDING: recordBlock resolves the on-chain canonical hash of
 *         blockNumber and requires it to equal the committed blockHash. Recent
 *         blocks (within the 256-block EVM window) use blockhash(); older blocks
 *         consult the EIP-2935 history-storage ring at
 *         0x0000F90827F1C53a10cb7A02335B175320002935 (once that fork is live). If
 *         the canonical hash is unavailable, or does not match, the call reverts,
 *         so a fabricated execution can never be anchored as a real block. When the
 *         parent's canonical hash is also resolvable, the committed parentHash is
 *         cross-checked against it too.
 *
 *         IMMUTABILITY / TRUST: SP1_VERIFIER and PROGRAM_VKEY are fixed at deploy.
 *         There is NO owner and NO admin: recording is permissionless. Soundness
 *         rests on the SP1 Groth16 proof AND the on-chain canonical-hash binding.
 *
 * ===========================================================================
 * HONEST SCOPE - READ THIS.
 * ===========================================================================
 *   This proves REAL EVM execution of a SMALL live chain-2800 block (a handful
 *   of transactions) and binds it to the canonical chain via blockhash. It is a
 *   legitimate real-EVM validity PROOF-OF-APPROACH. It is NOT yet a full-throughput
 *   production rollup: it does not prove a large batch, and it records isolated
 *   bound blocks rather than a continuous sequencer chain.
 * ===========================================================================
 */
contract AereEVMValidityV2 {

    /// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector).
    address public immutable SP1_VERIFIER;

    /// @notice SP1 vkey binding the exact V2 revm-in-zkVM guest ELF.
    bytes32 public immutable PROGRAM_VKEY;

    /// @notice EIP-2935 history-storage ring (serves historical block hashes once
    ///         the fork is live; before then this address has no code).
    address internal constant HISTORY_STORAGE_ADDRESS =
        0x0000F90827F1C53a10cb7A02335B175320002935;

    /// @notice Number of proven, canonically-bound blocks recorded.
    uint256 public provenCount;

    struct ProvenBlock {
        uint64  blockNumber;
        bytes32 parentHash;
        bytes32 blockHash;
        bytes32 prevStateRoot;
        bytes32 postStateRoot;
        bytes32 txRoot;
        uint64  gasUsed;
        uint32  numTxns;
        uint64  at;            // block timestamp when the proof verified
        address prover;        // who submitted it
    }

    /// @notice recordId (0-based) => proven block record.
    mapping(uint256 => ProvenBlock) public records;

    /// @notice chain-2800 blockNumber => 1-based recordId (0 == not proven).
    mapping(uint256 => uint256) public blockToRecord;

    /// @notice The most recently proven post-state root (informational tip).
    bytes32 public latestPostRoot;

    /* --------------------------------- events ------------------------------- */

    event BlockProven(
        uint256 indexed recordId,
        uint256 indexed blockNumber,
        bytes32 parentHash,
        bytes32 blockHash,
        bytes32 prevStateRoot,
        bytes32 postStateRoot,
        bytes32 txRoot,
        uint256 gasUsed,
        uint256 numTxns,
        address prover,
        uint256 at
    );

    /* --------------------------------- errors ------------------------------- */

    error InvalidProof();
    error BadPublicValuesLength(uint256 got);
    error WrongChain(uint256 inProof, uint256 onChain);
    error AlreadyProven(uint256 blockNumber, uint256 recordId);
    error BlockHashUnavailable(uint256 blockNumber);
    error BlockHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
    error ParentHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
    error ZeroAddress();
    error ZeroVKey();

    /* ----------------------------- constructor ------------------------------ */

    constructor(address sp1Verifier, bytes32 programVKey) {
        if (sp1Verifier == address(0)) revert ZeroAddress();
        if (programVKey == bytes32(0)) revert ZeroVKey();
        SP1_VERIFIER = sp1Verifier;
        PROGRAM_VKEY = programVKey;
    }

    /* --------------------------- canonical hash ----------------------------- */

    /// @notice Resolve the canonical hash of a past block, or bytes32(0) if it is
    ///         not available. Recent blocks use the 256-deep EVM blockhash window;
    ///         older blocks consult the EIP-2935 history ring (empty before the
    ///         fork is live, which yields bytes32(0) and a safe revert upstream).
    function canonicalHash(uint256 blockNumber) public view returns (bytes32) {
        // No hash exists for the current or a future block.
        if (blockNumber >= block.number) return bytes32(0);

        // Recent window: the EVM exposes the last 256 ancestors directly.
        if (block.number - blockNumber <= 256) {
            return blockhash(blockNumber);
        }

        // Older block: ask the EIP-2935 history-storage ring. Before that fork is
        // live the address has no code; a staticcall then succeeds with empty
        // returndata, so ret.length != 32 and we report "unavailable".
        (bool ok, bytes memory ret) =
            HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber));
        if (!ok || ret.length != 32) return bytes32(0);
        return abi.decode(ret, (bytes32));
    }

    /* ------------------------------ record block ---------------------------- */

    /// @notice Verify a full-EVM validity proof for a chain-2800 block, bind it to
    ///         the canonical chain, and record it. Permissionless. Reverts if the
    ///         proof is not valid for PROGRAM_VKEY, if it is for a different chain,
    ///         if the block's canonical hash is unavailable or does not match the
    ///         committed blockHash, or if that block number was already recorded.
    /// @param publicValues abi.encode(chainId, blockNumber, parentHash, blockHash,
    ///                     prevStateRoot, postStateRoot, txRoot, gasUsed, numTxns),
    ///                     288 bytes.
    /// @param proof        SP1 Groth16 proof bytes (leading selector 0x4388a21c).
    /// @return recordId    the 0-based id of the recorded proven block.
    function recordBlock(
        bytes calldata publicValues,
        bytes calldata proof
    ) external returns (uint256 recordId) {
        // 9 x 32 = 288 bytes.
        if (publicValues.length != 288) revert BadPublicValuesLength(publicValues.length);

        // 1) zk proof: reverts if the proof is not valid for PROGRAM_VKEY.
        try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
            // verified - did not revert
        } catch {
            revert InvalidProof();
        }

        (
            uint256 chainId,
            uint256 blockNumber,
            bytes32 parentHash,
            bytes32 blockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            bytes32 txRoot,
            uint256 gasUsed,
            uint256 numTxns
        ) = abi.decode(
            publicValues,
            (uint256, uint256, bytes32, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
        );

        // 2) bind to this chain.
        if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);

        // 3) CANONICAL BINDING: the committed blockHash must equal the on-chain
        //    canonical hash of blockNumber. This is what makes the recorded root
        //    canonical rather than merely self-consistent.
        bytes32 onChainHash = canonicalHash(blockNumber);
        if (onChainHash == bytes32(0)) revert BlockHashUnavailable(blockNumber);
        if (onChainHash != blockHash) {
            revert BlockHashMismatch(blockNumber, onChainHash, blockHash);
        }

        // 3b) When the parent's canonical hash is also resolvable, cross-check the
        //     committed parentHash against it. This closes fabricated-parent tricks
        //     without ever blocking a legitimate in-window record.
        if (blockNumber > 0) {
            bytes32 onChainParent = canonicalHash(blockNumber - 1);
            if (onChainParent != bytes32(0) && onChainParent != parentHash) {
                revert ParentHashMismatch(blockNumber, onChainParent, parentHash);
            }
        }

        // 4) one record per block number.
        uint256 existing = blockToRecord[blockNumber];
        if (existing != 0) revert AlreadyProven(blockNumber, existing - 1);

        // 5) record the proven, canonically-bound block.
        recordId = provenCount;
        records[recordId] = ProvenBlock({
            blockNumber: uint64(blockNumber),
            parentHash: parentHash,
            blockHash: blockHash,
            prevStateRoot: prevStateRoot,
            postStateRoot: postStateRoot,
            txRoot: txRoot,
            gasUsed: uint64(gasUsed),
            numTxns: uint32(numTxns),
            at: uint64(block.timestamp),
            prover: msg.sender
        });
        blockToRecord[blockNumber] = recordId + 1; // 1-based so 0 means "unseen"
        latestPostRoot = postStateRoot;
        provenCount = recordId + 1;

        emit BlockProven(
            recordId, blockNumber, parentHash, blockHash, prevStateRoot,
            postStateRoot, txRoot, gasUsed, numTxns, msg.sender, block.timestamp
        );
    }

    /* ----------------------------------- views ------------------------------ */

    /// @notice Stateless verification: reverts if the proof is invalid for
    ///         PROGRAM_VKEY, otherwise returns the decoded fields. Does NOT check
    ///         the canonical blockhash and records nothing; recordBlock is the
    ///         canonical-bound path.
    function verify(
        bytes calldata publicValues,
        bytes calldata proof
    )
        external
        view
        returns (
            uint256 blockNumber,
            bytes32 parentHash,
            bytes32 blockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            bytes32 txRoot,
            uint256 gasUsed,
            uint256 numTxns
        )
    {
        if (publicValues.length != 288) revert BadPublicValuesLength(publicValues.length);
        ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
        (
            , blockNumber, parentHash, blockHash, prevStateRoot,
            postStateRoot, txRoot, gasUsed, numTxns
        ) = abi.decode(
            publicValues,
            (uint256, uint256, bytes32, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
        );
    }

    /// @notice Convenience accessor for a recorded proven block.
    function getRecord(uint256 recordId)
        external
        view
        returns (
            uint64 blockNumber,
            bytes32 parentHash,
            bytes32 blockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            bytes32 txRoot,
            uint64 gasUsed,
            uint32 numTxns,
            uint64 at,
            address prover
        )
    {
        ProvenBlock memory r = records[recordId];
        return (
            r.blockNumber, r.parentHash, r.blockHash, r.prevStateRoot,
            r.postStateRoot, r.txRoot, r.gasUsed, r.numTxns, r.at, r.prover
        );
    }
}
