// 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 AereRollupValidity uses it. interface ISp1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view; } /** * @title AereEVMValidity - on-chain anchor for REAL FULL-EVM validity proofs of * live chain-2800 blocks. * * @notice Verifies SP1 zkVM Groth16 proofs that a REAL AERE mainnet (chain 2800, * QBFT / Besu) block was re-executed with revm INSIDE the zkVM and that * the recomputed post-state root equals the `header.stateRoot` carried in * the proof's public values. The guest (bin/aere-client) runs the * SP1-patched reth 1.9.3 / revm stack over the block's transactions against * the parent-block pre-state (account + storage MPT proofs gathered by the * host from `eth_getProof`), re-roots the sparse trie, and only produces a * valid proof when computed stateRoot == header.stateRoot. * * BINDING CAVEAT (READ THIS): this V1 anchor attests a SELF-CONSISTENT * execution, NOT that the recorded block is the CANONICAL chain-2800 block * at that height. The public values carry no blockHash/parentHash and this * contract performs no blockhash() check, so a genuine SP1 proof of a * FABRICATED execution can be recorded here, and first-write-wins dedup lets * a slot be squatted. Do NOT treat a V1 record as canonical. Canonical * binding (committed blockHash + on-chain blockhash()/EIP-2935 check + batch * continuity) lands in AereEVMValidityV2 / AereEVMValidityBatchV2. * * Unlike AereRollupValidity (which proves AERE's BOUNDED toy-VM executor: * Transfer/Sweep/Increment/AmmSwap), this contract anchors proofs of * ARBITRARY EVM bytecode execution: full opcode set, call frames, * SLOAD/SSTORE, LOG, EIP-1559 base-fee burn + priority-to-coinbase, and * the Cancun/Prague/Osaka system calls, all of which affect the proven * state root. * * PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact * guest ELF that performs revm-in-zkVM execution + witness re-root. * Binding the vkey binds the exact prover program. * * PUBLIC-VALUES CONVENTION (exactly 224 bytes = 7 x 32): * abi.encode( * uint256 chainId, // must equal block.chainid (2800) * uint256 blockNumber, // the proven chain-2800 block height * bytes32 prevStateRoot, // parent block state root (pre-state anchor, * // NOT cross-checked on-chain in V1) * bytes32 postStateRoot, // self-consistent 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 * ) * * 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 (blockNumber, prevStateRoot, postStateRoot) triple that the zk * proof establishes for PROGRAM_VKEY. * * =========================================================================== * HONEST SCOPE - READ THIS. * =========================================================================== * This proves REAL EVM execution of a SMALL live chain-2800 block (a handful * of transactions). 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 proven blocks rather than a continuous * sequencer chain. No code, UI or claim built on it should imply otherwise. * =========================================================================== */ contract AereEVMValidity { /// @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 guest ELF. bytes32 public immutable PROGRAM_VKEY; /// @notice Number of proven blocks recorded. uint256 public provenCount; struct ProvenBlock { uint64 blockNumber; 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 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 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; } /* ------------------------------ record block ---------------------------- */ /// @notice Verify a full-EVM validity proof for a chain-2800 block and, on /// success, record it. Permissionless. Reverts if the proof is not /// valid for PROGRAM_VKEY, if the proof is for a different chain, or /// if that block number was already recorded. /// @param publicValues abi.encode(chainId, blockNumber, prevStateRoot, /// postStateRoot, txRoot, gasUsed, numTxns), 224 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) { // 7 x 32 = 224 bytes. if (publicValues.length != 224) 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 prevStateRoot, bytes32 postStateRoot, bytes32 txRoot, uint256 gasUsed, uint256 numTxns ) = abi.decode( publicValues, (uint256, uint256, bytes32, bytes32, bytes32, uint256, uint256) ); // 2) bind to this chain. if (chainId != block.chainid) revert WrongChain(chainId, block.chainid); // 3) one record per block number. uint256 existing = blockToRecord[blockNumber]; if (existing != 0) revert AlreadyProven(blockNumber, existing - 1); // 4) record the proven block. recordId = provenCount; records[recordId] = ProvenBlock({ blockNumber: uint64(blockNumber), 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, 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. Records nothing. function verify( bytes calldata publicValues, bytes calldata proof ) external view returns ( uint256 blockNumber, bytes32 prevStateRoot, bytes32 postStateRoot, bytes32 txRoot, uint256 gasUsed, uint256 numTxns ) { if (publicValues.length != 224) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof); (, blockNumber, prevStateRoot, postStateRoot, txRoot, gasUsed, numTxns) = abi.decode( publicValues, (uint256, uint256, bytes32, bytes32, bytes32, uint256, uint256) ); } /// @notice Convenience accessor for a recorded proven block. function getRecord(uint256 recordId) external view returns ( uint64 blockNumber, bytes32 prevStateRoot, bytes32 postStateRoot, bytes32 txRoot, uint64 gasUsed, uint32 numTxns, uint64 at, address prover ) { ProvenBlock memory r = records[recordId]; return ( r.blockNumber, r.prevStateRoot, r.postStateRoot, r.txRoot, r.gasUsed, r.numTxns, r.at, r.prover ); } }