// 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 AereZKMLVerifier uses it. interface ISp1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view; } /** * @title AereRollupValidity - on-chain anchor for ZERO-KNOWLEDGE VALIDITY PROOFS * of AERE's rollup executor. * * @notice Verifies SP1 zkVM Groth16 proofs that AERE's deterministic rollup * executor (the `aere-block-stm` sequential oracle) transitioned a prior * state root `prevRoot` to a new state root `postRoot` by applying a * specific batch of transactions committed as `batchHash`. On a valid * proof that continues the canonical chain (prevRoot == the last accepted * postRoot) it records a validated rollup epoch and advances `latestRoot`. * * This is a VALIDITY (zk) rollup settlement anchor: unlike the optimistic * AereRollupSettlement (propose + challenge window), an epoch recorded * here is final the instant the proof verifies. No fraud-proof window is * required because the zk proof already establishes correctness. * * PROGRAM BINDING: PROGRAM_VKEY is the SP1 verification key of the exact * guest ELF (program/src/main.rs) that ports AERE's transaction semantics * (execute_txn), keccak256 and state-root folding VERBATIM from * parallel-executor/src/{vm,keccak}.rs. Binding the vkey binds the exact * executor whose transition is being proven. * * PUBLIC-VALUES CONVENTION (exactly 160 bytes): * abi.encode( * uint256 chainId, // must equal block.chainid (2800) * bytes32 prevRoot, // keccak256 state root before the batch * bytes32 postRoot, // keccak256 state root after the batch * bytes32 batchHash, // keccak256 of the canonical batch encoding * uint256 numTxns // number of transactions in the batch * ) * * BATCH HASH ENCODING (matches the guest's batch_hash, so an operator can * recompute it off-chain to identify the batch): num_txns (u32 BE), then * per tx a 1-byte tag and big-endian fields, then gas (u32 BE): * 0x00 Transfer : from u64 | to u64 | amount u128 * 0x01 Sweep : from u64 | to u64 * 0x02 Increment : contract u64 | slot u64 * 0x03 AmmSwap : contract u64 | x_slot u64 | y_slot u64 | dx u128 * * IMMUTABILITY / TRUST: SP1_VERIFIER, PROGRAM_VKEY and GENESIS_ROOT are * fixed at deploy. There is NO owner and NO admin: epoch submission is * permissionless. Soundness rests entirely on the SP1 Groth16 proof plus * the prevRoot == latestRoot continuity check, so any party running the * prover can advance the canonical chain, and none can forge or fork it. * * =========================================================================== * HONEST SCOPE - READ THIS. * =========================================================================== * This proves the state transition of AERE's CURRENT rollup executor, whose VM * implements a BOUNDED set of transaction kinds (Transfer, Sweep, Increment, * AmmSwap) over a balance/storage map. It is NOT arbitrary EVM bytecode. * * It is a REAL validity proof of THAT executor: the proven computation is * byte-identical to what the production `aere-block-stm` binary self-checks and * what BlockSTMExecutor commits via AereRollupSettlement.proposeStateRoot. * * The honest next step toward a FULL validity rollup is replacing the * executor's VM with a real EVM (revm) inside the zkVM, which is a separate * multi-quarter effort. This contract does NOT prove a full EVM, and no code, * UI or claim built on it should imply that it does. * =========================================================================== */ contract AereRollupValidity { /// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector). address public immutable SP1_VERIFIER; /// @notice SP1 vkey binding the exact rollup-executor guest ELF. bytes32 public immutable PROGRAM_VKEY; /// @notice Pinned starting state root; epoch 0 must prove prevRoot == this. bytes32 public immutable GENESIS_ROOT; /// @notice The most recently accepted postRoot (starts at GENESIS_ROOT). bytes32 public latestRoot; /// @notice Number of validated epochs recorded. uint256 public epochCount; struct Epoch { bytes32 prevRoot; bytes32 postRoot; bytes32 batchHash; uint64 numTxns; uint64 at; // block timestamp when the proof verified } /// @notice epochId (0-based) => validated epoch record. mapping(uint256 => Epoch) public epochs; /// @notice batchHash => 1-based epochId that recorded it (0 == not seen). mapping(bytes32 => uint256) public batchToEpoch; /* --------------------------------- events ------------------------------- */ event EpochValidated( uint256 indexed epochId, bytes32 indexed prevRoot, bytes32 indexed postRoot, bytes32 batchHash, uint256 numTxns, address prover, uint256 at ); /* --------------------------------- errors ------------------------------- */ error InvalidProof(); error BadPublicValuesLength(uint256 got); error WrongChain(uint256 inProof, uint256 onChain); error RootDiscontinuity(bytes32 prevInProof, bytes32 expected); error ZeroAddress(); error ZeroVKey(); /* ----------------------------- constructor ------------------------------ */ constructor(address sp1Verifier, bytes32 programVKey, bytes32 genesisRoot) { if (sp1Verifier == address(0)) revert ZeroAddress(); if (programVKey == bytes32(0)) revert ZeroVKey(); SP1_VERIFIER = sp1Verifier; PROGRAM_VKEY = programVKey; GENESIS_ROOT = genesisRoot; latestRoot = genesisRoot; } /* ------------------------------ submit epoch ---------------------------- */ /// @notice Verify a validity proof for the next rollup epoch and, on success, /// record it and advance `latestRoot`. Permissionless. /// @param publicValues abi.encode(chainId, prevRoot, postRoot, batchHash, numTxns), 160 bytes. /// @param proof SP1 Groth16 proof bytes (leading selector 0x4388a21c). /// @return epochId the 0-based id of the recorded epoch. function submitEpoch( bytes calldata publicValues, bytes calldata proof ) external returns (uint256 epochId) { // 5 x 32 = 160 bytes for (uint256, bytes32, bytes32, bytes32, uint256). if (publicValues.length != 160) 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, bytes32 prevRoot, bytes32 postRoot, bytes32 batchHash, uint256 numTxns ) = abi.decode(publicValues, (uint256, bytes32, bytes32, bytes32, uint256)); // 2) bind to this chain. if (chainId != block.chainid) revert WrongChain(chainId, block.chainid); // 3) canonical-chain continuity: the batch must start from the current tip. if (prevRoot != latestRoot) revert RootDiscontinuity(prevRoot, latestRoot); // 4) record the validated epoch and advance the tip. epochId = epochCount; epochs[epochId] = Epoch({ prevRoot: prevRoot, postRoot: postRoot, batchHash: batchHash, numTxns: uint64(numTxns), at: uint64(block.timestamp) }); batchToEpoch[batchHash] = epochId + 1; // 1-based so 0 means "unseen" latestRoot = postRoot; epochCount = epochId + 1; emit EpochValidated( epochId, prevRoot, postRoot, batchHash, numTxns, msg.sender, block.timestamp ); } /* ----------------------------------- views ------------------------------ */ /// @notice Stateless verification: reverts if the proof is invalid for /// PROGRAM_VKEY, otherwise returns the decoded (prevRoot, postRoot, /// batchHash, numTxns). Records nothing and ignores chain continuity. function verify( bytes calldata publicValues, bytes calldata proof ) external view returns (bytes32 prevRoot, bytes32 postRoot, bytes32 batchHash, uint256 numTxns) { if (publicValues.length != 160) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof); (, prevRoot, postRoot, batchHash, numTxns) = abi.decode(publicValues, (uint256, bytes32, bytes32, bytes32, uint256)); } /// @notice Convenience accessor for a recorded epoch. function getEpoch(uint256 epochId) external view returns (bytes32 prevRoot, bytes32 postRoot, bytes32 batchHash, uint64 numTxns, uint64 at) { Epoch memory e = epochs[epochId]; return (e.prevRoot, e.postRoot, e.batchHash, e.numTxns, e.at); } }