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