// 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 AereEVMValidityBatch uses it.
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereEVMValidityBatchV2 - CANONICAL-BOUND, CONTINUITY-ENFORCED anchor for
 *        REAL FULL-EVM validity proofs of a CONTIGUOUS BATCH of chain-2800 blocks.
 *
 * @dev STAGED / DO-NOT-DEPLOY-UNTIL-FORK: this contract, its sibling
 *      AereEVMValidityV2, and the re-proved SP1 batch guest are STAGED and MUST NOT
 *      be deployed until (1) the batch 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 tip binding. See aerenew/rollup-evm-validity/NOTE.md.
 *
 * @notice Successor to AereEVMValidityBatch. It fixes two canonical-binding gaps in
 *         V1: (a) the recorded batch tip was never bound to the real chain (no
 *         blockHash, no blockhash() check), and (b) there was no continuity between
 *         records, so an attacker could anchor overlapping or fabricated ranges.
 *
 *         The V2 batch guest re-executes every block in [firstBlock, lastBlock] with
 *         revm INSIDE the SP1 zkVM, threading each block's recomputed post-state root
 *         into the next block's pre-state, enforces prevStateRoot == parent.stateRoot
 *         at the range boundary inside the circuit, and commits the batch tip's
 *         blockHash plus firstBlock's parentHash. A valid proof exists only when the
 *         final recomputed state root equals the canonical header.stateRoot of the
 *         last block.
 *
 *         PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact V2
 *         BATCH guest ELF. It differs from the V1 batch guest and from the
 *         single-block guest, so proofs are not interchangeable.
 *
 *         PUBLIC-VALUES CONVENTION (exactly 320 bytes = 10 x 32):
 *           abi.encode(
 *             uint256 chainId,          // must equal block.chainid (2800)
 *             uint256 firstBlock,       // first proven chain-2800 block height
 *             uint256 lastBlock,        // last proven chain-2800 block height
 *             uint256 numBlocks,        // count of blocks in the batch
 *             bytes32 firstParentHash,  // header.parentHash of firstBlock
 *             bytes32 lastBlockHash,    // canonical hash of lastBlock (the tip)
 *             bytes32 prevStateRoot,    // parent state root of firstBlock
 *             bytes32 postStateRoot,    // proven post-state root of lastBlock
 *             uint256 totalGas,         // total gas used across the batch
 *             uint256 numTxns           // total transactions executed across the batch
 *           )
 *
 *         CANONICAL BINDING: recordBatch resolves the on-chain canonical hash of
 *         lastBlock and requires it to equal the committed lastBlockHash. Recent
 *         blocks use the 256-deep EVM blockhash window; older blocks consult the
 *         EIP-2935 history ring at 0x0000F90827F1C53a10cb7A02335B175320002935 (once
 *         that fork is live). If the tip hash is unavailable or mismatched, the call
 *         reverts, so a fabricated range can never be anchored.
 *
 *         CONTINUITY: once at least one batch is recorded, every subsequent batch
 *         must satisfy firstBlock == priorBatch.lastBlock + 1 AND prevStateRoot ==
 *         priorBatch.postStateRoot. This forms a single, gap-free, fork-free chain
 *         of proven ranges (the first recorded batch anchors the chain).
 *
 *         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, the canonical tip binding, and continuity.
 *
 * ===========================================================================
 * HONEST SCOPE - READ THIS.
 * ===========================================================================
 *   This proves REAL EVM execution of a SMALL contiguous batch of live chain-2800
 *   blocks and binds the tip to the canonical chain. It is a legitimate real-EVM
 *   validity PROOF-OF-APPROACH for batched blocks. It is NOT yet a full-throughput
 *   production rollup: batch size is small.
 * ===========================================================================
 */
