diff --git a/l1/.env.example b/l1/.env.example index cbf22bf..547f32e 100644 --- a/l1/.env.example +++ b/l1/.env.example @@ -1,6 +1,8 @@ DEPLOY_RPC_URL= STARKNET_CORE_ADDRESS= -L2_RECIPIENT_ADDRESS= +OPTIMISM_L2_OUTPUT_ORACLE_ADDRESS= +L2_RECIPIENT_ADDRESS_FROM_ETHEREUM= +L2_RECIPIENT_ADDRESS_FROM_OPTIMISM= AGGREGATORS_FACTORY_ADDRESS= ETHERSCAN_API_KEY= PRIVATE_KEY= diff --git a/l1/script/L1MessagesSender.s.sol b/l1/script/L1MessagesSender.s.sol index 1a29ec4..cc1e9ac 100644 --- a/l1/script/L1MessagesSender.s.sol +++ b/l1/script/L1MessagesSender.s.sol @@ -6,6 +6,7 @@ import "forge-std/console.sol"; import {L1MessagesSender} from "../src/L1MessagesSender.sol"; import {IStarknetCore} from "../src/interfaces/IStarknetCore.sol"; +import {IOptimismL2OutputOracle} from "../src/interfaces/IOptimismL2OutputOracle.sol"; contract L1MessagesSenderDeployer is Script { function run() external { @@ -14,7 +15,11 @@ contract L1MessagesSenderDeployer is Script { L1MessagesSender l1MessagesSender = new L1MessagesSender( IStarknetCore(vm.envAddress("STARKNET_CORE_ADDRESS")), - vm.envUint("L2_RECIPIENT_ADDRESS"), + IOptimismL2OutputOracle( + vm.envAddress("OPTIMISM_L2_OUTPUT_ORACLE_ADDRESS") + ), + vm.envUint("L2_RECIPIENT_ADDRESS_FROM_ETHEREUM"), + vm.envUint("L2_RECIPIENT_ADDRESS_FROM_OPTIMISM"), vm.envAddress("AGGREGATORS_FACTORY_ADDRESS") ); diff --git a/l1/src/L1MessagesSender.sol b/l1/src/L1MessagesSender.sol index 541c4a0..edbd2f7 100644 --- a/l1/src/L1MessagesSender.sol +++ b/l1/src/L1MessagesSender.sol @@ -5,6 +5,7 @@ import {Ownable} from "openzeppelin/access/Ownable.sol"; import {FormatWords64} from "./lib/FormatWords64.sol"; import {IStarknetCore} from "./interfaces/IStarknetCore.sol"; +import {IOptimismL2OutputOracle} from "./interfaces/IOptimismL2OutputOracle.sol"; import {IAggregatorsFactory} from "./interfaces/IAggregatorsFactory.sol"; import {IAggregator} from "./interfaces/IAggregator.sol"; @@ -15,8 +16,10 @@ contract L1MessagesSender is Ownable { using Uint256Splitter for uint256; IStarknetCore public immutable starknetCore; + IOptimismL2OutputOracle public immutable optimismOutputOracle; - uint256 public l2RecipientAddr; + uint256 public ethereumCommitmentsInboxAddr; + uint256 public optimismCommitmentsInboxAddr; IAggregatorsFactory public aggregatorsFactory; @@ -29,15 +32,21 @@ contract L1MessagesSender is Ownable { 0x36c76e67f1d589956059cbd9e734d42182d1f8a57d5876390bb0fcfe1090bb4; /// @param starknetCore_ a StarknetCore address to send and consume messages on/from L2 - /// @param l2RecipientAddr_ a L2 recipient address that is the recipient contract on L2. + /// @param optimismOutputOracle_ address of the optimism rollup output contract + /// @param ethereumCommitmentsInboxAddr_ a L2 recipient address that is the recipient contract on L2. + /// @param optimismCommitmentsInboxAddr_ a L2 recipient address that is the recipient contract on L2. /// @param aggregatorsFactoryAddr_ Herodotus aggregators factory address (where MMR trees are referenced) constructor( IStarknetCore starknetCore_, - uint256 l2RecipientAddr_, + IOptimismL2OutputOracle optimismOutputOracle_, + uint256 ethereumCommitmentsInboxAddr_, + uint256 optimismCommitmentsInboxAddr_, address aggregatorsFactoryAddr_ ) { starknetCore = starknetCore_; - l2RecipientAddr = l2RecipientAddr_; + optimismOutputOracle = optimismOutputOracle_; + ethereumCommitmentsInboxAddr = ethereumCommitmentsInboxAddr_; + optimismCommitmentsInboxAddr = optimismCommitmentsInboxAddr_; aggregatorsFactory = IAggregatorsFactory(aggregatorsFactoryAddr_); } @@ -47,13 +56,29 @@ contract L1MessagesSender is Ownable { bytes32 parentHash = blockhash(blockNumber_ - 1); require(parentHash != bytes32(0), "ERR_INVALID_BLOCK_NUMBER"); - _sendBlockHashToL2(parentHash, blockNumber_); + _sendBlockHashToL2(parentHash, blockNumber_, ethereumCommitmentsInboxAddr); + } + + // See https://github.com/ethereum-optimism/optimism/blob/0086b6dd4eaa579227607216a83ca0d6a652b264/packages/contracts-bedrock/src/libraries/Hashing.sol#L114 + function sendOptimismBlockhashToL2(uint256 outputIndex_, IOptimismL2OutputOracle.OutputRootProof calldata outputRootPreimage_) external payable { + IOptimismL2OutputOracle.OutputProposal memory outputProposal = optimismOutputOracle.getL2Output(outputIndex_); + bytes32 actualOutputRoot = keccak256( + abi.encode( + outputRootPreimage_.version, + outputRootPreimage_.stateRoot, + outputRootPreimage_.messagePasserStorageRoot, + outputRootPreimage_.latestBlockhash + ) + ); + + require(actualOutputRoot == outputProposal.outputRoot, "ERR_OUTPUT_ROOT_PROOF_INVALID"); + _sendBlockHashToL2(outputRootPreimage_.latestBlockhash, outputProposal.l2BlockNumber, optimismCommitmentsInboxAddr); } /// @notice Send the L1 latest parent hash to L2 function sendLatestParentHashToL2() external payable { bytes32 parentHash = blockhash(block.number - 1); - _sendBlockHashToL2(parentHash, block.number); + _sendBlockHashToL2(parentHash, block.number, ethereumCommitmentsInboxAddr); } /// @param aggregatorId The id of a tree previously created by the aggregators factory @@ -76,7 +101,8 @@ contract L1MessagesSender is Ownable { function _sendBlockHashToL2( bytes32 parentHash_, - uint256 blockNumber_ + uint256 blockNumber_, + uint256 commitmentsInboxAddr_ ) internal { uint256[] memory message = new uint256[](4); (uint256 parentHashLow, uint256 parentHashHigh) = uint256(parentHash_) @@ -89,7 +115,7 @@ contract L1MessagesSender is Ownable { message[3] = blockNumberHigh; starknetCore.sendMessageToL2{value: msg.value}( - l2RecipientAddr, + commitmentsInboxAddr_, RECEIVE_COMMITMENT_L1_HANDLER_SELECTOR, message ); @@ -108,18 +134,26 @@ contract L1MessagesSender is Ownable { // Pass along msg.value starknetCore.sendMessageToL2{value: msg.value}( - l2RecipientAddr, + ethereumCommitmentsInboxAddr, RECEIVE_MMR_L1_HANDLER_SELECTOR, message ); } + + /// @notice Set the L2 recipient address from ethereum + /// @param newethereumCommitmentsInboxAddr_ The new L2 recipient address from ethereum + function setethereumCommitmentsInboxAddr( + uint256 newethereumCommitmentsInboxAddr_ + ) external onlyOwner { + ethereumCommitmentsInboxAddr = newethereumCommitmentsInboxAddr_; + } - /// @notice Set the L2 recipient address - /// @param newL2RecipientAddr_ The new L2 recipient address - function setL2RecipientAddr( - uint256 newL2RecipientAddr_ + /// @notice Set the L2 recipient address from optimism + /// @param newoptimismCommitmentsInboxAddr_ The new L2 recipient address from optimism + function setoptimismCommitmentsInboxAddr( + uint256 newoptimismCommitmentsInboxAddr_ ) external onlyOwner { - l2RecipientAddr = newL2RecipientAddr_; + optimismCommitmentsInboxAddr = newoptimismCommitmentsInboxAddr_; } /// @notice Set the aggregators factory address diff --git a/l1/src/interfaces/IOptimismL2OutputOracle.sol b/l1/src/interfaces/IOptimismL2OutputOracle.sol new file mode 100644 index 0000000..87aff44 --- /dev/null +++ b/l1/src/interfaces/IOptimismL2OutputOracle.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.13; + +interface IOptimismL2OutputOracle { + /** + * @notice OutputProposal represents a commitment to the L2 state. The timestamp is the L1 + * timestamp that the output root is posted. This timestamp is used to verify that the + * finalization period has passed since the output root was submitted. + * + * @custom:field outputRoot Hash of the L2 output. + * @custom:field timestamp Timestamp of the L1 block that the output root was submitted in. + * @custom:field l2BlockNumber L2 block number that the output corresponds to. + */ + struct OutputProposal { + bytes32 outputRoot; + uint128 timestamp; + uint128 l2BlockNumber; + } + + /** + * @notice Struct representing the elements that are hashed together to generate an output root + * which itself represents a snapshot of the L2 state. + * + * @custom:field version Version of the output root. + * @custom:field stateRoot Root of the state trie at the block of this output. + * @custom:field messagePasserStorageRoot Root of the message passer storage trie. + * @custom:field latestBlockhash Hash of the block this output was generated from. + */ + struct OutputRootProof { + bytes32 version; + bytes32 stateRoot; + bytes32 messagePasserStorageRoot; + bytes32 latestBlockhash; + } + + /** + * @notice Returns an output by index. Exists because Solidity's array access will return a + * tuple instead of a struct. + * + * @param _l2OutputIndex Index of the output to return. + * + * @return The output at the given index. + */ + function getL2Output(uint256 _l2OutputIndex) + external + view + returns (OutputProposal memory); +} \ No newline at end of file diff --git a/l1/test/L1MessagesSender.t.sol b/l1/test/L1MessagesSender.t.sol index 3f51e87..a1df0b0 100644 --- a/l1/test/L1MessagesSender.t.sol +++ b/l1/test/L1MessagesSender.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import {L1MessagesSender} from "../src/L1MessagesSender.sol"; import {IStarknetCore} from "../src/interfaces/IStarknetCore.sol"; +import {IOptimismL2OutputOracle} from "../src/interfaces/IOptimismL2OutputOracle.sol"; contract L1MessagesSenderTest is Test { L1MessagesSender public sender; @@ -14,7 +15,9 @@ contract L1MessagesSenderTest is Test { sender = new L1MessagesSender( IStarknetCore(0xde29d060D45901Fb19ED6C6e959EB22d8626708e), + IOptimismL2OutputOracle(0xdfe97868233d1aa22e815a266982f2cf17685a27), 0x07bf6b32382276bFF5341f810A6811233A9591228642F60160129629448a21b6, + 0x047cCc40b58Bb8f4B0c33A4E232478A312a0C9d4e51f8D3A9D9D275f3C167C6A, 0xB8Cb7707b5160eaE8931e0cf02B563a5CeA75F09 ); } diff --git a/multicall/deploy.toml b/multicall/deploy.toml index 308f37f..54ceac7 100644 --- a/multicall/deploy.toml +++ b/multicall/deploy.toml @@ -1,6 +1,6 @@ [[call]] call_type = "deploy" -class_hash = "0x710fb6c249e9c76996f01e170218e31aec7498a8ae618842a0f67b66987c436" +class_hash = "0x06835a4591dc71821bfc769372d2da76e428d5eef7dccc048e036e8ca80aa740" inputs = [ "0x1", "0x2", @@ -12,14 +12,40 @@ unique = false [[call]] call_type = "deploy" -class_hash = "0x4965a6ea3851cde0fd10952802af3300fac851122051c3743644aabe901ee13" +class_hash = "0x06835a4591dc71821bfc769372d2da76e428d5eef7dccc048e036e8ca80aa740" +inputs = [ + "0x1", + "0x2", + "0x0", + "0x007327d012f432a9f940228ae6b01032ea4742d8bba8599d7a34e1d5c120e983", +] +id = "op_commitments_inbox" +unique = false + +[[call]] +call_type = "deploy" +class_hash = "0x2d505685c0bf351d41c0a8d8645c48fa7c1b1ee956da23414e725ff2de35807" inputs = ["0x1"] id = "headers_store" unique = false [[call]] call_type = "deploy" -class_hash = "0xfa77018c244261ab65551a07c0f6e3fab9592027e4cd456b9d074f2b94a9ae" +class_hash = "0x2d505685c0bf351d41c0a8d8645c48fa7c1b1ee956da23414e725ff2de35807" +inputs = ["0x1"] +id = "op_headers_store" +unique = false + +[[call]] +call_type = "deploy" +class_hash = "0x2d22cd79ecdd6eed68fddbe0640454e3b8613be645b961d059c7ea54e5b0fcc" inputs = ["0x1"] id = "evm_facts_registry" unique = false + +[[call]] +call_type = "deploy" +class_hash = "0x2d22cd79ecdd6eed68fddbe0640454e3b8613be645b961d059c7ea54e5b0fcc" +inputs = ["0x1"] +id = "op_facts_registry" +unique = false