// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @dev Subset of the LIVE AereStorageProofVerifier 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 AereHistoryStateRootAnchor * @notice EIP-2935 sibling of AereStateRootAnchor. It binds an AERE state root * to the CANONICAL chain for blocks FAR OLDER than the 256-block * BLOCKHASH window, closing the same trust gap AereStorageProofVerifier * leaves open (that verifier proves a value against a GIVEN root but * cannot show the root is canonical). * * WHERE THE BLOCK HASH COMES FROM (this is the whole point). * The canonical block hash is read, at call time, from the EIP-2935 * history-storage system contract at * 0x0000F90827F1C53a10cb7A02335B175320002935 via STATICCALL. That * contract is a protocol-native ring buffer: on the AerePQC fork * (futureEips milestone, same futureEipsTime activation as the native * PQC precompiles) Besu's PraguePreExecutionProcessor writes the parent * block hash into slot (number-1) % 8191 at the start of EVERY block, * as a system call from address 0xffff...fffe. No externally-owned * account, relayer, keeper, or Foundation key ever writes it. The value * this anchor trusts is therefore produced by consensus itself, exactly * like the BLOCKHASH opcode but with an 8191-block reach instead of 256. * * At AERE's 0.5s block time, 8191 blocks is roughly 68 minutes of * reach, versus the ~128 seconds BLOCKHASH gives. Once a header is * anchored here, the (blockNumber -> stateRoot) record is permanent and * independent of the moving 8191-block ring buffer. * * The block hash is recovered strictly from the system contract, never * from a caller argument, so an attacker cannot anchor a fabricated * root: keccak256(rlpHeader) must equal the ring buffer'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 AereHistoryStateRootAnchor { /// @notice Live AereStorageProofVerifier this anchor drives for proofs. IAereStorageProofVerifier public immutable VERIFIER; /// @notice EIP-2935 history-storage system contract (protocol-written ring buffer). address public constant HISTORY_STORAGE_ADDRESS = 0x0000F90827F1C53a10cb7A02335B175320002935; /// @notice EIP-2935 serve window: the ring buffer addresses the last 8191 blocks. uint256 public constant HISTORY_SERVE_WINDOW = 8191; 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 OutOfHistoryWindow(uint256 blockNumber, uint256 currentBlock); error HistoryContractMissing(); 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: the EIP-2935 history-storage contract. * Overridable so tests can inject a REAL AERE canonical hash without a * live fork; the production path always STATICCALLs the system * contract, whose storage is written by consensus (see contract note). * * Read ABI (per EIP-2935 runtime code): input is the 32-byte big-endian * block number; output is the 32-byte canonical hash. The contract * REVERTS for any block outside [number-8191, number-1], so a revert or * a non-32-byte return is treated as out-of-window. */ function _historyBlockHash(uint256 blockNumber) internal view virtual returns (bytes32 h) { if (HISTORY_STORAGE_ADDRESS.code.length == 0) revert HistoryContractMissing(); (bool ok, bytes memory ret) = HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber)); if (!ok || ret.length != 32) revert OutOfHistoryWindow(blockNumber, block.number); h = abi.decode(ret, (bytes32)); // A real block hash is never zero; zero means the slot was never written // for this block number (out of the served window). if (h == bytes32(0)) revert OutOfHistoryWindow(blockNumber, block.number); } /** * @notice Anchor the state root of a historical canonical block using the * EIP-2935 ring buffer. Works for blocks up to 8191 back, far beyond * the 256-block BLOCKHASH horizon. * @param blockNumber The block whose header is being anchored. MUST be * within the last 8191 blocks (EIP-2935 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. */ function anchorHistoricalHeader(uint256 blockNumber, bytes calldata rlpHeader) external returns (bytes32 stateRoot) { bytes32 canonical = _historyBlockHash(blockNumber); 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. * 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. Identical layout logic to // AereStateRootAnchor: the first four header fields are fixed size, so the // stateRoot sits at a fixed offset in the list payload. // [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 // ---------------------------------------------------------------------- function _decodeStateRoot(bytes calldata rlpHeader) internal pure returns (bytes32 stateRoot) { uint256 len = rlpHeader.length; if (len < 121) revert MalformedHeader(1); uint8 first = uint8(rlpHeader[0]); 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; if (payloadStart + payloadLen != len) revert MalformedHeader(3); if (payloadStart + 120 > len) revert MalformedHeader(4); 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 EIP-2935 ring buffer serves right now /// for `blockNumber`. Reverts OutOfHistoryWindow if not served. function historyBlockHash(uint256 blockNumber) external view returns (bytes32) { return _historyBlockHash(blockNumber); } /// @notice True iff `blockNumber` is currently served by the EIP-2935 buffer. function isWithinHistoryWindow(uint256 blockNumber) external view returns (bool) { if (HISTORY_STORAGE_ADDRESS.code.length == 0) return false; (bool ok, bytes memory ret) = HISTORY_STORAGE_ADDRESS.staticcall(abi.encode(blockNumber)); if (!ok || ret.length != 32) return false; return abi.decode(ret, (bytes32)) != bytes32(0); } /// @notice True iff the EIP-2935 system contract has code (fork active here). function historyContractDeployed() external view returns (bool) { return HISTORY_STORAGE_ADDRESS.code.length != 0; } /// @notice Preview the state root a header decodes to, WITHOUT canonicity /// checks or recording. Pure helper for off-chain header assembly. function previewStateRoot(bytes calldata rlpHeader) external pure returns (bytes32) { return _decodeStateRoot(rlpHeader); } }