contract AereEVMValidityBatchV2 {

    /// @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 BATCH 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 batches recorded.
    uint256 public provenCount;

    struct ProvenBatch {
        uint64  firstBlock;
        uint64  lastBlock;
        uint32  numBlocks;
        bytes32 firstParentHash;
        bytes32 lastBlockHash;
        bytes32 prevStateRoot;
        bytes32 postStateRoot;
        uint64  totalGas;
        uint32  numTxns;
        uint64  at;            // block timestamp when the proof verified
        address prover;        // who submitted it
    }

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

    /// @notice keccak256(firstBlock, lastBlock) => 1-based recordId (0 == unseen).
    mapping(bytes32 => uint256) public batchToRecord;

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

    /// @notice The last block of the most recently proven batch (continuity anchor).
    uint64 public latestLastBlock;

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

    event BatchProven(
        uint256 indexed recordId,
        uint256 indexed firstBlock,
        uint256 indexed lastBlock,
        uint256 numBlocks,
        bytes32 firstParentHash,
        bytes32 lastBlockHash,
        bytes32 prevStateRoot,
        bytes32 postStateRoot,
        uint256 totalGas,
        uint256 numTxns,
        address prover,
        uint256 at
    );

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

    error InvalidProof();
    error BadPublicValuesLength(uint256 got);
    error WrongChain(uint256 inProof, uint256 onChain);
    error BadRange(uint256 firstBlock, uint256 lastBlock, uint256 numBlocks);
    error AlreadyProven(uint256 firstBlock, uint256 lastBlock, uint256 recordId);
    error BlockHashUnavailable(uint256 blockNumber);
    error BlockHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed);
    error Discontinuous(uint256 gotFirstBlock, uint256 wantFirstBlock, bytes32 gotPrevRoot, bytes32 wantPrevRoot);
    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;
    }

    /* ------------------------------ batch key ------------------------------- */

    /// @notice Deterministic dedupe key for a contiguous batch range.
    function batchKey(uint256 firstBlock, uint256 lastBlock) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(uint64(firstBlock), uint64(lastBlock)));
    }

    /* --------------------------- 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) {
        if (blockNumber >= block.number) return bytes32(0);
        if (block.number - blockNumber <= 256) {
            return blockhash(blockNumber);
        }
        (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 batch ----------------------------- */

    /// @notice Verify a full-EVM validity proof for a contiguous batch of chain-2800
    ///         blocks, bind the tip to the canonical chain, enforce continuity with
    ///         the previously recorded batch, and record it. Permissionless.
    /// @param publicValues abi.encode(chainId, firstBlock, lastBlock, numBlocks,
    ///                     firstParentHash, lastBlockHash, prevStateRoot,
    ///                     postStateRoot, totalGas, numTxns), 320 bytes.
    /// @param proof        SP1 Groth16 proof bytes (leading selector 0x4388a21c).
    /// @return recordId    the 0-based id of the recorded proven batch.
    function recordBatch(
        bytes calldata publicValues,
        bytes calldata proof
    ) external returns (uint256 recordId) {
        // 10 x 32 = 320 bytes.
        if (publicValues.length != 320) 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 firstBlock,
            uint256 lastBlock,
            uint256 numBlocks,
            bytes32 firstParentHash,
            bytes32 lastBlockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            uint256 totalGas,
            uint256 numTxns
        ) = abi.decode(
            publicValues,
            (uint256, uint256, uint256, uint256, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
        );

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

        // 3) contiguity invariant guaranteed by the batch guest.
        if (lastBlock < firstBlock || numBlocks != lastBlock - firstBlock + 1) {
            revert BadRange(firstBlock, lastBlock, numBlocks);
        }

        // 4) CANONICAL TIP BINDING: committed lastBlockHash must equal the on-chain
        //    canonical hash of lastBlock.
        bytes32 tipHash = canonicalHash(lastBlock);
        if (tipHash == bytes32(0)) revert BlockHashUnavailable(lastBlock);
        if (tipHash != lastBlockHash) {
            revert BlockHashMismatch(lastBlock, tipHash, lastBlockHash);
        }

        // 5) CONTINUITY: after the first record, ranges must chain gap-free and the
        //    pre-state must equal the prior batch's proven post-state.
        if (provenCount != 0) {
            uint256 wantFirst = uint256(latestLastBlock) + 1;
            if (firstBlock != wantFirst || prevStateRoot != latestPostRoot) {
                revert Discontinuous(firstBlock, wantFirst, prevStateRoot, latestPostRoot);
            }
        }

        // 6) one record per exact range.
        bytes32 key = batchKey(firstBlock, lastBlock);
        uint256 existing = batchToRecord[key];
        if (existing != 0) revert AlreadyProven(firstBlock, lastBlock, existing - 1);

        // 7) record the proven, canonically-bound batch.
        recordId = provenCount;
        records[recordId] = ProvenBatch({
            firstBlock: uint64(firstBlock),
            lastBlock: uint64(lastBlock),
            numBlocks: uint32(numBlocks),
            firstParentHash: firstParentHash,
            lastBlockHash: lastBlockHash,
            prevStateRoot: prevStateRoot,
            postStateRoot: postStateRoot,
            totalGas: uint64(totalGas),
            numTxns: uint32(numTxns),
            at: uint64(block.timestamp),
            prover: msg.sender
        });
        batchToRecord[key] = recordId + 1; // 1-based so 0 means "unseen"
        latestPostRoot = postStateRoot;
        latestLastBlock = uint64(lastBlock);
        provenCount = recordId + 1;

        emit BatchProven(
            recordId, firstBlock, lastBlock, numBlocks, firstParentHash,
            lastBlockHash, prevStateRoot, postStateRoot, totalGas, 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 or continuity and records nothing.
    function verify(
        bytes calldata publicValues,
        bytes calldata proof
    )
        external
        view
        returns (
            uint256 firstBlock,
            uint256 lastBlock,
            uint256 numBlocks,
            bytes32 firstParentHash,
            bytes32 lastBlockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            uint256 totalGas,
            uint256 numTxns
        )
    {
        if (publicValues.length != 320) revert BadPublicValuesLength(publicValues.length);
        ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof);
        (
            , firstBlock, lastBlock, numBlocks, firstParentHash, lastBlockHash,
            prevStateRoot, postStateRoot, totalGas, numTxns
        ) = abi.decode(
            publicValues,
            (uint256, uint256, uint256, uint256, bytes32, bytes32, bytes32, bytes32, uint256, uint256)
        );
    }

    /// @notice Convenience accessor for a recorded proven batch.
    function getRecord(uint256 recordId)
        external
        view
        returns (
            uint64 firstBlock,
            uint64 lastBlock,
            uint32 numBlocks,
            bytes32 firstParentHash,
            bytes32 lastBlockHash,
            bytes32 prevStateRoot,
            bytes32 postStateRoot,
            uint64 totalGas,
            uint32 numTxns,
            uint64 at,
            address prover
        )
    {
        ProvenBatch memory r = records[recordId];
        return (
            r.firstBlock, r.lastBlock, r.numBlocks, r.firstParentHash,
            r.lastBlockHash, r.prevStateRoot, r.postStateRoot, r.totalGas,
            r.numTxns, r.at, r.prover
        );
    }

    /// @notice recordId for an exact range, or reverts if that range is unseen.
    function recordIdForRange(uint256 firstBlock, uint256 lastBlock)
        external
        view
        returns (uint256 recordId)
    {
        uint256 existing = batchToRecord[batchKey(firstBlock, lastBlock)];
        require(existing != 0, "range not proven");
        return existing - 1;
    }
}
