// 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 AereStorageProofVerifierV2 — zk storage-proof coprocessor primitive.
 *         CORRECTED fork of AereStorageProofVerifier 0xF9a1A183…9362E (see THE FIX below).
 *
 * @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 (UNCHANGED — same ELF, same vkey), 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) — IDENTICAL to V1, guest UNCHANGED:
 *           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)
 *           )
 *
 * @dev    ------------------------------------------------------------------
 *         THE FIX (vs V1 0xF9a1A183…9362E) — RECORD POISONING ACROSS TRUST DOMAINS.
 *
 *         KNOWN AND ALREADY DOCUMENTED (not the bug): this contract proves V against a GIVEN,
 *         prover-supplied stateRoot and CANNOT itself show that root is canonical. That is the
 *         honest, intended scope of a storage-proof primitive.
 *
 *         THE ACTUAL BUG: V1 wrote EVERY submission — canonically-anchored or not — into ONE
 *         shared mapping keyed only by (blockNumber, account, slot):
 *             proven[keccak256(blockNumber, account, slot)] = ProvenSlot({...});
 *         So a record established through the trust-minimized path (AereStateRootAnchor
 *         .proveAgainstAnchor, which forces the proof's root to equal a root recovered from the
 *         BLOCKHASH opcode) could be SILENTLY OVERWRITTEN by anyone calling the unbound
 *         `submitProof` with a proof against a state root of their OWN making for the SAME key.
 *
 *         THIS IS NOT THEORETICAL. The live V1 holds a genuinely anchored record —
 *         AereTreasury 0x687933…6119 slot 0 (owner) == Foundation 0x0243A4…f3C3 at block
 *         8,915,939, against the canonical stateRoot 0x515fce04…4a92f3 (== the real
 *         eth_getBlockByNumber(8915939).stateRoot). An attacker builds a trie of their own
 *         where that slot holds THEIR address, proves it honestly against their own root (the
 *         guest cannot know which root is canonical — that is precisely its documented scope),
 *         and calls submitProof for the same (8915939, AereTreasury, slot 0). V1 overwrites
 *         the anchored record in place. Every downstream reader of getProven() then sees the
 *         attacker as the Treasury owner, with `exists == true` and no way to tell.
 *
 *         THE FIX — SEPARATE THE TRUST DOMAINS. Anchored and unbound records now live in
 *         DIFFERENT mappings and can never collide:
 *           * ANCHORED records: key = (blockNumber, account, slot). WRITABLE ONLY by the
 *             IMMUTABLE CANONICAL_ANCHOR, whose own logic forces the proof's committed root to
 *             equal a root it recovered from consensus (BLOCKHASH). This is the canonical domain.
 *           * UNBOUND records: key = (SUBMITTER, blockNumber, account, slot). Every submitter
 *             gets their OWN namespace, so an unbound submission cannot overwrite the anchored
 *             record NOR another submitter's record. Records are tagged Provenance.Unbound.
 *         `getProven(blockNumber, account, slot)` — the V1-shaped read — now returns the
 *         ANCHORED record ONLY. Any consumer that read V1's getProven and could be poisoned
 *         reads canonical-or-nothing on V2: FAIL-CLOSED, not fail-quiet.
 *
 *         THE UNBOUND API IS NOT DROPPED — it has legitimate uses (a caller who obtained a root
 *         from a source it trusts, off-chain indexers, and the anchor's own two-call pattern).
 *         It is simply made clearly NON-CANONICAL and NON-OVERWRITING.
 *
 *         WHY PROVENANCE IS DECIDED BY THE CALLER, NOT THE FUNCTION. Note that
 *         `submitProofWithExpectedRoot` is NOT self-securing: the CALLER supplies
 *         `expectedStateRoot`, so an attacker simply passes their own fabricated root and the
 *         check passes trivially. Canonicity comes from WHO is calling — the anchor derives its
 *         expected root from the BLOCKHASH opcode. Hence V2 grants Provenance.Anchored strictly
 *         to CANONICAL_ANCHOR and to nobody else.
 *         ------------------------------------------------------------------
 *
 * @dev    CANONICITY SOURCE ON CHAIN 2800. CANONICAL_ANCHOR is an AereStateRootAnchor
 *         (BLOCKHASH-derived, 256-block reach). EIP-2935 history storage
 *         (0x0000F90827F1C53a10cb7A02335B175320002935) is NOT deployed on chain 2800 today, so
 *         the ~8191-block AereHistoryStateRootAnchor path is inert here. CANONICAL_ANCHOR is a
 *         SINGLE immutable address: adding the EIP-2935 anchor as a second canonical writer,
 *         once that contract is live on 2800, requires a fresh deployment of this verifier.
 *         Stated plainly — same expiring-credential discipline as the light-client anchors, not
 *         a silent gap.
 *
 * @dev    HONEST SCOPE / RESIDUAL TRUST — read before quoting.
 *         * ANCHORED records are trust-minimized: the root came from the BLOCKHASH opcode, never
 *           from a caller argument. Their reach is the anchor's 256-block window at anchoring
 *           time; once anchored the record is permanent.
 *         * UNBOUND records are NOT canonical and must never be read as if they were. They say
 *           only: "submitter X proved value V against a root X chose". That is a real statement
 *           and sometimes a useful one — it is not a statement about chain 2800's history.
 *         * This contract does not make the unbound path trustless; it makes it HARMLESS to the
 *           canonical path, which is what was broken.
 *         * NOT QUANTUM-SAFE, TRUST-MINIMIZED NOT TRUSTLESS: SP1 Groth16 over BN254 is classical.
 *           APPLICATION layer only; does not touch AERE consensus (still classical ECDSA QBFT).
 *
 *         IMMUTABILITY: SP1_VERIFIER, PROGRAM_VKEY and CANONICAL_ANCHOR are fixed at deploy.
 *         There is no owner and no admin: anyone may submit a valid proof.
 */
contract AereStorageProofVerifierV2 {
    /// @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;

    /// @notice The ONLY contract whose submissions earn Provenance.Anchored. An
    ///         AereStateRootAnchor: it recovers the canonical block hash from the BLOCKHASH
    ///         opcode, checks the RLP header against it, and forces the proof's committed
    ///         stateRoot to equal the header's. IMMUTABLE.
    address public immutable CANONICAL_ANCHOR;

    /// @notice Trust domain of a record. None = absent.
    enum Provenance {
        None,
        Unbound,
        Anchored
    }

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

    /// @notice CANONICAL domain. key = keccak256(blockNumber, account, slot).
    ///         Writable ONLY via CANONICAL_ANCHOR. Cannot be touched by the unbound path.
    mapping(bytes32 => ProvenSlot) private _anchoredRecords;

    /// @notice NON-CANONICAL domain. key = keccak256(submitter, blockNumber, account, slot).
    ///         Each submitter has their own namespace: no cross-writes, ever.
    mapping(bytes32 => ProvenSlot) private _unboundRecords;

    /// @notice Emitted for a trust-minimized record written through CANONICAL_ANCHOR.
    event SlotProvenAnchored(
        address indexed account,
        bytes32 indexed slot,
        uint256 indexed blockNumber,
        bytes32 stateRoot,
        bytes32 value
    );

    /// @notice Emitted for a NON-CANONICAL record. `submitter` is part of the identity of the
    ///         record: it is namespaced under them and overwrites nothing else.
    event SlotProvenUnbound(
        address indexed account,
        bytes32 indexed slot,
        uint256 indexed blockNumber,
        address submitter,
        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);
    error VerifierHasNoCode();
    error NotAnchored(uint256 blockNumber, address account, bytes32 slot);
    /// @notice An anchored record already exists for this key with a DIFFERENT canonical root.
    ///         Impossible for a correct anchor (a block has exactly one canonical state root);
    ///         asserted so a broken anchor fails loudly instead of silently rewriting history.
    error AnchoredRootConflict(bytes32 existing, bytes32 incoming);

    constructor(address sp1Verifier, bytes32 programVKey, address canonicalAnchor) {
        if (sp1Verifier == address(0) || canonicalAnchor == 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 (sp1Verifier.code.length == 0) revert VerifierHasNoCode();
        if (programVKey == bytes32(0)) revert ZeroVKey();
        SP1_VERIFIER = sp1Verifier;
        PROGRAM_VKEY = programVKey;
        // NOTE: CANONICAL_ANCHOR is NOT code-checked here. The anchor's constructor takes this
        // verifier's address, so the two are mutually referential and the anchor cannot exist
        // yet at this point. The deploy script predicts the anchor's CREATE address, deploys it
        // immediately after, and then VERIFIES BOTH DIRECTIONS on chain
        // (anchor.VERIFIER() == this && this.CANONICAL_ANCHOR() == anchor) before the pair is
        // published. See scripts/deploy-storage-proof-verifier-v2.js.
        CANONICAL_ANCHOR = canonicalAnchor;
    }

    /* ------------------------------------ keys ------------------------------------- */

    /// @dev Canonical-domain key: identity is the chain position, not the submitter.
    function _key(uint256 blockNumber, address account, bytes32 slot) internal pure returns (bytes32) {
        return keccak256(abi.encode(blockNumber, account, slot));
    }

    /// @dev Unbound-domain key: the SUBMITTER is part of the identity, which is exactly what
    ///      stops an unbound submission from landing on top of anyone else's record.
    function _unboundKey(address submitter, uint256 blockNumber, address account, bytes32 slot)
        internal
        pure
        returns (bytes32)
    {
        return keccak256(abi.encode(submitter, blockNumber, account, slot));
    }

    /* ---------------------------------- verify ------------------------------------- */

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

    /* ---------------------------------- writes ------------------------------------- */

    /// @notice Verify an SP1 storage proof and record it in the CALLER'S OWN non-canonical
    ///         namespace. Reverts if the proof is invalid or the committed chainId is wrong.
    /// @dev    Does NOT check stateRoot canonicity — see the contract-level note. The resulting
    ///         record is tagged Provenance.Unbound, is keyed under msg.sender, and CANNOT
    ///         overwrite an anchored record or another submitter's record (the V2 fix).
    ///         Read it with {getUnboundProven}; it is NOT visible through {getProven}.
    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);

        _writeUnbound(blockNumber, stateRoot, account, slot, v);
        return v;
    }

    /// @notice Same as {submitProof} but reverts unless the proof's committed stateRoot equals
    ///         `expectedStateRoot`, moving the canonicity check to the call site.
    /// @dev    PROVENANCE: if msg.sender is CANONICAL_ANCHOR, the record is written to the
    ///         canonical domain (the anchor derived `expectedStateRoot` from the BLOCKHASH
    ///         opcode). For ANY OTHER caller the expected root is just a value they chose, so
    ///         the record goes to their own unbound namespace. This asymmetry is the point:
    ///         the function cannot tell a canonical root from a fabricated one — the CALLER can.
    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);

        if (msg.sender == CANONICAL_ANCHOR) {
            _writeAnchored(blockNumber, stateRoot, account, slot, v);
        } else {
            _writeUnbound(blockNumber, stateRoot, account, slot, v);
        }
        return v;
    }

    function _writeAnchored(
        uint256 blockNumber,
        bytes32 stateRoot,
        address account,
        bytes32 slot,
        bytes32 v
    ) internal {
        bytes32 k = _key(blockNumber, account, slot);
        ProvenSlot storage existing = _anchoredRecords[k];
        // A block has exactly one canonical state root, so a re-anchor must agree. If it does
        // not, the anchor is broken: fail loudly rather than rewrite canonical history.
        if (existing.exists && existing.stateRoot != stateRoot) {
            revert AnchoredRootConflict(existing.stateRoot, stateRoot);
        }
        _anchoredRecords[k] = ProvenSlot({
            exists: true,
            stateRoot: stateRoot,
            value: v,
            blockNumber: uint64(blockNumber),
            at: uint64(block.timestamp),
            provenance: Provenance.Anchored,
            submitter: msg.sender
        });
        emit SlotProvenAnchored(account, slot, blockNumber, stateRoot, v);
    }

    function _writeUnbound(
        uint256 blockNumber,
        bytes32 stateRoot,
        address account,
        bytes32 slot,
        bytes32 v
    ) internal {
        _unboundRecords[_unboundKey(msg.sender, blockNumber, account, slot)] = ProvenSlot({
            exists: true,
            stateRoot: stateRoot,
            value: v,
            blockNumber: uint64(blockNumber),
            at: uint64(block.timestamp),
            provenance: Provenance.Unbound,
            submitter: msg.sender
        });
        emit SlotProvenUnbound(account, slot, blockNumber, msg.sender, stateRoot, v);
    }

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

    /// @notice Stateless verification: reverts if the proof is invalid or the chainId is wrong,
    ///         otherwise returns the decoded tuple. Records nothing.
    /// @dev    UNCHANGED from V1 and INTENTIONALLY unbound: this is the primitive the anchor
    ///         itself calls to read a proof's contents BEFORE deciding whether its root is
    ///         canonical. Its return value is a statement about the proof, not about chain
    ///         history — a consumer must not treat it as a canonical read. The canonical read
    ///         is {getProven}.
    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 CANONICAL read: the anchored record for (blockNumber, account, slot), or
    ///         exists == false. Unbound submissions are NOT visible here and can never
    ///         influence it. This is the V1-shaped read, now fail-closed.
    function getProven(uint256 blockNumber, address account, bytes32 slot)
        external
        view
        returns (bool exists, bytes32 stateRoot, bytes32 value, uint64 at)
    {
        ProvenSlot memory p = _anchoredRecords[_key(blockNumber, account, slot)];
        return (p.exists, p.stateRoot, p.value, p.at);
    }

    /// @notice CANONICAL read with full detail (explicitly named).
    function getAnchoredProven(uint256 blockNumber, address account, bytes32 slot)
        external
        view
        returns (bool exists, bytes32 stateRoot, bytes32 value, uint64 blockNum, uint64 at, address submitter)
    {
        ProvenSlot memory p = _anchoredRecords[_key(blockNumber, account, slot)];
        return (p.exists, p.stateRoot, p.value, p.blockNumber, p.at, p.submitter);
    }

    /// @notice NON-CANONICAL read: what `submitter` proved against a root THEY chose.
    ///         Never treat this as chain history.
    function getUnboundProven(address submitter, uint256 blockNumber, address account, bytes32 slot)
        external
        view
        returns (bool exists, bytes32 stateRoot, bytes32 value, uint64 blockNum, uint64 at)
    {
        ProvenSlot memory p = _unboundRecords[_unboundKey(submitter, blockNumber, account, slot)];
        return (p.exists, p.stateRoot, p.value, p.blockNumber, p.at);
    }

    /// @notice Provenance of the canonical-domain record for this key: Anchored or None.
    function provenanceOf(uint256 blockNumber, address account, bytes32 slot) external view returns (Provenance) {
        return _anchoredRecords[_key(blockNumber, account, slot)].provenance;
    }

    /// @notice True iff a trust-minimized (anchored) record exists for this key.
    function isAnchoredProven(uint256 blockNumber, address account, bytes32 slot) external view returns (bool) {
        return _anchoredRecords[_key(blockNumber, account, slot)].exists;
    }

    /// @notice Canonical value, or revert. For consumers that must never read a missing or
    ///         non-canonical record by accident.
    function anchoredValue(uint256 blockNumber, address account, bytes32 slot) external view returns (bytes32) {
        ProvenSlot memory p = _anchoredRecords[_key(blockNumber, account, slot)];
        if (!p.exists) revert NotAnchored(blockNumber, account, slot);
        return p.value;
    }
}
