// 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 AereEVMValidityBatch - on-chain anchor for REAL FULL-EVM validity proofs * of a CONTIGUOUS BATCH of live chain-2800 blocks. * * @notice Sibling of AereEVMValidity. Where AereEVMValidity anchors the proof of a * single re-executed chain-2800 (AERE mainnet, QBFT / Besu) block, this * contract anchors the proof of a CONTIGUOUS RANGE of blocks proven as one * SP1 zkVM Groth16 batch. The guest re-executes every block in * [firstBlock, lastBlock] with revm (the SP1-patched reth 1.9.3 / revm * stack) INSIDE the zkVM, threading each block's recomputed post-state root * into the next block's pre-state, and only produces a valid proof when the * final recomputed state root equals the `header.stateRoot` of the last * block carried in the public values. This is REAL EVM execution (full * opcode set, call frames, SLOAD/SSTORE, LOG, EIP-1559 base-fee burn + * priority-to-coinbase, and the Cancun/Prague/Osaka system calls), not a * bounded toy VM. * * BINDING CAVEAT (READ THIS): this V1 batch anchor attests a SELF-CONSISTENT * execution of a range, NOT that the range is the CANONICAL chain-2800 * history. The public values carry no blockHash and this contract performs * no blockhash() check and NO inter-record continuity, so a genuine SP1 * proof of a FABRICATED range can be recorded and overlapping/forked ranges * can coexist. Do NOT treat a V1 record as canonical. Canonical tip binding * plus gap-free/fork-free continuity lands in AereEVMValidityBatchV2. * * PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact * BATCH guest ELF. It differs from the single-block guest's vkey. Binding * the vkey binds the exact prover program, so a proof for the single-block * program cannot be recorded here and vice-versa. * * PUBLIC-VALUES CONVENTION (exactly 256 bytes = 8 x 32), matching what the * batch guest commits via abi.encode: * 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 prevStateRoot, // parent state root of firstBlock (pre-state, * // NOT cross-checked on-chain in V1) * bytes32 postStateRoot, // self-consistent post-state root of lastBlock * uint256 totalGas, // total gas used across the batch * uint256 numTxns // total transactions executed across the batch * ) * * IMMUTABILITY / TRUST: SP1_VERIFIER and PROGRAM_VKEY are fixed at deploy. * There is NO owner and NO admin: recording is permissionless. Soundness * rests entirely on the SP1 Groth16 proof: a record can only be written for * a (firstBlock, lastBlock, prevStateRoot, postStateRoot) tuple that the zk * proof establishes for PROGRAM_VKEY. * * =========================================================================== * HONEST SCOPE - READ THIS. * =========================================================================== * This proves REAL EVM execution of a SMALL contiguous batch of live chain-2800 * blocks (a handful of transactions across the range). 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 and it records isolated * proven ranges rather than a continuous sequencer chain. No code, UI or claim * built on it should imply otherwise. * =========================================================================== */ contract AereEVMValidityBatch { /// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector). address public immutable SP1_VERIFIER; /// @notice SP1 vkey binding the exact revm-in-zkVM BATCH guest ELF. bytes32 public immutable PROGRAM_VKEY; /// @notice Number of proven batches recorded. uint256 public provenCount; struct ProvenBatch { uint64 firstBlock; uint64 lastBlock; uint32 numBlocks; 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 (informational tip). bytes32 public latestPostRoot; /// @notice The last block of the most recently proven batch (informational tip). uint64 public latestLastBlock; /* --------------------------------- events ------------------------------- */ event BatchProven( uint256 indexed recordId, uint256 indexed firstBlock, uint256 indexed lastBlock, uint256 numBlocks, 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 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))); } /* ----------------------------- record batch ----------------------------- */ /// @notice Verify a full-EVM validity proof for a contiguous batch of chain-2800 /// blocks and, on success, record it. Permissionless. Reverts if the /// proof is not valid for PROGRAM_VKEY, if the proof is for a different /// chain, if the range is malformed, or if that exact range was already /// recorded. /// @param publicValues abi.encode(chainId, firstBlock, lastBlock, numBlocks, /// prevStateRoot, postStateRoot, totalGas, numTxns), 256 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) { // 8 x 32 = 256 bytes. if (publicValues.length != 256) 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 prevStateRoot, bytes32 postStateRoot, uint256 totalGas, uint256 numTxns ) = abi.decode( publicValues, (uint256, uint256, uint256, uint256, 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) one record per exact range. bytes32 key = batchKey(firstBlock, lastBlock); uint256 existing = batchToRecord[key]; if (existing != 0) revert AlreadyProven(firstBlock, lastBlock, existing - 1); // 5) record the proven batch. recordId = provenCount; records[recordId] = ProvenBatch({ firstBlock: uint64(firstBlock), lastBlock: uint64(lastBlock), numBlocks: uint32(numBlocks), 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, 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. Records nothing. function verify( bytes calldata publicValues, bytes calldata proof ) external view returns ( uint256 firstBlock, uint256 lastBlock, uint256 numBlocks, bytes32 prevStateRoot, bytes32 postStateRoot, uint256 totalGas, uint256 numTxns ) { if (publicValues.length != 256) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof); (, firstBlock, lastBlock, numBlocks, prevStateRoot, postStateRoot, totalGas, numTxns) = abi.decode( publicValues, (uint256, uint256, uint256, uint256, 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 prevStateRoot, bytes32 postStateRoot, uint64 totalGas, uint32 numTxns, uint64 at, address prover ) { ProvenBatch memory r = records[recordId]; return ( r.firstBlock, r.lastBlock, r.numBlocks, 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; } }