// SPDX-License-Identifier: MIT pragma solidity 0.8.23; // Reuse the LIVE verifier interface and the AnchoredRoot semantics from the // original anchor. Importing the V1 file also brings in IAereStorageProofVerifier // so the two contracts share one interface definition and one ABI convention. import "./AereHistoryStateRootAnchor.sol"; /** * @title AereHistoryStateRootAnchorV2 * @notice Corrected redeploy of AereHistoryStateRootAnchor. The anchoring path * (EIP-2935 ring-buffer block-hash recovery, keccak match, RLP state * root decode, permanent record) is byte-for-byte identical to V1. The * only change is in {proveAgainstAnchor}. * * WHAT V1 DID WRONG (audit FINDING 4, low). * V1 called VERIFIER.verify() to recover the committed blockNumber and * stateRoot, then called VERIFIER.submitProofWithExpectedRoot(), which * re-runs the very same SP1 Groth16 pairing check on the very same * publicValues and proof. The expensive proof verification therefore ran * TWICE per call for no added safety, since submitProofWithExpectedRoot * already reverts on an invalid proof and already enforces that the * proof's committed root equals the expected root passed to it. * * WHAT V2 DOES. * V2 decodes the block number, account, and slot LOCALLY from the fixed * 192-byte public-values layout (no external call), looks up the root * anchored for that block number, and calls ONLY * submitProofWithExpectedRoot(publicValues, proof, anchoredRoot). The * single verification inside submit still reverts on an invalid proof * and still binds the committed root to the anchored root, so behaviour * is unchanged while the redundant pairing check is removed. * * PUBLIC-VALUES LAYOUT (abi.encode, 6 words, 192 bytes): * [0:32] chainId * [32:64] blockNumber * [64:96] stateRoot * [96:128] account * [128:160] slot * [160:192] value * * 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 AereHistoryStateRootAnchorV2 { /// @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; /// @notice Fixed byte length of the SP1 public values this anchor accepts. uint256 public constant PUBLIC_VALUES_LEN = 192; 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 BadPublicValuesLength(uint256 got); 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 the V1 contract * note for the full rationale). * * 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. Logic is identical to V1. * @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, * running the SP1 proof verification EXACTLY ONCE. * @dev The block number, account, and slot are decoded locally from the * fixed 192-byte public-values layout, so no call to VERIFIER.verify * is made. The anchored root for that block number is passed as the * expected root to submitProofWithExpectedRoot, which performs the * single verification, reverts on an invalid proof, and reverts if * the proof's committed root does not equal the anchored root. This * is the same guarantee V1 provided, without the redundant second * verification. */ function proveAgainstAnchor(bytes calldata publicValues, bytes calldata proof) external returns (bytes32 value) { if (publicValues.length != PUBLIC_VALUES_LEN) { revert BadPublicValuesLength(publicValues.length); } (uint256 blockNumber, address account, bytes32 slot) = _decodePublicValues(publicValues); AnchoredRoot memory a = _anchored[blockNumber]; if (!a.exists) revert NotAnchored(blockNumber); // Single verification. submitProofWithExpectedRoot reverts on an invalid // SP1 proof and reverts unless the proof's committed root == a.stateRoot, // so no unverified or off-root state can be recorded. value = VERIFIER.submitProofWithExpectedRoot(publicValues, proof, a.stateRoot); emit ProvenAgainstAnchor(blockNumber, a.stateRoot, account, slot, value); } /** * @dev Decode blockNumber (word 1), account (word 3), and slot (word 4) from * the fixed 192-byte public-values layout. chainId, stateRoot, and value * are not needed here: the committed stateRoot is bound to the anchored * root inside submitProofWithExpectedRoot, and chainId is enforced by the * verifier itself. */ function _decodePublicValues(bytes calldata publicValues) internal pure returns (uint256 blockNumber, address account, bytes32 slot) { uint256 accountWord; assembly { blockNumber := calldataload(add(publicValues.offset, 32)) accountWord := calldataload(add(publicValues.offset, 96)) slot := calldataload(add(publicValues.offset, 128)) } // Low 20 bytes of word 3, matching abi.decode of an address. account = address(uint160(accountWord)); } // ---------------------------------------------------------------------- // RLP header state-root decode. Identical layout logic to // AereHistoryStateRootAnchor: 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); } }