From c101c06bd10a0fa8193629d88f82bd4136a96868 Mon Sep 17 00:00:00 2001 From: Luiz Date: Thu, 19 Feb 2026 11:19:38 -0300 Subject: [PATCH 1/8] add OP Pusher and Buffer --- package-lock.json | 24 +--- .../optimism/OptimismBuffer.sol | 61 ++++++++++ .../optimism/OptimismPusher.sol | 59 ++++++++++ .../mocks/MockOpCrosschainDomainMessenger.sol | 33 ++++++ .../optimism/OptimismBuffer.t.sol | 105 ++++++++++++++++++ .../optimism/OptimismPusher.t.sol | 72 ++++++++++++ 6 files changed, 332 insertions(+), 22 deletions(-) create mode 100644 src/contracts/block-hash-pusher/optimism/OptimismBuffer.sol create mode 100644 src/contracts/block-hash-pusher/optimism/OptimismPusher.sol create mode 100644 test/block-hash-pusher/mocks/MockOpCrosschainDomainMessenger.sol create mode 100644 test/block-hash-pusher/optimism/OptimismBuffer.t.sol create mode 100644 test/block-hash-pusher/optimism/OptimismPusher.t.sol diff --git a/package-lock.json b/package-lock.json index 99873b8..d592bfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1639,7 +1639,6 @@ "integrity": "sha512-T0JTnuib7QcpsWkHCPLT7Z6F483EjTdcdjb1e00jqS9zTGCPqinPB66LLtR/duDLdvgoiCVS6K8WxTQkA/xR1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@nomicfoundation/ignition-core": "^0.15.15", "@nomicfoundation/ignition-ui": "^0.15.13", @@ -1660,7 +1659,6 @@ "integrity": "sha512-BKWRZSkW1dLMW6VRjQCLei+JGYmotTsQo/QeR21CyVG0FMkPESTU6X1XPKynqNjaYJebV8rDsjtdB/Bzi7FRog==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@nomicfoundation/hardhat-ignition": "^0.15.16", "@nomicfoundation/hardhat-viem": "^2.1.0", @@ -1675,7 +1673,6 @@ "integrity": "sha512-p7HaUVDbLj7ikFivQVNhnfMHUBgiHYMwQWvGn9AriieuopGOELIrwj2KjyM2a6z70zai5YKO264Vwz+3UFJZPQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ethereumjs-util": "^7.1.4" }, @@ -1716,7 +1713,6 @@ "integrity": "sha512-danbGjPp2WBhLkJdQy9/ARM3WQIK+7vwzE0urNem1qZJjh9f54Kf5f1xuQv8DvqewUAkuPxVt/7q4Grz5WjqSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@ethersproject/address": "^5.0.2", @@ -1738,7 +1734,6 @@ "integrity": "sha512-tjF5WE9lzUIWnPqPHy3yJUeRo1stMG3o3MQhmKnYMl6Ulg7WMy1zYk+LuFE6f0XER6c3A6+ukRIYxXV+RZFiCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "abitype": "^0.9.8", "lodash.memoize": "^4.1.2" @@ -2296,8 +2291,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai-as-promised": { "version": "7.1.8", @@ -2305,7 +2299,6 @@ "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "*" } @@ -2357,8 +2350,7 @@ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/node": { "version": "25.1.0", @@ -2366,7 +2358,6 @@ "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2467,7 +2458,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -3128,7 +3118,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -3837,7 +3826,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4120,7 +4108,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -4805,7 +4792,6 @@ "integrity": "sha512-iQC4WNWjWMz7cVVFqzEBNisUQ/EEEJrWysJ2hRAMTnfXJx6Y11UXdmtz4dHIzvGL0z27XCCaJrcApDPH0KaZEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", @@ -4869,7 +4855,6 @@ "integrity": "sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-uniq": "1.0.3", "eth-gas-reporter": "^0.2.25", @@ -7544,7 +7529,6 @@ "integrity": "sha512-5P8vnB6qVX9tt1MfuONtCTEaEGO/O4WuEidPHIAJjx4sktHHKhO3rFvnE0q8L30nWJPTrcqGQMT7jpE29B2qow==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "@ethersproject/abi": "^5.0.9", "@solidity-parser/parser": "^0.20.1", @@ -8055,7 +8039,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8182,7 +8165,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8288,7 +8270,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -8610,7 +8591,6 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/src/contracts/block-hash-pusher/optimism/OptimismBuffer.sol b/src/contracts/block-hash-pusher/optimism/OptimismBuffer.sol new file mode 100644 index 0000000..6a458e0 --- /dev/null +++ b/src/contracts/block-hash-pusher/optimism/OptimismBuffer.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {BaseBuffer} from "../BaseBuffer.sol"; +import {IBuffer} from "../interfaces/IBuffer.sol"; +import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; + +/// @title OptimismBuffer +/// @notice Implementation of BaseBuffer for storing Ethereum L1 block hashes on Optimism L2. +/// @dev This contract extends BaseBuffer with access control specific to Optimism's L1->L2 messaging. +/// The pusher address on L1 must send the message via L1CrossDomainMessengerProxy to the buffer address on L2. +/// The L2CrossDomainMessenger contract on L2 is responsible for relaying the message to the buffer contract on L2. +/// @custom:security-contact security@openzeppelin.com +contract OptimismBuffer is BaseBuffer { + /// @dev The address of the L2CrossDomainMessenger contract on L2. + address private immutable _l2CrossDomainMessenger; + + /// @dev The address of the pusher contract on L1. + address private immutable _pusher; + + /// @notice Thrown when attempting to set an invalid L2CrossDomainMessenger address. + error InvalidL2CrossDomainMessengerAddress(); + + /// @notice Thrown when attempting to set an invalid pusher address. + error InvalidPusherAddress(); + + /// @notice Thrown when the domain message sender does not match the pusher address. + error DomainMessageSenderMismatch(); + + /// @notice Thrown when the sender is not the L2CrossDomainMessenger contract. + error InvalidSender(); + + constructor(address l2CrossDomainMessenger_, address pusher_) { + require(l2CrossDomainMessenger_ != address(0), InvalidL2CrossDomainMessengerAddress()); + require(pusher_ != address(0), InvalidPusherAddress()); + + _l2CrossDomainMessenger = l2CrossDomainMessenger_; + _pusher = pusher_; + } + + /// @inheritdoc IBuffer + function receiveHashes(uint256 firstBlockNumber, bytes32[] calldata blockHashes) external { + ICrossDomainMessenger l2CrossDomainMessengerCached = ICrossDomainMessenger(l2CrossDomainMessenger()); + + require(msg.sender == address(l2CrossDomainMessengerCached), InvalidSender()); + require(l2CrossDomainMessengerCached.xDomainMessageSender() == _pusher, DomainMessageSenderMismatch()); + + _receiveHashes(firstBlockNumber, blockHashes); + } + + /// @inheritdoc IBuffer + function pusher() public view returns (address) { + return _pusher; + } + + /// @notice The address of the L2CrossDomainMessenger contract on L2. + /// @return The address of the L2CrossDomainMessenger contract on L2. + function l2CrossDomainMessenger() public view returns (address) { + return _l2CrossDomainMessenger; + } +} diff --git a/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol b/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol new file mode 100644 index 0000000..da41879 --- /dev/null +++ b/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {BlockHashArrayBuilder} from "../BlockHashArrayBuilder.sol"; +import {IBuffer} from "../interfaces/IBuffer.sol"; +import {IPusher} from "../interfaces/IPusher.sol"; +import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; + +/// @title OPtimismPusher +/// @notice Implementation of IPusher for pushing block hashes to Optimism L2. +/// @dev This contract sends block hashes from Ethereum L1 to a OptimismBuffer contract on Optimism L2 +/// via the Optimism L1CrossDomainMessenger's `sendMessage` function. The pusher must be configured +/// with the correct L1CrossDomainMessengerProxy address. +/// @custom:security-contact security@openzeppelin.com +contract OptimismPusher is BlockHashArrayBuilder, IPusher { + /// @dev The address of the Optimism L1CrossDomainMessengerProxy contract on L1. + address private immutable _l1CrossDomainMessengerProxy; + + /// @notice Parameters for the L2 transaction that will be executed on Optimism. + /// @param gasLimit The gas limit for the L2 transaction. + struct OptimismL2Transaction { + uint32 gasLimit; + } + + /// @notice Thrown when attempting to set an invalid L1CrossDomainMessengerProxy address. + error InvalidL1CrossDomainMessengerProxyAddress(); + + constructor(address l1CrossDomainMessengerProxy_) { + if (l1CrossDomainMessengerProxy_ == address(0)) { + revert InvalidL1CrossDomainMessengerProxyAddress(); + } + + _l1CrossDomainMessengerProxy = l1CrossDomainMessengerProxy_; + } + + /// @inheritdoc IPusher + function pushHashes(address buffer, uint256 firstBlockNumber, uint256 batchSize, bytes calldata l2TransactionData) + external + payable + { + require(buffer != address(0), InvalidBuffer(buffer)); + require(msg.value == 0, IncorrectMsgValue(0, msg.value)); + + bytes32[] memory blockHashes = _buildBlockHashArray(firstBlockNumber, batchSize); + bytes memory l2Calldata = abi.encodeCall(IBuffer.receiveHashes, (firstBlockNumber, blockHashes)); + + OptimismL2Transaction memory l2Transaction = abi.decode(l2TransactionData, (OptimismL2Transaction)); + + ICrossDomainMessenger(l1CrossDomainMessengerProxy()).sendMessage(buffer, l2Calldata, l2Transaction.gasLimit); + + emit BlockHashesPushed(firstBlockNumber, firstBlockNumber + batchSize - 1); + } + + /// @notice The address of the Optimism L1CrossDomainMessengerProxy contract on L1. + /// @return The address of the Optimism L1CrossDomainMessengerProxy contract on L1. + function l1CrossDomainMessengerProxy() public view returns (address) { + return _l1CrossDomainMessengerProxy; + } +} diff --git a/test/block-hash-pusher/mocks/MockOpCrosschainDomainMessenger.sol b/test/block-hash-pusher/mocks/MockOpCrosschainDomainMessenger.sol new file mode 100644 index 0000000..3707128 --- /dev/null +++ b/test/block-hash-pusher/mocks/MockOpCrosschainDomainMessenger.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; +import {IL2CrossDomainMessenger} from "@eth-optimism/contracts/L2/messaging/IL2CrossDomainMessenger.sol"; + +contract MockOpCrosschainDomainMessenger is IL2CrossDomainMessenger { + address transient TRANSIENT_MESSAGE_SENDER; + + function sendMessage(address _to, bytes calldata _message, uint32 _gasLimit) external override { + // no-op + + emit SentMessage(_to, msg.sender, _message, 0, _gasLimit); + } + + function xDomainMessageSender() external view override returns (address) { + return TRANSIENT_MESSAGE_SENDER; + } + + function relayMessage(address _target, address _sender, bytes calldata _message, uint256 _messageNonce) + external + override + { + TRANSIENT_MESSAGE_SENDER = _sender; + + (bool callSuccess, bytes memory returnData) = _target.call(_message); + if (!callSuccess) { + revert("Message sending failed"); + } + + TRANSIENT_MESSAGE_SENDER = address(0); + } +} diff --git a/test/block-hash-pusher/optimism/OptimismBuffer.t.sol b/test/block-hash-pusher/optimism/OptimismBuffer.t.sol new file mode 100644 index 0000000..688f325 --- /dev/null +++ b/test/block-hash-pusher/optimism/OptimismBuffer.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Test, Vm} from "forge-std/Test.sol"; +import {OptimismBuffer} from "../../../src/contracts/block-hash-pusher/optimism/OptimismBuffer.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IBuffer} from "../../../src/contracts/block-hash-pusher/interfaces/IBuffer.sol"; +import {MockOpCrosschainDomainMessenger} from "../mocks/MockOpCrosschainDomainMessenger.sol"; +import {ICrossDomainMessenger} from "@eth-optimism/contracts/libraries/bridge/ICrossDomainMessenger.sol"; + +contract OptimismBufferTest is Test { + address public pusher = makeAddr("pusher"); + + address public relayer = makeAddr("relayer"); + + MockOpCrosschainDomainMessenger public mockOpCrosschainDomainMessenger; + + function setUp() public { + mockOpCrosschainDomainMessenger = new MockOpCrosschainDomainMessenger(); + } + + function testFuzz_receiveHashes(uint16 batchSize, uint8 firstBlockNumber) public { + vm.assume(batchSize > 0 && batchSize <= 8191); + + OptimismBuffer buffer = new OptimismBuffer(address(mockOpCrosschainDomainMessenger), pusher); + + bytes32[] memory blockHashes = new bytes32[](batchSize); + for (uint256 i = 0; i < batchSize; i++) { + blockHashes[i] = keccak256(abi.encode(i + 1)); + } + + bytes memory l2Calldata = abi.encodeCall(buffer.receiveHashes, (firstBlockNumber, blockHashes)); + + vm.expectCall(address(buffer), l2Calldata); + if (firstBlockNumber == 0) { + vm.expectRevert("Message sending failed"); + } + vm.prank(relayer); + mockOpCrosschainDomainMessenger.relayMessage(address(buffer), pusher, l2Calldata, 0); + } + + function test_receiveHashes_does_not_emit_event_when_no_hashes_written() public { + OptimismBuffer buffer = new OptimismBuffer(address(mockOpCrosschainDomainMessenger), pusher); + + bytes32[] memory blockHashes = new bytes32[](5); + for (uint256 i = 0; i < 5; i++) { + blockHashes[i] = keccak256(abi.encode(i + 1)); + } + + bytes memory l2Calldata = abi.encodeCall(buffer.receiveHashes, (1, blockHashes)); + + // First push: should emit + vm.expectEmit(); + emit IBuffer.BlockHashesPushed(1, 5); + vm.prank(relayer); + mockOpCrosschainDomainMessenger.relayMessage(address(buffer), pusher, l2Calldata, 0); + assertEq(buffer.newestBlockNumber(), 5); + + // Duplicate push: should NOT emit BlockHashesPushed + vm.recordLogs(); + vm.prank(relayer); + mockOpCrosschainDomainMessenger.relayMessage(address(buffer), pusher, l2Calldata, 0); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i = 0; i < logs.length; i++) { + assertTrue(logs[i].topics[0] != IBuffer.BlockHashesPushed.selector, "Unexpected BlockHashesPushed event"); + } + assertEq(buffer.newestBlockNumber(), 5); + } + + function test_constructor_reverts_if_pusher_is_zero_address() public { + vm.expectRevert(abi.encodeWithSelector(OptimismBuffer.InvalidPusherAddress.selector)); + new OptimismBuffer(address(mockOpCrosschainDomainMessenger), address(0)); + } + + function test_constructor_reverts_if_l2_scroll_messenger_is_zero_address() public { + vm.expectRevert(abi.encodeWithSelector(OptimismBuffer.InvalidL2CrossDomainMessengerAddress.selector)); + new OptimismBuffer(address(0), pusher); + } + + function testFuzz_receiveHashes_reverts_if_sender_is_not_l2_cross_domain_messenger(address notOpCrosschainDomainMessenger) + public + { + vm.assume(notOpCrosschainDomainMessenger != address(mockOpCrosschainDomainMessenger)); + OptimismBuffer buffer = new OptimismBuffer(address(mockOpCrosschainDomainMessenger), pusher); + + bytes memory l2Calldata = abi.encodeCall(buffer.receiveHashes, (1, new bytes32[](1))); + + vm.expectRevert(abi.encodeWithSelector(OptimismBuffer.InvalidSender.selector)); + vm.prank(notOpCrosschainDomainMessenger); + buffer.receiveHashes(1, new bytes32[](1)); + } + + /// forge-config: default.allow_internal_expect_revert = true + function test_receiveHashes_reverts_if_xDomainMessageSender_does_not_match_pusher(address notPusher) public { + vm.assume(notPusher != pusher); + OptimismBuffer buffer = new OptimismBuffer(address(mockOpCrosschainDomainMessenger), pusher); + + bytes memory l2Calldata = abi.encodeCall(buffer.receiveHashes, (1, new bytes32[](1))); + + vm.expectRevert(); + vm.prank(relayer); + mockOpCrosschainDomainMessenger.relayMessage(address(buffer), notPusher, l2Calldata, 0); + } +} diff --git a/test/block-hash-pusher/optimism/OptimismPusher.t.sol b/test/block-hash-pusher/optimism/OptimismPusher.t.sol new file mode 100644 index 0000000..647566a --- /dev/null +++ b/test/block-hash-pusher/optimism/OptimismPusher.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {OptimismPusher} from "../../../src/contracts/block-hash-pusher/optimism/OptimismPusher.sol"; +import {IPusher} from "../../../src/contracts/block-hash-pusher/interfaces/IPusher.sol"; +import {MockOpCrosschainDomainMessenger} from "../mocks/MockOpCrosschainDomainMessenger.sol"; + +contract OptimismPusherTest is Test { + address public user = makeAddr("user"); + address public mockOpCrosschainDomainMessenger; + address public buffer = makeAddr("buffer"); + + address public l1CrossDomainMessengerProxyAddress = 0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef; // Address for Ethereum Sepolia + + function setUp() public { + mockOpCrosschainDomainMessenger = address(new MockOpCrosschainDomainMessenger()); + } + + function test_pushHashes_fork() public { + vm.createSelectFork(vm.envString("ETHEREUM_RPC_URL")); + + OptimismPusher optimismPusher = new OptimismPusher(l1CrossDomainMessengerProxyAddress); + + bytes memory l2TransactionData = abi.encode(OptimismPusher.OptimismL2Transaction({gasLimit: 200000})); + + optimismPusher.pushHashes(buffer, block.number - 1, 1, l2TransactionData); + + optimismPusher.pushHashes(buffer, block.number - 10, 10, l2TransactionData); + + optimismPusher.pushHashes(buffer, block.number - 15, 15, l2TransactionData); + + optimismPusher.pushHashes(buffer, block.number - 1000, 10, l2TransactionData); + + optimismPusher.pushHashes(buffer, block.number - 8000, 20, l2TransactionData); + } + + function testFuzz_pushHashes(uint16 batchSize) public { + vm.assume(batchSize > 0 && batchSize <= 256); + vm.roll(batchSize + 1); + + OptimismPusher optimismPusher = new OptimismPusher(mockOpCrosschainDomainMessenger); + + bytes memory l2TransactionData = abi.encode(OptimismPusher.OptimismL2Transaction({gasLimit: 200000})); + + vm.prank(user); + optimismPusher.pushHashes(buffer, block.number - batchSize, batchSize, l2TransactionData); + } + + function testFuzz_pushHashes_invalidBatchSize(uint16 batchSize) public { + vm.assume(batchSize == 0 || batchSize > 8191); + vm.roll(uint32(batchSize) + 1); // uint32 to avoid overflow + + OptimismPusher optimismPusher = new OptimismPusher(mockOpCrosschainDomainMessenger); + + bytes memory l2TransactionData = abi.encode(OptimismPusher.OptimismL2Transaction({gasLimit: 200000})); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(IPusher.InvalidBatch.selector, block.number - batchSize, batchSize)); + optimismPusher.pushHashes(buffer, block.number - batchSize, batchSize, l2TransactionData); + } + + function test_constructor_reverts_if_l1_cross_domain_messenger_proxy_is_zero_address() public { + vm.expectRevert(abi.encodeWithSelector(OptimismPusher.InvalidL1CrossDomainMessengerProxyAddress.selector)); + new OptimismPusher(address(0)); + } + + function test_viewFunctions() public { + OptimismPusher optimismPusher = new OptimismPusher(mockOpCrosschainDomainMessenger); + assertEq(optimismPusher.l1CrossDomainMessengerProxy(), mockOpCrosschainDomainMessenger); + } +} From e61cc92614d0349a85da1c3403547599fdd9969f Mon Sep 17 00:00:00 2001 From: Luiz Date: Thu, 19 Feb 2026 11:29:43 -0300 Subject: [PATCH 2/8] up --- test/block-hash-pusher/optimism/OptimismBuffer.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/block-hash-pusher/optimism/OptimismBuffer.t.sol b/test/block-hash-pusher/optimism/OptimismBuffer.t.sol index 688f325..866bd28 100644 --- a/test/block-hash-pusher/optimism/OptimismBuffer.t.sol +++ b/test/block-hash-pusher/optimism/OptimismBuffer.t.sol @@ -73,7 +73,7 @@ contract OptimismBufferTest is Test { new OptimismBuffer(address(mockOpCrosschainDomainMessenger), address(0)); } - function test_constructor_reverts_if_l2_scroll_messenger_is_zero_address() public { + function test_constructor_reverts_if_l2_cross_domain_messenger_is_zero_address() public { vm.expectRevert(abi.encodeWithSelector(OptimismBuffer.InvalidL2CrossDomainMessengerAddress.selector)); new OptimismBuffer(address(0), pusher); } From b9ca49b2f8ad30409dd7f712c2427a0a4132bfc5 Mon Sep 17 00:00:00 2001 From: Luiz Date: Fri, 20 Feb 2026 17:26:52 -0300 Subject: [PATCH 3/8] up --- src/contracts/block-hash-pusher/optimism/OptimismPusher.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol b/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol index da41879..1d781ca 100644 --- a/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol +++ b/src/contracts/block-hash-pusher/optimism/OptimismPusher.sol @@ -26,9 +26,7 @@ contract OptimismPusher is BlockHashArrayBuilder, IPusher { error InvalidL1CrossDomainMessengerProxyAddress(); constructor(address l1CrossDomainMessengerProxy_) { - if (l1CrossDomainMessengerProxy_ == address(0)) { - revert InvalidL1CrossDomainMessengerProxyAddress(); - } + require(l1CrossDomainMessengerProxy_ != address(0), InvalidL1CrossDomainMessengerProxyAddress()); _l1CrossDomainMessengerProxy = l1CrossDomainMessengerProxy_; } From 9aa1c09eb2ca3e590524f0514b5ad627f37be034 Mon Sep 17 00:00:00 2001 From: Luiz Date: Fri, 20 Feb 2026 18:25:57 -0300 Subject: [PATCH 4/8] update prover to use buffer --- snapshots/verifyBroadcastMessage.json | 4 +- .../provers/optimism/ChildToParentProver.sol | 68 ++++---- test/Receiver.t.sol | 67 ++++---- test/VerifyBroadcastMessageBenchmark.t.sol | 33 ++-- .../optimism/ChildToParentProver.t.sol | 151 ++---------------- test/provers/zksync/ChildToParentProver.t.sol | 9 -- 6 files changed, 104 insertions(+), 228 deletions(-) diff --git a/snapshots/verifyBroadcastMessage.json b/snapshots/verifyBroadcastMessage.json index 8c32d44..632116f 100644 --- a/snapshots/verifyBroadcastMessage.json +++ b/snapshots/verifyBroadcastMessage.json @@ -1,9 +1,9 @@ { - "EthereumToOptimism": "1639195", + "EthereumToOptimism": "1642409", "EthereumToTaikoL2": "1052214", "LineaL2ToEthereum": "2551776", "ScrollL2ToEthereum": "1361940", - "ScrollToOptimism": "1348688", + "ScrollToOptimism": "1351832", "TaikoL2ToEthereum": "1022277", "ZkSyncL2ToEthereum": "125479" } \ No newline at end of file diff --git a/src/contracts/provers/optimism/ChildToParentProver.sol b/src/contracts/provers/optimism/ChildToParentProver.sol index acef4fc..0396187 100644 --- a/src/contracts/provers/optimism/ChildToParentProver.sol +++ b/src/contracts/provers/optimism/ChildToParentProver.sol @@ -3,36 +3,34 @@ pragma solidity 0.8.30; import {ProverUtils} from "../../libraries/ProverUtils.sol"; import {IStateProver} from "../../interfaces/IStateProver.sol"; +import {IBuffer} from "../../block-hash-pusher/interfaces/IBuffer.sol"; +import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; -interface IL1Block { - function hash() external view returns (bytes32); -} - -/// @notice OP-stack implementation of a child to parent IStateProver. -/// @dev verifyTargetStateCommitment and getTargetStateCommitment get block hashes from the L1Block predeploy. -/// verifyStorageSlot is implemented to work against any target chain with a standard Ethereum block header and state trie. -/// -/// @dev Note: L1Block only stores the LATEST L1 block hash. -/// Historical messages CAN be verified by generating fresh proofs on-demand. -/// Pre-generated proofs become stale when L1Block updates (~5 minutes). -/// Operational difference from Arbitrum: proofs must be generated just-in-time rather than pre-cached. +/// @notice OP-Stack implementation of a child to parent IStateProver. +/// @dev verifyTargetStateCommitment and getTargetStateCommitment get block hashes from the block hash buffer. +/// See https://github.com/openintentsframework/broadcaster/blob/main/src/contracts/block-hash-pusher for more details. +/// verifyStorageSlot is implemented to work against any parent chain with a standard Ethereum block header and state trie. contract ChildToParentProver is IStateProver { - address public constant l1BlockPredeploy = 0x4200000000000000000000000000000000000015; - uint256 public constant l1BlockHashSlot = 2; // hash is at slot 2 + /// @dev Address of the block hash buffer contract. + address public immutable blockHashBuffer; + /// @dev Storage slot the buffer contract uses to store block hashes. + /// See https://github.com/openintentsframework/broadcaster/blob/main/src/contracts/block-hash-pusher/BaseBuffer.sol + uint256 public constant blockHashMappingSlot = 1; - /// @dev The chain ID of the home chain (Optimism L2) + /// @dev The chain ID of the home chain (child chain). uint256 public immutable homeChainId; error CallNotOnHomeChain(); error CallOnHomeChain(); - constructor(uint256 _homeChainId) { + constructor(address _blockHashBuffer, uint256 _homeChainId) { + blockHashBuffer = _blockHashBuffer; homeChainId = _homeChainId; } - /// @notice Verify the latest available target block hash given a home chain block hash and a storage proof of the L1Block predeploy. + /// @notice Get a parent chain block hash from the buffer at `blockHashBuffer` using a storage proof /// @param homeBlockHash The block hash of the home chain. - /// @param input ABI encoded (bytes blockHeader, bytes accountProof, bytes storageProof) + /// @param input ABI encoded (bytes blockHeader, uint256 targetBlockNumber, bytes accountProof, bytes storageProof) function verifyTargetStateCommitment(bytes32 homeBlockHash, bytes calldata input) external view @@ -41,33 +39,31 @@ contract ChildToParentProver is IStateProver { if (block.chainid == homeChainId) { revert CallOnHomeChain(); } - // decode the input - bytes memory rlpBlockHeader; - bytes memory accountProof; - bytes memory storageProof; - (rlpBlockHeader, accountProof, storageProof) = abi.decode(input, (bytes, bytes, bytes)); + (bytes memory rlpBlockHeader, uint256 targetBlockNumber, bytes memory accountProof, bytes memory storageProof) = + abi.decode(input, (bytes, uint256, bytes, bytes)); - // verify proofs and get the value + // calculate the slot based on the provided block number + // see: https://github.com/OffchainLabs/block-hash-pusher/blob/a1e26f2e42e6306d1e7f03c5d20fa6aa64ff7a12/contracts/Buffer.sol#L32 + uint256 slot = uint256(SlotDerivation.deriveMapping(bytes32(blockHashMappingSlot), targetBlockNumber)); + + // verify proofs and get the block hash targetStateCommitment = ProverUtils.getSlotFromBlockHeader( - homeBlockHash, rlpBlockHeader, l1BlockPredeploy, l1BlockHashSlot, accountProof, storageProof + homeBlockHash, rlpBlockHeader, blockHashBuffer, slot, accountProof, storageProof ); } - /// @notice Get the latest parent chain block hash from the L1Block predeploy. Bytes argument is ignored. - /// @dev OP stack does not provide access to historical block hashes, so this function can only return the latest. - /// - /// Calls to the Receiver contract could revert because proofs can become stale after the predeploy's block hash is updated. - /// In this case, failing calls may need to be retried with a new proof. - /// - /// If the L1Block is consistently updated too frequently, calls to the Receiver may be DoS'd. - /// In this case, this prover contract may need to be modified to use a different source of block hashes, - /// such as a backup contract that calls the L1Block predeploy and caches the latest block hash. - function getTargetStateCommitment(bytes calldata) external view returns (bytes32 targetStateCommitment) { + /// @notice Get a parent chain block hash from the buffer at `blockHashBuffer`. + /// @param input ABI encoded (uint256 targetBlockNumber) + function getTargetStateCommitment(bytes calldata input) external view returns (bytes32 targetStateCommitment) { if (block.chainid != homeChainId) { revert CallNotOnHomeChain(); } - return IL1Block(l1BlockPredeploy).hash(); + // decode the input + uint256 targetBlockNumber = abi.decode(input, (uint256)); + + // get the block hash from the buffer + targetStateCommitment = IBuffer(blockHashBuffer).parentChainBlockHash(targetBlockNumber); } /// @notice Verify a storage slot given a target chain block hash and a proof. diff --git a/test/Receiver.t.sol b/test/Receiver.t.sol index 2def15b..4443e35 100644 --- a/test/Receiver.t.sol +++ b/test/Receiver.t.sol @@ -251,7 +251,8 @@ contract ReceiverTest is Test { vm.selectFork(optimismForkId); receiver = new Receiver(); - OPChildToParentProver childToParentProver = new OPChildToParentProver(block.chainid); + address blockHashBuffer = address(new BufferMock()); + OPChildToParentProver childToParentProver = new OPChildToParentProver(blockHashBuffer, block.chainid); StateProverPointer stateProverPointer = new StateProverPointer(owner); @@ -282,18 +283,17 @@ contract ReceiverTest is Test { assertEq(blockHash, expectedBlockHash); bytes memory input = abi.encode(rlpBlockHeader, account, slot, rlpAccountProof, rlpStorageProof); - IL1Block l1Block = IL1Block(childToParentProver.l1BlockPredeploy()); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; - vm.prank(l1Block.DEPOSITOR_ACCOUNT()); - l1Block.setL1BlockValues( - uint64(blockNumber), uint64(block.timestamp), block.basefee, blockHash, 0, bytes32(0), 0, 0 - ); + buffer.receiveHashes(blockNumber, blockHashes); address[] memory route = new address[](1); route[0] = address(stateProverPointer); bytes[] memory scpInputs = new bytes[](1); - scpInputs[0] = bytes(""); + scpInputs[0] = abi.encode(blockNumber); bytes memory storageProofToLastProver = input; @@ -325,7 +325,8 @@ contract ReceiverTest is Test { receiver = new Receiver(); - OPChildToParentProver childToParentProver = new OPChildToParentProver(block.chainid); + address blockHashBuffer = address(new BufferMock()); + OPChildToParentProver childToParentProver = new OPChildToParentProver(blockHashBuffer, block.chainid); StateProverPointer stateProverPointer = new StateProverPointer(owner); @@ -353,18 +354,17 @@ contract ReceiverTest is Test { assertEq(blockHash, expectedBlockHash); bytes memory input = abi.encode(rlpBlockHeader, account, slot, rlpAccountProof, rlpStorageProof); - IL1Block l1Block = IL1Block(childToParentProver.l1BlockPredeploy()); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; - vm.prank(l1Block.DEPOSITOR_ACCOUNT()); - l1Block.setL1BlockValues( - uint64(blockNumber), uint64(block.timestamp), block.basefee, blockHash, 0, bytes32(0), 0, 0 - ); + buffer.receiveHashes(blockNumber, blockHashes); address[] memory route = new address[](1); route[0] = address(stateProverPointer); bytes[] memory scpInputs = new bytes[](1); - scpInputs[0] = bytes(""); + scpInputs[0] = abi.encode(blockNumber); bytes memory storageProofToLastProver = input; @@ -398,7 +398,8 @@ contract ReceiverTest is Test { receiver = new Receiver(); - OPChildToParentProver childToParentProver = new OPChildToParentProver(block.chainid); + address blockHashBuffer = address(new BufferMock()); + OPChildToParentProver childToParentProver = new OPChildToParentProver(blockHashBuffer, block.chainid); StateProverPointer stateProverPointer = new StateProverPointer(owner); @@ -426,18 +427,17 @@ contract ReceiverTest is Test { assertEq(blockHash, expectedBlockHash); bytes memory input = abi.encode(rlpBlockHeader, account, slot, rlpAccountProof, rlpStorageProof); - IL1Block l1Block = IL1Block(childToParentProver.l1BlockPredeploy()); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; - vm.prank(l1Block.DEPOSITOR_ACCOUNT()); - l1Block.setL1BlockValues( - uint64(blockNumber), uint64(block.timestamp), block.basefee, blockHash, 0, bytes32(0), 0, 0 - ); + buffer.receiveHashes(blockNumber, blockHashes); address[] memory route = new address[](1); route[0] = address(stateProverPointer); bytes[] memory scpInputs = new bytes[](1); - scpInputs[0] = bytes(""); + scpInputs[0] = abi.encode(blockNumber); bytes memory storageProofToLastProver = input; @@ -455,7 +455,8 @@ contract ReceiverTest is Test { receiver = new Receiver(); - OPChildToParentProver childToParentProver = new OPChildToParentProver(block.chainid); + address blockHashBuffer = address(new BufferMock()); + OPChildToParentProver childToParentProver = new OPChildToParentProver(blockHashBuffer, block.chainid); StateProverPointer stateProverPointer = new StateProverPointer(owner); @@ -491,18 +492,17 @@ contract ReceiverTest is Test { bytes memory inputForOPChildToParentProver = abi.encode(rlpBlockHeader, account, slot, rlpAccountProof, rlpStorageProof); - IL1Block l1Block = IL1Block(childToParentProver.l1BlockPredeploy()); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; - vm.prank(l1Block.DEPOSITOR_ACCOUNT()); - l1Block.setL1BlockValues( - uint64(blockNumber), uint64(block.timestamp), block.basefee, blockHash, 0, bytes32(0), 0, 0 - ); + buffer.receiveHashes(blockNumber, blockHashes); address[] memory route = new address[](1); route[0] = address(stateProverPointer); bytes[] memory scpInputs = new bytes[](1); - scpInputs[0] = bytes(""); + scpInputs[0] = abi.encode(blockNumber); bytes memory storageProofToLastProver = inputForOPChildToParentProver; @@ -555,18 +555,17 @@ contract ReceiverTest is Test { bytes memory rlpAccountProofArbitrum = jsonArbitrum.readBytes(".rlpAccountProof"); bytes memory rlpStorageProofArbitrum = jsonArbitrum.readBytes(".rlpStorageProof"); - IL1Block l1Block = IL1Block(childToParentProver.l1BlockPredeploy()); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHashEthereum; - vm.prank(l1Block.DEPOSITOR_ACCOUNT()); - l1Block.setL1BlockValues( - uint64(blockNumberEthereum), uint64(block.timestamp), block.basefee, blockHashEthereum, 0, bytes32(0), 0, 0 - ); + buffer.receiveHashes(blockNumberEthereum, blockHashes); address[] memory route = new address[](2); route[0] = address(stateProverPointer); route[1] = arbParentToChildProverPointerAddress; - bytes memory input0 = bytes(""); + bytes memory input0 = abi.encode(blockNumberEthereum); bytes memory input1 = abi.encode(rlpBlockHeaderEthereum, sendRootArbitrum, rlpAccountProofEthereum, rlpStorageProofEthereum); diff --git a/test/VerifyBroadcastMessageBenchmark.t.sol b/test/VerifyBroadcastMessageBenchmark.t.sol index 01ad5a1..8804921 100644 --- a/test/VerifyBroadcastMessageBenchmark.t.sol +++ b/test/VerifyBroadcastMessageBenchmark.t.sol @@ -20,6 +20,8 @@ import { } from "../src/contracts/provers/zksync/ParentToChildProver.sol"; import {MockZkChain} from "./provers/zksync/ParentChildToProver.t.sol"; +import {IBuffer} from "../src/contracts/block-hash-pusher/interfaces/IBuffer.sol"; +import {BufferMock} from "./mocks/BufferMock.sol"; /** * @title verifyBroadcastMessage Gas Benchmarks @@ -348,7 +350,8 @@ contract VerifyBroadcastMessageBenchmark is Test { vm.chainId(L2_CHAIN_ID); receiver = new Receiver(); - OptimismC2P prover = new OptimismC2P(L2_CHAIN_ID); + address blockHashBuffer = address(new BufferMock()); + OptimismC2P prover = new OptimismC2P(blockHashBuffer, L2_CHAIN_ID); StateProverPointer pointer = new StateProverPointer(owner); vm.prank(owner); @@ -368,10 +371,11 @@ contract VerifyBroadcastMessageBenchmark is Test { bytes32 message = 0x0000000000000000000000000000000000000000000000000000000074657374; address publisher = 0x9a56fFd72F4B526c523C733F1F74197A51c495E1; - // Mock L1Block predeploy - address l1Block = prover.l1BlockPredeploy(); - vm.mockCall(l1Block, abi.encodeWithSignature("hash()"), abi.encode(blockHash)); - vm.mockCall(l1Block, abi.encodeWithSignature("number()"), abi.encode(blockNumber)); + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; + + buffer.receiveHashes(blockNumber, blockHashes); bytes memory storageProof = abi.encode(rlpBlockHeader, account, slot, rlpAccountProof, rlpStorageProof); @@ -379,12 +383,12 @@ contract VerifyBroadcastMessageBenchmark is Test { route[0] = address(pointer); bytes[] memory scpInputs = new bytes[](1); - scpInputs[0] = bytes(""); + scpInputs[0] = abi.encode(blockNumber); IReceiver.RemoteReadArgs memory args = IReceiver.RemoteReadArgs({route: route, scpInputs: scpInputs, proof: storageProof}); - // Call and snapshot + //Call and snapshot receiver.verifyBroadcastMessage(args, message, publisher); vm.snapshotGasLastCall("verifyBroadcastMessage", "EthereumToOptimism"); } @@ -408,7 +412,8 @@ contract VerifyBroadcastMessageBenchmark is Test { receiver = new Receiver(); // First hop prover: Optimism C2P (gets Ethereum block hash on OP L2) - OptimismC2P opC2PProver = new OptimismC2P(OP_L2_CHAIN_ID); + address blockHashBuffer = address(new BufferMock()); + OptimismC2P opC2PProver = new OptimismC2P(blockHashBuffer, OP_L2_CHAIN_ID); StateProverPointer opPointer = new StateProverPointer(owner); vm.prank(owner); @@ -434,10 +439,12 @@ contract VerifyBroadcastMessageBenchmark is Test { uint256 ethBlockNumber = ethJson.readUint(".blockNumber"); bytes32 ethBlockHash = ethJson.readBytes32(".blockHash"); - // Mock L1Block predeploy for first hop - address l1Block = opC2PProver.l1BlockPredeploy(); - vm.mockCall(l1Block, abi.encodeWithSignature("hash()"), abi.encode(ethBlockHash)); - vm.mockCall(l1Block, abi.encodeWithSignature("number()"), abi.encode(ethBlockNumber)); + // Mock Ethereum block hash + IBuffer buffer = IBuffer(blockHashBuffer); + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = ethBlockHash; + + buffer.receiveHashes(ethBlockNumber, blockHashes); // Load Scroll proof for second hop string memory scrollJson = vm.readFile("test/payloads/scroll/e2e-proof.json"); @@ -463,7 +470,7 @@ contract VerifyBroadcastMessageBenchmark is Test { vm.startSnapshotGas("verifyBroadcastMessage", "ScrollToOptimism"); // First: get Ethereum block hash via OP C2P (simulates first hop verification) - opC2PProver.getTargetStateCommitment(bytes("")); + opC2PProver.getTargetStateCommitment(abi.encode(ethBlockNumber)); // Second: verify Scroll storage using Scroll P2C (simulates second hop verification) scrollP2CProverCopy.verifyStorageSlot(scrollStateRoot, scrollStorageProof); diff --git a/test/provers/optimism/ChildToParentProver.t.sol b/test/provers/optimism/ChildToParentProver.t.sol index 53e0e63..4353427 100644 --- a/test/provers/optimism/ChildToParentProver.t.sol +++ b/test/provers/optimism/ChildToParentProver.t.sol @@ -3,8 +3,12 @@ pragma solidity 0.8.30; import {console, Test} from "forge-std/Test.sol"; import {stdJson} from "forge-std/StdJson.sol"; -import {ChildToParentProver} from "../../../src/contracts/provers/optimism/ChildToParentProver.sol"; +import {Broadcaster} from "../../../src/contracts/Broadcaster.sol"; +import {IBroadcaster} from "../../../src/contracts/interfaces/IBroadcaster.sol"; +import {ChildToParentProver} from "../../../src/contracts/provers/zksync/ChildToParentProver.sol"; import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; +import {IBuffer} from "../../../src/contracts/block-hash-pusher/interfaces/IBuffer.sol"; +import {BufferMock} from "../../mocks/BufferMock.sol"; /** * @title Optimism ChildToParentProver Test @@ -21,6 +25,8 @@ contract OptimismChildToParentProverTest is Test { ChildToParentProver public childToParentProver; // Home is Optimism, Target is Ethereum uint256 public childChainId; + address public blockHashBuffer; + function setUp() public { // Create forks parentForkId = vm.createFork(vm.envString("ETHEREUM_RPC_URL")); // Ethereum Sepolia @@ -29,138 +35,8 @@ contract OptimismChildToParentProverTest is Test { // Deploy prover on Optimism (home chain) vm.selectFork(childForkId); childChainId = block.chainid; - childToParentProver = new ChildToParentProver(childChainId); - } - - function _loadPayload(string memory path) internal view returns (bytes memory payload) { - payload = vm.parseBytes(vm.readFile(string.concat(vm.projectRoot(), "/", path))); - } - - /// @notice Test getTargetStateCommitment() - reads L1Block predeploy on Optimism - /// @dev Uses LIVE data instead of payload files because L1Block updates constantly. - /// This approach is more reliable than static payloads for Optimism. - function test_getTargetStateCommitment() public { - vm.selectFork(childForkId); - - // Read the CURRENT L1 block hash from the predeploy - address l1BlockPredeploy = 0x4200000000000000000000000000000000000015; - bytes32 expectedL1Hash; - - // Call the predeploy directly to get expected value - (bool success, bytes memory data) = l1BlockPredeploy.staticcall(abi.encodeWithSignature("hash()")); - require(success, "Failed to read L1Block predeploy"); - expectedL1Hash = abi.decode(data, (bytes32)); - - // Test our prover returns the same value - bytes32 result = childToParentProver.getTargetStateCommitment(""); - - assertEq(result, expectedL1Hash, "Block hash should match L1Block predeploy"); - assertTrue(result != bytes32(0), "Block hash should not be zero"); - } - - /// @notice Test getTargetStateCommitment() reverts when called on target chain (Ethereum) - function test_reverts_getTargetStateCommitment_on_target_chain() public { - vm.selectFork(parentForkId); - bytes memory payload = _loadPayload("test/payloads/optimism/calldata_get.hex"); - - ChildToParentProver newChildToParentProver = new ChildToParentProver(childChainId); - - assertEq(payload.length, 64); - - bytes32 input; - - assembly { - input := mload(add(payload, 0x20)) - } - - // Should revert because we're on Ethereum, not Optimism - vm.expectRevert(ChildToParentProver.CallNotOnHomeChain.selector); - newChildToParentProver.getTargetStateCommitment(abi.encode(input)); - } - - /// @notice Test verifyTargetStateCommitment() - uses Merkle proofs - /// @dev Currently skipped due to memory allocation issues during proof decoding - /// The underlying Merkle proof verification logic IS tested in Arbitrum tests. - /// Root cause: Likely an ABI decoding issue with the specific proof structure from Optimism. - /// The ProverUtils.getSlotFromBlockHeader() function is identical for both chains. - function skip_test_verifyTargetStateCommitment() public { - vm.selectFork(parentForkId); // Run verification on Ethereum - - bytes memory payload = _loadPayload("test/payloads/optimism/calldata_verify_target.hex"); - - ChildToParentProver childToParentProverCopy = new ChildToParentProver(childChainId); - - assertGt(payload.length, 64, "Payload should be > 64 bytes"); - - bytes32 homeBlockHash; - bytes32 targetStateCommitment; - bytes memory input = Bytes.slice(payload, 64); - - assembly { - homeBlockHash := mload(add(payload, 0x20)) - targetStateCommitment := mload(add(payload, 0x40)) - } - - bytes32 result = childToParentProverCopy.verifyTargetStateCommitment(homeBlockHash, input); - - assertEq(result, targetStateCommitment, "Target block hash should match"); - } - - /// @notice Test verifyTargetStateCommitment() reverts when called on home chain (Optimism) - function test_verifyTargetStateCommitment_reverts_on_home_chain() public { - vm.selectFork(childForkId); // On Optimism (home chain) - - bytes memory payload = _loadPayload("test/payloads/optimism/calldata_verify_target.hex"); - - ChildToParentProver childToParentProverCopy = new ChildToParentProver(childChainId); - - assertGt(payload.length, 64); - - bytes32 homeBlockHash; - bytes memory input = Bytes.slice(payload, 64); - - assembly { - homeBlockHash := mload(add(payload, 0x20)) - } - - // Should revert because we're on Optimism (home chain) - vm.expectRevert(ChildToParentProver.CallOnHomeChain.selector); - childToParentProverCopy.verifyTargetStateCommitment(homeBlockHash, input); - } - - /// @notice Test verifyStorageSlot() - verifies Ethereum storage from Optimism - /// @dev Currently skipped due to memory allocation issues during proof decoding - /// The underlying storage proof verification logic IS tested in Arbitrum tests. - /// Root cause: Same ABI decoding issue as skip_test_verifyTargetStateCommitment. - /// The ProverUtils.getSlotFromBlockHeader() function is identical for both chains. - function skip_test_verifyStorageSlot() public { - vm.selectFork(parentForkId); // Run on Ethereum - - // Known account and slot (from payload generation) - address knownAccount = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; // WETH Sepolia - uint256 knownSlot = 0; - - bytes memory payload = _loadPayload("test/payloads/optimism/calldata_verify_slot.hex"); - - ChildToParentProver childToParentProverCopy = new ChildToParentProver(childChainId); - - assertGt(payload.length, 64, "Payload should be > 64 bytes"); - - bytes32 targetStateCommitment; - bytes32 storageSlotValue; - bytes memory input = Bytes.slice(payload, 64); - - assembly { - targetStateCommitment := mload(add(payload, 0x20)) - storageSlotValue := mload(add(payload, 0x40)) - } - - (address account, uint256 slot, bytes32 value) = - childToParentProverCopy.verifyStorageSlot(targetStateCommitment, input); - - assertEq(account, knownAccount, "Account should match"); - assertEq(slot, knownSlot, "Slot should match"); - assertEq(value, storageSlotValue, "Storage value should match"); + blockHashBuffer = address(new BufferMock()); + childToParentProver = new ChildToParentProver(blockHashBuffer, childChainId); } function test_verifyStorageSlot_broadcaster() public { @@ -189,6 +65,13 @@ contract OptimismChildToParentProverTest is Test { assertEq(blockHash, expectedBlockHash); + IBuffer buffer = IBuffer(blockHashBuffer); + + bytes32[] memory blockHashes = new bytes32[](1); + blockHashes[0] = blockHash; + + buffer.receiveHashes(blockNumber, blockHashes); + bytes memory input = abi.encode(rlpBlockHeader, account, expectedSlot, rlpAccountProof, rlpStorageProof); (address actualAccount, uint256 actualSlot, bytes32 actualValue) = @@ -227,7 +110,7 @@ contract OptimismChildToParentProverTest is Test { bytes memory input = abi.encode(rlpBlockHeader, account, expectedSlot, rlpAccountProof, rlpStorageProof); - ChildToParentProver childToParentProverCopy = new ChildToParentProver(childChainId); + ChildToParentProver childToParentProverCopy = new ChildToParentProver(blockHashBuffer, childChainId); (address actualAccount, uint256 actualSlot, bytes32 actualValue) = childToParentProverCopy.verifyStorageSlot(blockHash, input); diff --git a/test/provers/zksync/ChildToParentProver.t.sol b/test/provers/zksync/ChildToParentProver.t.sol index 71f3d80..be11c39 100644 --- a/test/provers/zksync/ChildToParentProver.t.sol +++ b/test/provers/zksync/ChildToParentProver.t.sol @@ -5,28 +5,19 @@ import {console, Test} from "forge-std/Test.sol"; import {stdJson} from "forge-std/StdJson.sol"; import {Broadcaster} from "../../../src/contracts/Broadcaster.sol"; import {IBroadcaster} from "../../../src/contracts/interfaces/IBroadcaster.sol"; -import {IOutbox} from "@arbitrum/nitro-contracts/src/bridge/IOutbox.sol"; import {ChildToParentProver} from "../../../src/contracts/provers/zksync/ChildToParentProver.sol"; import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; import {IBuffer} from "../../../src/contracts/block-hash-pusher/interfaces/IBuffer.sol"; import {BufferMock} from "../../mocks/BufferMock.sol"; -import {RLP} from "@openzeppelin/contracts/utils/RLP.sol"; -import {BlockHeaders} from "../../utils/BlockHeaders.sol"; contract ZksyncChildToParentProverTest is Test { using stdJson for string; - using RLP for RLP.Encoder; using Bytes for bytes; uint256 public parentForkId; uint256 public childForkId; - IOutbox public outbox = IOutbox(0x65f07C7D521164a4d5DaC6eB8Fac8DA067A3B78F); - - uint256 public rootSlot = 3; - ChildToParentProver public childToParentProver; // Home is Child, Target is Parent - uint256 childChainId; address public blockHashBuffer; From 173b9b15a564e5773f43dc3a19ca90b1cd320561 Mon Sep 17 00:00:00 2001 From: Luiz Date: Mon, 23 Feb 2026 10:55:54 -0300 Subject: [PATCH 5/8] add security contact --- src/contracts/provers/optimism/ChildToParentProver.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contracts/provers/optimism/ChildToParentProver.sol b/src/contracts/provers/optimism/ChildToParentProver.sol index 0396187..02e55dc 100644 --- a/src/contracts/provers/optimism/ChildToParentProver.sol +++ b/src/contracts/provers/optimism/ChildToParentProver.sol @@ -10,6 +10,7 @@ import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol"; /// @dev verifyTargetStateCommitment and getTargetStateCommitment get block hashes from the block hash buffer. /// See https://github.com/openintentsframework/broadcaster/blob/main/src/contracts/block-hash-pusher for more details. /// verifyStorageSlot is implemented to work against any parent chain with a standard Ethereum block header and state trie. +/// @custom:security-contact security@openzeppelin.com contract ChildToParentProver is IStateProver { /// @dev Address of the block hash buffer contract. address public immutable blockHashBuffer; From f32256520b7d7183e3fb6d31dbc59613b8e018e9 Mon Sep 17 00:00:00 2001 From: Luiz Date: Mon, 23 Feb 2026 20:53:06 -0300 Subject: [PATCH 6/8] up --- src/contracts/provers/optimism/ChildToParentProver.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/contracts/provers/optimism/ChildToParentProver.sol b/src/contracts/provers/optimism/ChildToParentProver.sol index 02e55dc..b818dfa 100644 --- a/src/contracts/provers/optimism/ChildToParentProver.sol +++ b/src/contracts/provers/optimism/ChildToParentProver.sol @@ -23,6 +23,7 @@ contract ChildToParentProver is IStateProver { error CallNotOnHomeChain(); error CallOnHomeChain(); + error InvalidTargetStateCommitment(); constructor(address _blockHashBuffer, uint256 _homeChainId) { blockHashBuffer = _blockHashBuffer; @@ -52,6 +53,8 @@ contract ChildToParentProver is IStateProver { targetStateCommitment = ProverUtils.getSlotFromBlockHeader( homeBlockHash, rlpBlockHeader, blockHashBuffer, slot, accountProof, storageProof ); + + require(targetStateCommitment != bytes32(0), InvalidTargetStateCommitment()); } /// @notice Get a parent chain block hash from the buffer at `blockHashBuffer`. @@ -65,6 +68,7 @@ contract ChildToParentProver is IStateProver { // get the block hash from the buffer targetStateCommitment = IBuffer(blockHashBuffer).parentChainBlockHash(targetBlockNumber); + require(targetStateCommitment != bytes32(0), InvalidTargetStateCommitment()); } /// @notice Verify a storage slot given a target chain block hash and a proof. From 66bab412dde04413aaf8e9ea0d8a4af6fda6bb9e Mon Sep 17 00:00:00 2001 From: Luiz Date: Tue, 24 Feb 2026 14:57:42 -0300 Subject: [PATCH 7/8] up --- test/provers/optimism/ChildToParentProver.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/provers/optimism/ChildToParentProver.t.sol b/test/provers/optimism/ChildToParentProver.t.sol index 4353427..4ad7e07 100644 --- a/test/provers/optimism/ChildToParentProver.t.sol +++ b/test/provers/optimism/ChildToParentProver.t.sol @@ -5,7 +5,7 @@ import {console, Test} from "forge-std/Test.sol"; import {stdJson} from "forge-std/StdJson.sol"; import {Broadcaster} from "../../../src/contracts/Broadcaster.sol"; import {IBroadcaster} from "../../../src/contracts/interfaces/IBroadcaster.sol"; -import {ChildToParentProver} from "../../../src/contracts/provers/zksync/ChildToParentProver.sol"; +import {ChildToParentProver} from "../../../src/contracts/provers/optimism/ChildToParentProver.sol"; import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; import {IBuffer} from "../../../src/contracts/block-hash-pusher/interfaces/IBuffer.sol"; import {BufferMock} from "../../mocks/BufferMock.sol"; From 6e254fe4ca7f3b302aaaf707b67e821aa00f6463 Mon Sep 17 00:00:00 2001 From: Luiz Date: Tue, 24 Feb 2026 15:52:44 -0300 Subject: [PATCH 8/8] up --- src/contracts/provers/linea/ChildToParentProver.sol | 2 +- src/contracts/provers/optimism/ChildToParentProver.sol | 2 +- src/contracts/provers/scroll/ChildToParentProver.sol | 2 +- src/contracts/provers/zksync/ChildToParentProver.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/provers/linea/ChildToParentProver.sol b/src/contracts/provers/linea/ChildToParentProver.sol index 424c30a..63364b3 100644 --- a/src/contracts/provers/linea/ChildToParentProver.sol +++ b/src/contracts/provers/linea/ChildToParentProver.sol @@ -45,7 +45,7 @@ contract ChildToParentProver is IStateProver { abi.decode(input, (bytes, uint256, bytes, bytes)); // calculate the slot based on the provided block number - // see: https://github.com/OffchainLabs/block-hash-pusher/blob/a1e26f2e42e6306d1e7f03c5d20fa6aa64ff7a12/contracts/Buffer.sol#L32 + // see: https://github.com/openintentsframework/broadcaster/blob/8d02f8e8e39de27de8f0ded481d3c4e5a129351f/src/contracts/block-hash-pusher/BaseBuffer.sol#L24 uint256 slot = uint256(SlotDerivation.deriveMapping(bytes32(BLOCK_HASH_MAPPING_SLOT), targetBlockNumber)); // verify proofs and get the block hash diff --git a/src/contracts/provers/optimism/ChildToParentProver.sol b/src/contracts/provers/optimism/ChildToParentProver.sol index 87d4d6e..82048dd 100644 --- a/src/contracts/provers/optimism/ChildToParentProver.sol +++ b/src/contracts/provers/optimism/ChildToParentProver.sol @@ -46,7 +46,7 @@ contract ChildToParentProver is IStateProver { abi.decode(input, (bytes, uint256, bytes, bytes)); // calculate the slot based on the provided block number - // see: https://github.com/OffchainLabs/block-hash-pusher/blob/a1e26f2e42e6306d1e7f03c5d20fa6aa64ff7a12/contracts/Buffer.sol#L32 + // see: https://github.com/openintentsframework/broadcaster/blob/8d02f8e8e39de27de8f0ded481d3c4e5a129351f/src/contracts/block-hash-pusher/BaseBuffer.sol#L24 uint256 slot = uint256(SlotDerivation.deriveMapping(bytes32(BLOCK_HASH_MAPPING_SLOT), targetBlockNumber)); // verify proofs and get the block hash diff --git a/src/contracts/provers/scroll/ChildToParentProver.sol b/src/contracts/provers/scroll/ChildToParentProver.sol index fb7d92c..0654258 100644 --- a/src/contracts/provers/scroll/ChildToParentProver.sol +++ b/src/contracts/provers/scroll/ChildToParentProver.sol @@ -46,7 +46,7 @@ contract ChildToParentProver is IStateProver { abi.decode(input, (bytes, uint256, bytes, bytes)); // calculate the slot based on the provided block number - // see: https://github.com/OffchainLabs/block-hash-pusher/blob/a1e26f2e42e6306d1e7f03c5d20fa6aa64ff7a12/contracts/Buffer.sol#L32 + // see: https://github.com/openintentsframework/broadcaster/blob/8d02f8e8e39de27de8f0ded481d3c4e5a129351f/src/contracts/block-hash-pusher/BaseBuffer.sol#L24 uint256 slot = uint256(SlotDerivation.deriveMapping(bytes32(BLOCK_HASH_MAPPING_SLOT), targetBlockNumber)); // verify proofs and get the block hash diff --git a/src/contracts/provers/zksync/ChildToParentProver.sol b/src/contracts/provers/zksync/ChildToParentProver.sol index 3205893..43e42ef 100644 --- a/src/contracts/provers/zksync/ChildToParentProver.sol +++ b/src/contracts/provers/zksync/ChildToParentProver.sol @@ -46,7 +46,7 @@ contract ChildToParentProver is IStateProver { abi.decode(input, (bytes, uint256, bytes, bytes)); // calculate the slot based on the provided block number - // see: https://github.com/OffchainLabs/block-hash-pusher/blob/a1e26f2e42e6306d1e7f03c5d20fa6aa64ff7a12/contracts/Buffer.sol#L32 + // see: https://github.com/openintentsframework/broadcaster/blob/8d02f8e8e39de27de8f0ded481d3c4e5a129351f/src/contracts/block-hash-pusher/BaseBuffer.sol#L24 uint256 slot = uint256(SlotDerivation.deriveMapping(bytes32(BLOCK_HASH_MAPPING_SLOT), targetBlockNumber)); // verify proofs and get the block hash