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