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

/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
///      NOTHING and REVERTS on an invalid proof.
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereStorageProofVerifier - zk storage-proof coprocessor primitive
 * @notice On-chain anchor for SP1 zkVM proofs that, at a GIVEN state root R, an
 *         account A's storage slot S held value V. The proof is generated by the
 *         `aere-storage-proof-program` guest, which RLP-decodes and hash-chains
 *         the `eth_getProof` account-proof and storage-proof nodes of AERE's REAL
 *         Merkle-Patricia state trie (the same trie committed in each Besu block
 *         header's stateRoot). No Foundation-seeded side tree is involved: the
 *         guest walks account trie stateRoot -> storageRoot, then storage trie
 *         storageRoot -> value, panicking on any node whose keccak256 does not
 *         match its parent reference.
 *
 *         PUBLIC-VALUES CONVENTION (exactly 192 bytes):
 *           abi.encode(
 *             uint256 chainId,      // must equal this chain
 *             uint256 blockNumber,  // METADATA only, see canonicity note
 *             bytes32 stateRoot,    // the root the value was proven against
 *             address account,      // A
 *             bytes32 slot,         // S
 *             bytes32 value         // V (left-padded 32-byte slot value)
 *           )
 *
 *         --------------------------------------------------------------------
 *         HONEST SCOPE / TRUST BOUNDARY (read this).
 *         This contract proves "value V is the content of slot S of account A in
 *         the trie whose root is stateRoot". It does NOT and CANNOT, from the
 *         proof alone, establish that `stateRoot` is the CANONICAL state root of
 *         `blockNumber` on this chain. `blockNumber` is committed as metadata for
 *         the consumer's convenience only.
 *
 *         To be trustless, the CONSUMER must independently establish that
 *         `stateRoot` is canonical. Two protocol-native coprocessor anchors do
 *         this without any keeper or oracle:
 *           - AereStateRootAnchor: recovers the canonical block hash from the
 *             BLOCKHASH opcode (~256 blocks) and checks the RLP header; or
 *           - AereHistoryStateRootAnchor: recovers it from the EIP-2935
 *             history-storage system contract
 *             (0x0000F90827F1C53a10cb7A02335B175320002935, ring buffer written
 *             by consensus each block on the AerePQC / futureEips fork), giving
 *             a ~8191-block reach far beyond BLOCKHASH.
 *         Both call {submitProofWithExpectedRoot} to force the committed
 *         stateRoot to equal a root established canonical on-chain.
 *         Use {submitProofWithExpectedRoot} to force the committed stateRoot to
 *         equal a root the caller already trusts, moving the canonicity check to
 *         the call site. This contract is the coprocessor storage-proof building
 *         block, NOT yet a full historical-state oracle.
 *         --------------------------------------------------------------------
 *
 *         IMMUTABILITY: SP1_VERIFIER and PROGRAM_VKEY are fixed at deploy. There
 *         is no owner and no admin: anyone may submit a valid proof.
 */
contract AereStorageProofVerifier {
    /// @notice SP1VerifierGateway (routes by proof selector to the versioned verifier).
    address public immutable SP1_VERIFIER;

    /// @notice Verification key binding the exact storage-proof guest ELF.
    bytes32 public immutable PROGRAM_VKEY;

    struct ProvenSlot {
        bool exists;
        bytes32 stateRoot;
        bytes32 value;
        uint64 blockNumber;
        uint64 at; // block timestamp when recorded
    }

    /// @notice keccak256(blockNumber, account, slot) => proven slot record.
    mapping(bytes32 => ProvenSlot) public proven;

    event SlotProven(
        address indexed account,
        bytes32 indexed slot,
        uint256 indexed blockNumber,
        bytes32 stateRoot,
        bytes32 value
    );

    error InvalidProof();
    error WrongChain(uint256 inProof, uint256 onChain);
    error BadPublicValuesLength(uint256 got);
    error ZeroAddress();
    error ZeroVKey();
    error StateRootMismatch(bytes32 inProof, bytes32 expected);

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

    function _key(uint256 blockNumber, address account, bytes32 slot) internal pure returns (bytes32) {
        return keccak256(abi.encode(blockNumber, account, slot));
    }

    /// @dev Verify the proof through the gateway (reverts on failure) and decode.
    function _verifyAndDecode(bytes calldata publicValues, bytes calldata proof)
        internal
        view
        returns (
            uint256 chainId,
            uint256 blockNumber,
            bytes32 stateRoot,
            address account,
            bytes32 slot,
            bytes32 value
        )
    {
        // 6 x 32 = 192 bytes for (uint256, uint256, bytes32, address, bytes32, bytes32).
        if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length);

        try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
            // verified - did not revert
        } catch {
            revert InvalidProof();
        }

        (chainId, blockNumber, stateRoot, account, slot, value) =
            abi.decode(publicValues, (uint256, uint256, bytes32, address, bytes32, bytes32));

        if (chainId != block.chainid) revert WrongChain(chainId, block.chainid);
    }

    /// @notice Verify an SP1 storage proof and record (blockNumber, account, slot) => value.
    ///         Reverts if the proof is invalid or the committed chainId is not this chain.
    ///         Does NOT check stateRoot canonicity: see the contract-level note; the
    ///         emitted stateRoot is the root the value was proven against.
    function submitProof(bytes calldata publicValues, bytes calldata proof)
        external
        returns (bytes32 value)
    {
        (
            ,
            uint256 blockNumber,
            bytes32 stateRoot,
            address account,
            bytes32 slot,
            bytes32 v
        ) = _verifyAndDecode(publicValues, proof);

        proven[_key(blockNumber, account, slot)] = ProvenSlot({
            exists: true,
            stateRoot: stateRoot,
            value: v,
            blockNumber: uint64(blockNumber),
            at: uint64(block.timestamp)
        });

        emit SlotProven(account, slot, blockNumber, stateRoot, v);
        return v;
    }

    /// @notice Same as {submitProof} but reverts unless the proof's committed
    ///         stateRoot equals `expectedStateRoot`. The caller is responsible for
    ///         obtaining `expectedStateRoot` from a source it trusts to be canonical
    ///         (recent BLOCKHASH, EIP-2935 history, or a block-hash-anchor oracle),
    ///         which moves the canonicity trust to the call site.
    function submitProofWithExpectedRoot(
        bytes calldata publicValues,
        bytes calldata proof,
        bytes32 expectedStateRoot
    ) external returns (bytes32 value) {
        (
            ,
            uint256 blockNumber,
            bytes32 stateRoot,
            address account,
            bytes32 slot,
            bytes32 v
        ) = _verifyAndDecode(publicValues, proof);

        if (stateRoot != expectedStateRoot) revert StateRootMismatch(stateRoot, expectedStateRoot);

        proven[_key(blockNumber, account, slot)] = ProvenSlot({
            exists: true,
            stateRoot: stateRoot,
            value: v,
            blockNumber: uint64(blockNumber),
            at: uint64(block.timestamp)
        });

        emit SlotProven(account, slot, blockNumber, stateRoot, v);
        return v;
    }

    /// @notice Stateless verification: reverts if the proof is invalid or the
    ///         chainId is wrong, otherwise returns the decoded tuple. Records nothing.
    function verify(bytes calldata publicValues, bytes calldata proof)
        external
        view
        returns (
            uint256 blockNumber,
            bytes32 stateRoot,
            address account,
            bytes32 slot,
            bytes32 value
        )
    {
        (, blockNumber, stateRoot, account, slot, value) = _verifyAndDecode(publicValues, proof);
    }

    /// @notice Read a previously proven slot record.
    function getProven(uint256 blockNumber, address account, bytes32 slot)
        external
        view
        returns (bool exists, bytes32 stateRoot, bytes32 value, uint64 at)
    {
        ProvenSlot memory p = proven[_key(blockNumber, account, slot)];
        return (p.exists, p.stateRoot, p.value, p.at);
    }
}
