// 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 AereEVMValidityBatch uses it. interface ISp1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view; } /** * @title AereEVMValidityBatchV2 - CANONICAL-BOUND, CONTINUITY-ENFORCED anchor for * REAL FULL-EVM validity proofs of a CONTIGUOUS BATCH of chain-2800 blocks. * * @dev STAGED / DO-NOT-DEPLOY-UNTIL-FORK: this contract, its sibling * AereEVMValidityV2, and the re-proved SP1 batch guest are STAGED and MUST NOT * be deployed until (1) the batch 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 tip binding. See aerenew/rollup-evm-validity/NOTE.md. * * @notice Successor to AereEVMValidityBatch. It fixes two canonical-binding gaps in * V1: (a) the recorded batch tip was never bound to the real chain (no * blockHash, no blockhash() check), and (b) there was no continuity between * records, so an attacker could anchor overlapping or fabricated ranges. * * The V2 batch guest re-executes every block in [firstBlock, lastBlock] with * revm INSIDE the SP1 zkVM, threading each block's recomputed post-state root * into the next block's pre-state, enforces prevStateRoot == parent.stateRoot * at the range boundary inside the circuit, and commits the batch tip's * blockHash plus firstBlock's parentHash. A valid proof exists only when the * final recomputed state root equals the canonical header.stateRoot of the * last block. * * PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact V2 * BATCH guest ELF. It differs from the V1 batch guest and from the * single-block guest, so proofs are not interchangeable. * * PUBLIC-VALUES CONVENTION (exactly 320 bytes = 10 x 32): * 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 firstParentHash, // header.parentHash of firstBlock * bytes32 lastBlockHash, // canonical hash of lastBlock (the tip) * bytes32 prevStateRoot, // parent state root of firstBlock * bytes32 postStateRoot, // proven post-state root of lastBlock * uint256 totalGas, // total gas used across the batch * uint256 numTxns // total transactions executed across the batch * ) * * CANONICAL BINDING: recordBatch resolves the on-chain canonical hash of * lastBlock and requires it to equal the committed lastBlockHash. Recent * blocks use the 256-deep EVM blockhash window; older blocks consult the * EIP-2935 history ring at 0x0000F90827F1C53a10cb7A02335B175320002935 (once * that fork is live). If the tip hash is unavailable or mismatched, the call * reverts, so a fabricated range can never be anchored. * * CONTINUITY: once at least one batch is recorded, every subsequent batch * must satisfy firstBlock == priorBatch.lastBlock + 1 AND prevStateRoot == * priorBatch.postStateRoot. This forms a single, gap-free, fork-free chain * of proven ranges (the first recorded batch anchors the chain). * * 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, the canonical tip binding, and continuity. * * =========================================================================== * HONEST SCOPE - READ THIS. * =========================================================================== * This proves REAL EVM execution of a SMALL contiguous batch of live chain-2800 * blocks and binds the tip to the canonical chain. 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. * =========================================================================== */ contract AereEVMValidityBatchV2 { /// @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 BATCH 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 batches recorded. uint256 public provenCount; struct ProvenBatch { uint64 firstBlock; uint64 lastBlock; uint32 numBlocks; bytes32 firstParentHash; bytes32 lastBlockHash; 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 (continuity anchor / tip). bytes32 public latestPostRoot; /// @notice The last block of the most recently proven batch (continuity anchor). uint64 public latestLastBlock; /* --------------------------------- events ------------------------------- */ event BatchProven( uint256 indexed recordId, uint256 indexed firstBlock, uint256 indexed lastBlock, uint256 numBlocks, bytes32 firstParentHash, bytes32 lastBlockHash, 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 BlockHashUnavailable(uint256 blockNumber); error BlockHashMismatch(uint256 blockNumber, bytes32 canonical, bytes32 committed); error Discontinuous(uint256 gotFirstBlock, uint256 wantFirstBlock, bytes32 gotPrevRoot, bytes32 wantPrevRoot); 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))); } /* --------------------------- 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) { if (blockNumber >= block.number) return bytes32(0); if (block.number - blockNumber <= 256) { return blockhash(blockNumber); } (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 batch ----------------------------- */ /// @notice Verify a full-EVM validity proof for a contiguous batch of chain-2800 /// blocks, bind the tip to the canonical chain, enforce continuity with /// the previously recorded batch, and record it. Permissionless. /// @param publicValues abi.encode(chainId, firstBlock, lastBlock, numBlocks, /// firstParentHash, lastBlockHash, prevStateRoot, /// postStateRoot, totalGas, numTxns), 320 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) { // 10 x 32 = 320 bytes. if (publicValues.length != 320) 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 firstParentHash, bytes32 lastBlockHash, bytes32 prevStateRoot, bytes32 postStateRoot, uint256 totalGas, uint256 numTxns ) = abi.decode( publicValues, (uint256, uint256, uint256, uint256, bytes32, bytes32, 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) CANONICAL TIP BINDING: committed lastBlockHash must equal the on-chain // canonical hash of lastBlock. bytes32 tipHash = canonicalHash(lastBlock); if (tipHash == bytes32(0)) revert BlockHashUnavailable(lastBlock); if (tipHash != lastBlockHash) { revert BlockHashMismatch(lastBlock, tipHash, lastBlockHash); } // 5) CONTINUITY: after the first record, ranges must chain gap-free and the // pre-state must equal the prior batch's proven post-state. if (provenCount != 0) { uint256 wantFirst = uint256(latestLastBlock) + 1; if (firstBlock != wantFirst || prevStateRoot != latestPostRoot) { revert Discontinuous(firstBlock, wantFirst, prevStateRoot, latestPostRoot); } } // 6) one record per exact range. bytes32 key = batchKey(firstBlock, lastBlock); uint256 existing = batchToRecord[key]; if (existing != 0) revert AlreadyProven(firstBlock, lastBlock, existing - 1); // 7) record the proven, canonically-bound batch. recordId = provenCount; records[recordId] = ProvenBatch({ firstBlock: uint64(firstBlock), lastBlock: uint64(lastBlock), numBlocks: uint32(numBlocks), firstParentHash: firstParentHash, lastBlockHash: lastBlockHash, 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, firstParentHash, lastBlockHash, 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. Does NOT check /// the canonical blockhash or continuity and records nothing. function verify( bytes calldata publicValues, bytes calldata proof ) external view returns ( uint256 firstBlock, uint256 lastBlock, uint256 numBlocks, bytes32 firstParentHash, bytes32 lastBlockHash, bytes32 prevStateRoot, bytes32 postStateRoot, uint256 totalGas, uint256 numTxns ) { if (publicValues.length != 320) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof); ( , firstBlock, lastBlock, numBlocks, firstParentHash, lastBlockHash, prevStateRoot, postStateRoot, totalGas, numTxns ) = abi.decode( publicValues, (uint256, uint256, uint256, uint256, bytes32, bytes32, 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 firstParentHash, bytes32 lastBlockHash, 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.firstParentHash, r.lastBlockHash, 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; } }