// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @dev Subset of the LIVE AereStorageProofVerifier * (0xF9a1A183bEb3147D88dA5927301683fEbFb9362E) used by this anchor. * `verify` is a stateless view that reverts on an invalid SP1 proof and * otherwise returns the tuple committed in the 192-byte public values. * `submitProofWithExpectedRoot` re-verifies and records the slot only if * the proof's committed stateRoot equals `expectedStateRoot`. */ interface IAereStorageProofVerifier { function verify(bytes calldata publicValues, bytes calldata proof) external view returns ( uint256 blockNumber, bytes32 stateRoot, address account, bytes32 slot, bytes32 value ); function submitProofWithExpectedRoot( bytes calldata publicValues, bytes calldata proof, bytes32 expectedStateRoot ) external returns (bytes32 value); } /** * @title AereStateRootAnchor * @notice Binds an AERE state root to the CANONICAL chain, closing the trust * gap left open by AereStorageProofVerifier. That verifier proves a * storage value against a GIVEN state root but cannot, on its own, show * the root is canonical for a block on chain 2800. This contract does * exactly that: it recovers a canonical block hash from the EVM and * checks a caller-supplied RLP header against it, then records the * header's state root as an anchored, trust-minimized value. * * CANONICITY SOURCE ON CHAIN 2800 (probed on-chain, 2026-07-10). * - BLOCKHASH opcode: LIVE. Serves the last 256 blocks. This is the * ONLY canonicity source available on chain 2800 today. * - EIP-2935 history-storage contract * (0x0000F90827F1C53a10cb7A02335B175320002935 and the two earlier * devnet addresses): NOT deployed (eth_getCode == 0x). So the * extended ~8191-block window is NOT available here. * Therefore this anchor uses BLOCKHASH and its window is 256 blocks. * At AERE's 0.5s block time that is only ~128 seconds of history, so * `anchorRecentHeader` MUST be called promptly after the target block * is produced. Once anchored, the (blockNumber -> stateRoot) record is * permanent and independent of the moving BLOCKHASH window. * * The block hash is recovered strictly from consensus (the BLOCKHASH * opcode), never from a caller argument, so an attacker cannot anchor a * fabricated root: keccak256(rlpHeader) must equal the opcode's answer. * * IMMUTABILITY: VERIFIER is fixed at deploy. No owner, no admin, no * upgrade path. Anyone may anchor any in-window header and anyone may * prove against an anchored root. */ contract AereStateRootAnchor { /// @notice Live AereStorageProofVerifier this anchor drives for proofs. IAereStorageProofVerifier public immutable VERIFIER; /// @notice EVM BLOCKHASH window: the opcode addresses the last 256 blocks. uint256 public constant BLOCKHASH_WINDOW = 256; struct AnchoredRoot { bool exists; bytes32 stateRoot; // header field index 3 bytes32 blockHash; // canonical hash the header was checked against uint64 blockNumber; uint64 verifiedAt; // block.timestamp when anchored } /// @notice blockNumber => anchored canonical state root record. mapping(uint256 => AnchoredRoot) private _anchored; event HeaderAnchored( uint256 indexed blockNumber, bytes32 indexed blockHash, bytes32 stateRoot, uint256 verifiedAt ); event ProvenAgainstAnchor( uint256 indexed blockNumber, bytes32 indexed stateRoot, address indexed account, bytes32 slot, bytes32 value ); error ZeroVerifier(); error OutOfWindow(uint256 blockNumber, uint256 currentBlock); error HeaderHashMismatch(bytes32 got, bytes32 canonical); error MalformedHeader(uint256 code); error NotAnchored(uint256 blockNumber); error RootNotCanonical(bytes32 proofRoot, bytes32 anchoredRoot); constructor(address verifier) { if (verifier == address(0)) revert ZeroVerifier(); VERIFIER = IAereStorageProofVerifier(verifier); } /** * @dev Canonical block-hash source. Overridable so tests can inject a REAL * AERE canonical hash for a block outside the live 256-block window * (e.g. genesis) without weakening the production path, which always * uses the BLOCKHASH opcode. */ function _blockHashOf(uint256 blockNumber) internal view virtual returns (bytes32) { return blockhash(blockNumber); } /** * @notice Anchor the state root of a recent canonical block. * @param blockNumber The block whose header is being anchored. MUST be * within the last 256 blocks (BLOCKHASH window) at call time. * @param rlpHeader The exact RLP-encoded block header whose keccak256 is * the canonical block hash. The state root is header field index 3. * @return stateRoot The decoded, now-anchored canonical state root. * * Reverts OutOfWindow if BLOCKHASH cannot address the block (older than 256 * blocks, the current block, or a future block). Reverts HeaderHashMismatch * if the supplied header does not hash to the canonical block hash. */ function anchorRecentHeader(uint256 blockNumber, bytes calldata rlpHeader) external returns (bytes32 stateRoot) { bytes32 canonical = _blockHashOf(blockNumber); // BLOCKHASH returns 0 for any block it cannot address. A real block hash // is never 0 (keccak of a nonempty header), so 0 == out of window. if (canonical == bytes32(0)) revert OutOfWindow(blockNumber, block.number); bytes32 got = keccak256(rlpHeader); if (got != canonical) revert HeaderHashMismatch(got, canonical); stateRoot = _decodeStateRoot(rlpHeader); _anchored[blockNumber] = AnchoredRoot({ exists: true, stateRoot: stateRoot, blockHash: canonical, blockNumber: uint64(blockNumber), verifiedAt: uint64(block.timestamp) }); emit HeaderAnchored(blockNumber, canonical, stateRoot, block.timestamp); } /** * @notice Drive the live storage-proof verifier against an ANCHORED root. * This is the trust-minimized end of the two-call pattern: rather * than trusting a caller-supplied stateRoot, the proof's committed * root is checked to equal the root anchored for the SAME block * number (that block number is committed inside the proof, so the * caller cannot mismatch them). Only then is the slot recorded in * the verifier via {submitProofWithExpectedRoot}. * @dev The verifier reverts the whole call on an invalid SP1 proof or a * wrong chainId, so no unverified state can be recorded. */ function proveAgainstAnchor(bytes calldata publicValues, bytes calldata proof) external returns (bytes32 value) { (uint256 blockNumber, bytes32 stateRoot, address account, bytes32 slot, ) = VERIFIER.verify(publicValues, proof); AnchoredRoot memory a = _anchored[blockNumber]; if (!a.exists) revert NotAnchored(blockNumber); if (a.stateRoot != stateRoot) revert RootNotCanonical(stateRoot, a.stateRoot); value = VERIFIER.submitProofWithExpectedRoot(publicValues, proof, a.stateRoot); emit ProvenAgainstAnchor(blockNumber, a.stateRoot, account, slot, value); } // ---------------------------------------------------------------------- // RLP header state-root decode. // // A block header is a long RLP list. Its first three fields are fixed size: // [0] parentHash 32 bytes (prefix 0xa0) // [1] ommersHash 32 bytes (prefix 0xa0) // [2] beneficiary 20 bytes (prefix 0x94) // [3] stateRoot 32 bytes (prefix 0xa0) <-- what we want // so the state root sits at a fixed offset inside the list payload, // regardless of any later fields (extraData, blob/beacon fields, or QBFT // commit-seal handling), all of which come AFTER stateRoot. // ---------------------------------------------------------------------- function _decodeStateRoot(bytes calldata rlpHeader) internal pure returns (bytes32 stateRoot) { uint256 len = rlpHeader.length; // Smallest possible header list prefix (1) + 4 leading fields. if (len < 121) revert MalformedHeader(1); uint8 first = uint8(rlpHeader[0]); // Block headers are always long lists (payload > 55 bytes) -> 0xf8..0xff. if (first < 0xf8) revert MalformedHeader(2); uint256 numLenBytes = uint256(first) - 0xf7; // 1..8 uint256 payloadLen; for (uint256 i = 0; i < numLenBytes; i++) { payloadLen = (payloadLen << 8) | uint256(uint8(rlpHeader[1 + i])); } uint256 payloadStart = 1 + numLenBytes; // The declared payload length must exactly cover the rest of the input. if (payloadStart + payloadLen != len) revert MalformedHeader(3); // Need room for the four leading fixed fields: 33 + 33 + 21 + 33 = 120. if (payloadStart + 120 > len) revert MalformedHeader(4); // Validate the fixed-size field prefixes so we know the layout is a // standard Ethereum-style header and offset 88 truly is the stateRoot. if (uint8(rlpHeader[payloadStart]) != 0xa0) revert MalformedHeader(5); // parentHash if (uint8(rlpHeader[payloadStart + 33]) != 0xa0) revert MalformedHeader(6); // ommersHash if (uint8(rlpHeader[payloadStart + 66]) != 0x94) revert MalformedHeader(7); // beneficiary if (uint8(rlpHeader[payloadStart + 87]) != 0xa0) revert MalformedHeader(8); // stateRoot prefix uint256 srContentOffset = payloadStart + 88; assembly { stateRoot := calldataload(add(rlpHeader.offset, srContentOffset)) } } // --------------------------- views ----------------------------------- function getAnchoredRoot(uint256 blockNumber) external view returns (bool exists, bytes32 stateRoot, bytes32 blockHash, uint64 blockNum, uint64 verifiedAt) { AnchoredRoot memory a = _anchored[blockNumber]; return (a.exists, a.stateRoot, a.blockHash, a.blockNumber, a.verifiedAt); } /// @notice Anchored state root for `blockNumber`; reverts if not anchored. function anchoredStateRoot(uint256 blockNumber) external view returns (bytes32) { AnchoredRoot memory a = _anchored[blockNumber]; if (!a.exists) revert NotAnchored(blockNumber); return a.stateRoot; } function isAnchored(uint256 blockNumber) external view returns (bool) { return _anchored[blockNumber].exists; } /// @notice The canonical block hash the anchor would use right now (0 if the /// block is outside the addressable BLOCKHASH window). function canonicalBlockHash(uint256 blockNumber) external view returns (bytes32) { return _blockHashOf(blockNumber); } /// @notice True iff `blockNumber` is currently anchorable (in the BLOCKHASH window). function isWithinWindow(uint256 blockNumber) external view returns (bool) { return _blockHashOf(blockNumber) != bytes32(0); } /// @notice Convenience preview of the state root a given header decodes to, /// WITHOUT checking canonicity or recording anything. Pure helper /// for off-chain callers assembling a header before anchoring. function previewStateRoot(bytes calldata rlpHeader) external pure returns (bytes32) { return _decodeStateRoot(rlpHeader); } }