diff --git a/cmd/ethrex/build_l2.rs b/cmd/ethrex/build_l2.rs index c67c2976a04..dc6890bd9da 100644 --- a/cmd/ethrex/build_l2.rs +++ b/cmd/ethrex/build_l2.rs @@ -101,6 +101,10 @@ pub fn download_script() { &Path::new("../../crates/l2/contracts/src/l1/CommonBridge.sol"), "CommonBridge", ), + ( + &Path::new("../../crates/l2/contracts/src/l1/Router.sol"), + "Router", + ), ]; for (path, name) in l1_contracts { compile_contract_to_bytecode( @@ -171,6 +175,7 @@ fn write_empty_bytecode_files(output_contracts_path: &Path) { "SP1Verifier", "OnChainProposer", "CommonBridge", + "Router", "CommonBridgeL2", "L2ToL1Messenger", "UpgradeableSystemContract", diff --git a/cmd/ethrex/l2/command.rs b/cmd/ethrex/l2/command.rs index 3616aa5f67a..28028d67213 100644 --- a/cmd/ethrex/l2/command.rs +++ b/cmd/ethrex/l2/command.rs @@ -202,8 +202,8 @@ pub enum Command { #[arg( long, value_parser = parse_private_key, - env = "SEQUENCER_PRIVATE_KEY", - help = "The private key of the sequencer", + env = "SEQUENCER_PRIVATE_KEY", + help = "The private key of the sequencer", help_heading = "Sequencer account options", group = "sequencer_signing", )] @@ -449,7 +449,7 @@ impl Command { .await?; // Get withdrawal hashes - let message_hashes = state_diff + let l1_message_hashes = state_diff .l1_messages .iter() .map(get_l1_message_hash) @@ -488,7 +488,9 @@ impl Command { last_block: new_block.number, state_root: new_block.state_root, privileged_transactions_hash: H256::zero(), - message_hashes, + l1_message_hashes, + // TODO: Check if this is restorable + l2_to_l2_messages: vec![], blobs_bundle: BlobsBundle::empty(), commit_tx: None, verify_tx: None, diff --git a/cmd/ethrex/l2/deployer.rs b/cmd/ethrex/l2/deployer.rs index ffb86e50717..50343b26d20 100644 --- a/cmd/ethrex/l2/deployer.rs +++ b/cmd/ethrex/l2/deployer.rs @@ -333,6 +333,23 @@ pub struct DeployerOptions { help = "The L1 address of the L2 native token (e.g., USDC, USDT, DAI, etc. Use address(0) for ETH)" )] pub native_token_l1_address: Address, + #[arg( + long = "router.deploy", + default_value = "false", + env = "ETHREX_SHARED_BRIDGE_DEPLOY_ROUTER", + help_heading = "Deployer options", + help = "If set, the deployer will deploy the shared bridge router contract. Default to false", + conflicts_with = "router" + )] + pub deploy_router: bool, + #[arg( + long = "router.address", + value_name = "ADDRESS", + env = "ETHREX_SHARED_BRIDGE_ROUTER_ADDRESS", + help_heading = "Deployer options", + help = "The address of the shared bridge router" + )] + pub router: Option
, } impl Default for DeployerOptions { @@ -416,6 +433,8 @@ impl Default for DeployerOptions { inclusion_max_wait: 3000, use_compiled_genesis: true, native_token_l1_address: H160::zero(), + router: None, + deploy_router: false, } } } @@ -459,6 +478,13 @@ pub enum DeployerError { Genesis, } +/// Bytecode of the Router contract. +/// This is generated by the [build script](./build.rs). +const ROUTER_BYTECODE: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/contracts/solc_out/Router.bytecode" +)); + /// Bytecode of the OnChainProposer contract. /// This is generated by the [build script](./build.rs). const ON_CHAIN_PROPOSER_BYTECODE: &[u8] = include_bytes!(concat!( @@ -500,7 +526,9 @@ const INITIALIZE_ON_CHAIN_PROPOSER_SIGNATURE: &str = "initialize(bool,address,ad const INITIALIZE_BRIDGE_ADDRESS_SIGNATURE: &str = "initializeBridgeAddress(address)"; const TRANSFER_OWNERSHIP_SIGNATURE: &str = "transferOwnership(address)"; const ACCEPT_OWNERSHIP_SIGNATURE: &str = "acceptOwnership()"; -const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address)"; +const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,address,address)"; +const ROUTER_INITIALIZER_SIGNATURE: &str = "initialize(address)"; +const ROUTER_REGISTER_SIGNATURE: &str = "register(uint256,address)"; // deposit(uint256 _amount, address _l2Recipient) const NATIVE_TOKEN_DEPOSIT_SIGNATURE: &str = "deposit(uint256,address)"; @@ -517,6 +545,7 @@ pub struct ContractAddresses { pub tdx_verifier_address: Address, pub sequencer_registry_address: Address, pub aligned_aggregator_address: Address, + pub router: Option
, } pub async fn deploy_l1_contracts( @@ -535,9 +564,32 @@ pub async fn deploy_l1_contracts( Some(opts.maximum_allowed_max_fee_per_blob_gas), )?; + let genesis: Genesis = if opts.use_compiled_genesis { + serde_json::from_str(LOCAL_DEVNETL2_GENESIS_CONTENTS).map_err(|_| DeployerError::Genesis)? + } else { + read_genesis_file( + opts.genesis_l2_path + .to_str() + .ok_or(DeployerError::FailedToGetStringFromPath)?, + ) + }; + let contract_addresses = deploy_contracts(ð_client, &opts, &signer).await?; - initialize_contracts(contract_addresses, ð_client, &opts, &signer).await?; + initialize_contracts(contract_addresses, ð_client, &opts, &genesis, &signer).await?; + + if contract_addresses.router.is_some() { + let _ = register_chain( + ð_client, + contract_addresses, + genesis.config.chain_id, + &signer, + ) + .await + .inspect_err(|err| { + warn!(%err, "Could not register chain in shared bridge router"); + }); + } if opts.deposit_rich { if opts.native_token_l1_address != Address::zero() { @@ -573,8 +625,6 @@ async fn deploy_contracts( ) -> Result { trace!("Deploying contracts"); - info!("Deploying OnChainProposer"); - let salt = if opts.randomize_contract_deployment { H256::random().as_bytes().to_vec() } else { @@ -584,6 +634,31 @@ async fn deploy_contracts( .to_vec() }; + let deployed_router = if opts.deploy_router { + info!("Deploying Router"); + + let bytecode = ROUTER_BYTECODE.to_vec(); + if bytecode.is_empty() { + return Err(DeployerError::BytecodeNotFound); + } + + let router_deployment = + deploy_with_proxy_from_bytecode(deployer, eth_client, &bytecode, &salt).await?; + info!( + "Router deployed:\n Proxy -> address={:#x}, tx_hash={:#x}\n Impl -> address={:#x}, tx_hash={:#x}", + router_deployment.proxy_address, + router_deployment.proxy_tx_hash, + router_deployment.implementation_address, + router_deployment.implementation_tx_hash, + ); + + Some(router_deployment.proxy_address) + } else { + None + }; + + info!("Deploying OnChainProposer"); + trace!("Attempting to deploy OnChainProposer contract"); let bytecode = if opts.deploy_based_contracts { ON_CHAIN_PROPOSER_BASED_BYTECODE.to_vec() @@ -696,6 +771,7 @@ async fn deploy_contracts( tdx_verifier_address, sequencer_registry_address: sequencer_registry_deployment.proxy_address, aligned_aggregator_address: opts.aligned_aggregator_address, + router: opts.router.or(deployed_router), }) } @@ -755,22 +831,13 @@ async fn initialize_contracts( contract_addresses: ContractAddresses, eth_client: &EthClient, opts: &DeployerOptions, + genesis: &Genesis, initializer: &Signer, ) -> Result<(), DeployerError> { trace!("Initializing contracts"); trace!(committer_l1_address = %opts.committer_l1_address, "Using committer L1 address for OnChainProposer initialization"); - let genesis: Genesis = if opts.use_compiled_genesis { - serde_json::from_str(LOCAL_DEVNETL2_GENESIS_CONTENTS).map_err(|_| DeployerError::Genesis)? - } else { - read_genesis_file( - opts.genesis_l2_path - .to_str() - .ok_or(DeployerError::FailedToGetStringFromPath)?, - ) - }; - let sp1_vk = read_vk(&opts.sp1_vk_path); let risc0_vk = read_vk(&opts.risc0_vk_path); @@ -834,6 +901,22 @@ async fn initialize_contracts( }; info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "SequencerRegistry initialized"); } else { + if let Some(router) = contract_addresses.router + && opts.deploy_router + { + let calldata_values = vec![Value::Address(deployer_address)]; + let router_initialization_calldata = + encode_calldata(ROUTER_INITIALIZER_SIGNATURE, &calldata_values)?; + let initialize_tx_hash = initialize_contract( + router, + router_initialization_calldata, + initializer, + eth_client, + ) + .await?; + info!(tx_hash = %format!("{initialize_tx_hash:#x}"), "Router initialized"); + } + // Initialize only OnChainProposer without Based config let calldata_values = vec![ Value::Bool(opts.validium), @@ -936,6 +1019,7 @@ async fn initialize_contracts( Value::Address(contract_addresses.on_chain_proposer_address), Value::Uint(opts.inclusion_max_wait.into()), Value::Address(opts.native_token_l1_address), + Value::Address(contract_addresses.router.unwrap_or_default()), ]; let bridge_initialization_calldata = encode_calldata(BRIDGE_INITIALIZER_SIGNATURE, &calldata_values)?; @@ -954,6 +1038,35 @@ async fn initialize_contracts( Ok(()) } +async fn register_chain( + eth_client: &EthClient, + contract_addresses: ContractAddresses, + chain_id: u64, + deployer: &Signer, +) -> Result<(), DeployerError> { + let params = vec![ + Value::Uint(U256::from(chain_id)), + Value::Address(contract_addresses.bridge_address), + ]; + + ethrex_l2_sdk::call_contract( + eth_client, + deployer, + contract_addresses + .router + .ok_or(DeployerError::InternalError( + "Router address is None. This is a bug.".to_string(), + ))?, + ROUTER_REGISTER_SIGNATURE, + params, + ) + .await?; + + info!(chain_id, "Chain registered"); + + Ok(()) +} + async fn make_deposits( bridge: Address, eth_client: &EthClient, @@ -1199,6 +1312,11 @@ fn write_contract_addresses_to_env( "ETHREX_NATIVE_TOKEN_L1_ADDRESS={:#x}", native_token_l1_address )?; + writeln!( + writer, + "ETHREX_SHARED_BRIDGE_ROUTER_ADDRESS={:#x}", + contract_addresses.router.unwrap_or_default() + )?; trace!(?env_file_path, "Contract addresses written to .env"); Ok(()) } diff --git a/crates/common/types/l2.rs b/crates/common/types/l2.rs index d1764247099..47258176fda 100644 --- a/crates/common/types/l2.rs +++ b/crates/common/types/l2.rs @@ -1,2 +1,3 @@ pub mod batch; pub mod fee_config; +pub mod l2_to_l2_message; diff --git a/crates/common/types/l2/batch.rs b/crates/common/types/l2/batch.rs index 379d6ce814e..78de4b808c5 100644 --- a/crates/common/types/l2/batch.rs +++ b/crates/common/types/l2/batch.rs @@ -1,4 +1,7 @@ -use crate::{H256, types::BlobsBundle}; +use crate::{ + H256, + types::{BlobsBundle, l2_to_l2_message::L2toL2Message}, +}; use serde::{Deserialize, Serialize}; #[derive(Clone, Serialize, Deserialize, Debug, Default)] @@ -8,7 +11,8 @@ pub struct Batch { pub last_block: u64, pub state_root: H256, pub privileged_transactions_hash: H256, - pub message_hashes: Vec, + pub l1_message_hashes: Vec, + pub l2_to_l2_messages: Vec, pub blobs_bundle: BlobsBundle, pub commit_tx: Option, pub verify_tx: Option, diff --git a/crates/common/types/l2/l2_to_l2_message.rs b/crates/common/types/l2/l2_to_l2_message.rs new file mode 100644 index 00000000000..1ad4965b41a --- /dev/null +++ b/crates/common/types/l2/l2_to_l2_message.rs @@ -0,0 +1,46 @@ +use bytes::Bytes; +use ethereum_types::{Address, U256}; +use ethrex_rlp::{decode::RLPDecode, structs::Decoder}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +/// Represents a message from the L2 to another L2 +pub struct L2toL2Message { + /// Chain id of the destination chain + pub chain_id: U256, + /// Address that originated the transaction + pub from: Address, + /// Address of the recipient in the destination chain + pub to: Address, + /// Amount of ETH to send to the recipient + pub value: U256, + /// Gas limit for the transaction execution in the destination chain + pub gas_limit: U256, + /// Calldata for the transaction in the destination chain + pub data: Bytes, +} + +impl RLPDecode for L2toL2Message { + fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), ethrex_rlp::error::RLPDecodeError> { + let decoder = Decoder::new(rlp)?; + + let (chain_id, decoder) = decoder.decode_field("chain_id")?; + let (from, decoder) = decoder.decode_field("from")?; + let (to, decoder) = decoder.decode_field("to")?; + let (value, decoder) = decoder.decode_field("value")?; + let (gas_limit, decoder) = decoder.decode_field("gas_limit")?; + let (data, decoder) = decoder.decode_field("data")?; + + Ok(( + L2toL2Message { + chain_id, + from, + to, + value, + gas_limit, + data, + }, + decoder.finish()?, + )) + } +} diff --git a/crates/l2/based/block_fetcher.rs b/crates/l2/based/block_fetcher.rs index 50235ac9b60..6bc542d2393 100644 --- a/crates/l2/based/block_fetcher.rs +++ b/crates/l2/based/block_fetcher.rs @@ -1,6 +1,7 @@ use std::{cmp::min, collections::HashMap, sync::Arc, time::Duration}; use ethrex_blockchain::{Blockchain, fork_choice::apply_fork_choice, vm::StoreVmDatabase}; +use ethrex_common::types::l2_to_l2_message::L2toL2Message; use ethrex_common::utils::keccak; use ethrex_common::{ Address, H160, H256, U256, @@ -8,6 +9,7 @@ use ethrex_common::{ AccountUpdate, Block, BlockNumber, PrivilegedL2Transaction, Transaction, batch::Batch, }, }; +use ethrex_l2_common::l1_messages::get_l2_to_l2_messages; use ethrex_l2_common::{ l1_messages::{L1Message, get_block_l1_messages, get_l1_message_hash}, privileged_transactions::compute_privileged_transactions_hash, @@ -332,7 +334,7 @@ impl BlockFetcher { .collect(); let mut messages = Vec::new(); for block in batch { - let block_messages = self.extract_block_messages(block.header.number).await?; + let (block_messages, _) = self.extract_block_messages(block.header.number).await?; messages.extend(block_messages); } let privileged_transactions_hash = @@ -391,40 +393,46 @@ impl BlockFetcher { let (blobs_bundle, _) = generate_blobs_bundle(&state_diff).map_err(|_| BlockFetcherError::BlobBundleError)?; + let (l1_message_hashes, l2_to_l2_messages) = self.get_batch_messages(batch).await?; + Ok(Batch { number: batch_number.as_u64(), first_block: first_block.header.number, last_block: last_block.header.number, state_root: new_state_root, privileged_transactions_hash, - message_hashes: self.get_batch_message_hashes(batch).await?, + l1_message_hashes, + l2_to_l2_messages, blobs_bundle, commit_tx: Some(commit_tx), verify_tx: None, }) } - async fn get_batch_message_hashes( + async fn get_batch_messages( &mut self, batch: &[Block], - ) -> Result, BlockFetcherError> { + ) -> Result<(Vec, Vec), BlockFetcherError> { let mut message_hashes = Vec::new(); + let mut l2_to_l2_messages = Vec::new(); for block in batch { - let block_messages = self.extract_block_messages(block.header.number).await?; + let (block_messages, block_l2_to_l2_messages) = + self.extract_block_messages(block.header.number).await?; + l2_to_l2_messages.extend(block_l2_to_l2_messages); for msg in &block_messages { message_hashes.push(get_l1_message_hash(msg)); } } - Ok(message_hashes) + Ok((message_hashes, l2_to_l2_messages)) } async fn extract_block_messages( &mut self, block_number: BlockNumber, - ) -> Result, BlockFetcherError> { + ) -> Result<(Vec, Vec), BlockFetcherError> { let Some(block_body) = self.store.get_block_body(block_number).await? else { return Err(BlockFetcherError::InconsistentStorage(format!( "Block {block_number} is supposed to be in store at this point" @@ -451,7 +459,10 @@ impl BlockFetcher { txs.push(tx.clone()); receipts.push(receipt); } - Ok(get_block_l1_messages(&receipts)) + Ok(( + get_block_l1_messages(&receipts), + get_l2_to_l2_messages(&receipts), + )) } /// Process the logs from the event `BatchVerified`. diff --git a/crates/l2/common/src/l1_messages.rs b/crates/l2/common/src/l1_messages.rs index 1724dab3267..2dc105008c0 100644 --- a/crates/l2/common/src/l1_messages.rs +++ b/crates/l2/common/src/l1_messages.rs @@ -1,16 +1,29 @@ -use std::sync::LazyLock; - +use bytes::Bytes; use ethereum_types::{Address, H256}; +use ethrex_common::types::l2_to_l2_message::L2toL2Message; use ethrex_common::utils::keccak; use ethrex_common::{H160, U256, types::Receipt}; use serde::{Deserialize, Serialize}; +use crate::calldata::Value; + pub const L1MESSENGER_ADDRESS: Address = H160([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, ]); +// keccak256("L1Message(address,bytes32,uint256)") +static L1MESSAGE_EVENT_SELECTOR: H256 = H256([ + 0x18, 0xd7, 0xb7, 0x05, 0x34, 0x4d, 0x61, 0x6d, 0x1b, 0x61, 0xda, 0xa6, 0xa8, 0xcc, 0xfc, 0xf9, + 0xf1, 0x0c, 0x27, 0xad, 0xe0, 0x07, 0xcc, 0x45, 0xcf, 0x87, 0x0d, 0x1e, 0x12, 0x1f, 0x1a, 0x9d, +]); +// keccak256("L2ToL2Message(uint256,address,address,uint256,uint256,bytes)") +static L2_MESSAGE_SELECTOR: H256 = H256([ + 0x09, 0xdb, 0x04, 0xf0, 0x10, 0xf1, 0x0e, 0xf2, 0x0f, 0xce, 0xf9, 0xca, 0xe9, 0xf6, 0x4a, 0xbb, + 0xde, 0x92, 0xfe, 0xe1, 0x2c, 0x68, 0xf6, 0x92, 0xc2, 0x3a, 0x72, 0xcc, 0x54, 0xb2, 0x96, 0x9e, +]); + #[derive(Serialize, Deserialize, Debug)] pub struct L1MessageProof { pub batch_number: u64, @@ -52,9 +65,6 @@ pub fn get_block_l1_message_hashes(receipts: &[Receipt]) -> Vec { } pub fn get_block_l1_messages(receipts: &[Receipt]) -> Vec { - static L1MESSAGE_EVENT_SELECTOR: LazyLock = - LazyLock::new(|| keccak("L1Message(address,bytes32,uint256)".as_bytes())); - receipts .iter() .flat_map(|receipt| { @@ -75,3 +85,59 @@ pub fn get_block_l1_messages(receipts: &[Receipt]) -> Vec { }) .collect() } + +pub fn get_l2_to_l2_messages(receipts: &[Receipt]) -> Vec { + receipts + .iter() + .flat_map(|receipt| { + receipt + .logs + .iter() + .filter(|log| { + log.address == L1MESSENGER_ADDRESS && log.topics.contains(&L2_MESSAGE_SELECTOR) + }) + .flat_map(|log| l2_message_from_log_data(&log.data)) + }) + .collect() +} + +fn l2_message_from_log_data(log_data: &[u8]) -> Option { + let mut offset = 0; + + let chain_id = U256::from_big_endian(log_data.get(offset..offset + 32)?); + offset += 32; + + let from = Address::from_slice(log_data.get(offset + 12..offset + 32)?); + offset += 32; + + let to = Address::from_slice(log_data.get(offset + 12..offset + 32)?); + offset += 32; + + let value = U256::from_big_endian(log_data.get(offset..offset + 32)?); + offset += 32; + + let gas_limit = U256::from_big_endian(log_data.get(offset..offset + 32)?); + offset += 64; // 32 from gas_limit + 32 from data offset + + let data_len: usize = U256::from_big_endian(log_data.get(offset..offset + 32)?).as_usize(); + let data = Bytes::copy_from_slice(log_data.get(offset + 32..offset + 32 + data_len)?); + + Some(L2toL2Message { + chain_id, + from, + to, + value, + gas_limit, + data, + }) +} + +pub fn value_from_l2_to_l2_message(msg: &L2toL2Message) -> Value { + Value::Tuple(vec![ + Value::Uint(msg.chain_id), + Value::Address(msg.to), + Value::Uint(msg.value), + Value::Uint(msg.gas_limit), + Value::Bytes(msg.data.clone()), + ]) +} diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index 0026ebfeb9c..7f8c1fb366b 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -14,6 +14,7 @@ import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProo import "./interfaces/ICommonBridge.sol"; import "./interfaces/IOnChainProposer.sol"; import "../l2/interfaces/ICommonBridgeL2.sol"; +import {IRouter} from "./interfaces/IRouter.sol"; /// @title CommonBridge contract. /// @author LambdaClass @@ -40,7 +41,7 @@ contract CommonBridge is /// @dev The value is the merkle root of the logs. /// @dev If there exist a merkle root for a given batch number it means /// that the logs were published on L1, and that that batch was committed. - mapping(uint256 => bytes32) public batchWithdrawalLogsMerkleRoots; + mapping(uint256 batchNumber => bytes32) public batchWithdrawalLogsMerkleRoots; /// @notice Array of hashed pending privileged transactions bytes32[] public pendingTxHashes; @@ -92,6 +93,8 @@ contract CommonBridge is /// Otherwise, this address is used for native token deposits and withdrawals. address public NATIVE_TOKEN_L1; + address public SHARED_BRIDGE_ROUTER = address(0); + modifier onlyOnChainProposer() { require( msg.sender == ON_CHAIN_PROPOSER, @@ -111,7 +114,8 @@ contract CommonBridge is address owner, address onChainProposer, uint256 inclusionMaxWait, - address _nativeToken + address _nativeToken, + address _sharedBridgeRouter ) public initializer { require( onChainProposer != address(0), @@ -126,6 +130,8 @@ contract CommonBridge is NATIVE_TOKEN_L1 = _nativeToken; + SHARED_BRIDGE_ROUTER = _sharedBridgeRouter; + OwnableUpgradeable.__Ownable_init(owner); ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); } @@ -175,8 +181,6 @@ contract CommonBridge is } function _sendToL2(address from, SendValues memory sendValues) private { - _burnGas(sendValues.gasLimit); - bytes32 l2MintTxHash = keccak256( bytes.concat( bytes20(from), @@ -209,6 +213,7 @@ contract CommonBridge is function sendToL2( SendValues calldata sendValues ) public override whenNotPaused { + _burnGas(sendValues.gasLimit); _sendToL2(_getSenderAlias(), sendValues); } @@ -217,6 +222,11 @@ contract CommonBridge is uint256 _amount, address l2Recipient ) public payable override whenNotPaused { + _burnGas(21000 * 5); + _deposit(_amount, l2Recipient); + } + + function _deposit(uint256 _amount, address l2Recipient) private { uint256 value; // Here we define value depending on whether the native token is ETH or an ERC20 @@ -298,6 +308,7 @@ contract CommonBridge is value: 0, data: callData }); + _burnGas(sendValues.gasLimit); _sendToL2(L2_BRIDGE_ADDRESS, sendValues); } @@ -349,9 +360,9 @@ contract CommonBridge is /// @inheritdoc ICommonBridge function getWithdrawalLogsMerkleRoot( - uint256 blockNumber + uint256 batchNumber ) public view returns (bytes32) { - return batchWithdrawalLogsMerkleRoots[blockNumber]; + return batchWithdrawalLogsMerkleRoots[batchNumber]; } /// @inheritdoc ICommonBridge @@ -364,9 +375,7 @@ contract CommonBridge is bytes32(0), "CommonBridge: withdrawal logs already published" ); - batchWithdrawalLogsMerkleRoots[ - withdrawalLogsBatchNumber - ] = withdrawalsLogsMerkleRoot; + batchWithdrawalLogsMerkleRoots[withdrawalLogsBatchNumber] = withdrawalsLogsMerkleRoot; emit WithdrawalsPublished( withdrawalLogsBatchNumber, withdrawalsLogsMerkleRoot @@ -490,6 +499,33 @@ contract CommonBridge is ); } + /// @inheritdoc ICommonBridge + function sendMessage(uint256 dstChainId, SendValues memory message) public override onlyOnChainProposer { + IRouter(SHARED_BRIDGE_ROUTER).sendMessage{value: message.value}(dstChainId, message); + } + + /// @inheritdoc ICommonBridge + function receiveMessage(SendValues calldata message) public override payable { + require( + msg.sender == SHARED_BRIDGE_ROUTER, + "CommonBridge: caller is not the shared bridge router" + ); + + if (message.data.length == 0) { + require( + message.value == msg.value, + "CommonBridge: message.value does not match msg.value" + ); + _deposit(0, message.to); + } else { + if (message.value != 0) { + _deposit(0, _getSenderAlias()); + } + _sendToL2(_getSenderAlias(), message); + } + + } + function upgradeL2Contract( address l2Contract, address newImplementation, @@ -506,6 +542,7 @@ contract CommonBridge is value: 0, data: callData }); + _burnGas(sendValues.gasLimit); _sendToL2(L2_PROXY_ADMIN, sendValues); } diff --git a/crates/l2/contracts/src/l1/OnChainProposer.sol b/crates/l2/contracts/src/l1/OnChainProposer.sol index 4a2d8ef711e..ae80abd5584 100644 --- a/crates/l2/contracts/src/l1/OnChainProposer.sol +++ b/crates/l2/contracts/src/l1/OnChainProposer.sol @@ -11,6 +11,7 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol"; import {IRiscZeroVerifier} from "./interfaces/IRiscZeroVerifier.sol"; import {ISP1Verifier} from "./interfaces/ISP1Verifier.sol"; import {ITDXVerifier} from "./interfaces/ITDXVerifier.sol"; +import {IRouter} from "./interfaces/IRouter.sol"; /// @title OnChainProposer contract. /// @author LambdaClass @@ -196,6 +197,7 @@ contract OnChainProposer is uint256 batchNumber, bytes32 newStateRoot, bytes32 withdrawalsLogsMerkleRoot, + L2toL2Message[] calldata l2CrossMessages, bytes32 processedPrivilegedTransactionsRollingHash, bytes32 lastBlockHash ) external override onlySequencer whenNotPaused { @@ -224,6 +226,7 @@ contract OnChainProposer is "OnChainProposer: invalid privileged transaction logs" ); } + if (withdrawalsLogsMerkleRoot != bytes32(0)) { ICommonBridge(BRIDGE).publishWithdrawals( batchNumber, @@ -231,6 +234,17 @@ contract OnChainProposer is ); } + for (uint256 i = 0; i < l2CrossMessages.length; i++) { + L2toL2Message calldata message = l2CrossMessages[i]; + ICommonBridge.SendValues memory sendValues = ICommonBridge.SendValues( + message.to, + message.gasLimit, + message.value, + message.data + ); + ICommonBridge(BRIDGE).sendMessage(message.chainId, sendValues); + } + // Blob is published in the (EIP-4844) transaction that calls this function. bytes32 blobVersionedHash = blobhash(0); if (VALIDIUM) { diff --git a/crates/l2/contracts/src/l1/Router.sol b/crates/l2/contracts/src/l1/Router.sol new file mode 100644 index 00000000000..459e9bbc394 --- /dev/null +++ b/crates/l2/contracts/src/l1/Router.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.29; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { IRouter } from "./interfaces/IRouter.sol"; +import { ICommonBridge } from "./interfaces/ICommonBridge.sol"; + +/// @title Router contract. +/// @author LambdaClass +contract Router is + IRouter, + Initializable, + UUPSUpgradeable, + Ownable2StepUpgradeable, + PausableUpgradeable +{ + mapping(uint256 chainId => address bridge) public bridges; + + function initialize(address owner) public initializer { + OwnableUpgradeable.__Ownable_init(owner); + } + + /// @inheritdoc IRouter + function register(uint256 chainId, address _commonBridge) onlyOwner whenNotPaused public { + if (_commonBridge == address(0)) { + revert InvalidAddress(address(0)); + } + + if (bridges[chainId] != address(0)) { + revert ChainAlreadyRegistered(chainId); + } + + bridges[chainId] = _commonBridge; + + emit ChainRegistered(chainId, _commonBridge); + } + + /// @inheritdoc IRouter + function deregister(uint256 chainId) onlyOwner whenNotPaused public { + if (bridges[chainId] == address(0)) { + revert ChainNotRegistered(chainId); + } + + delete bridges[chainId]; + + emit ChainDeregistered(chainId); + } + + /// @inheritdoc IRouter + function sendMessage(uint256 chainId, ICommonBridge.SendValues calldata message) public override payable { + if (bridges[chainId] == address(0)) { + revert ChainNotRegistered(chainId); + } + + ICommonBridge(bridges[chainId]).receiveMessage{value: msg.value}(message); + } + + /// @notice Allow owner to upgrade the contract. + /// @param newImplementation the address of the new implementation + function _authorizeUpgrade( + address newImplementation + ) internal virtual override onlyOwner {} + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } +} diff --git a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol index db1ec114c8e..f90e7bcbc18 100644 --- a/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol +++ b/crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol @@ -87,11 +87,11 @@ interface ICommonBridge { /// @notice Method to retrieve the merkle root of the withdrawal logs of a /// given block. /// @dev This method is used by the L2 OnChainOperator at the verify stage. - /// @param blockNumber the block number in L2 where the withdrawal logs were + /// @param batchNumber the batch number in L2 where the withdrawal logs were /// emitted. /// @return the merkle root of the withdrawal logs of the given block. function getWithdrawalLogsMerkleRoot( - uint256 blockNumber + uint256 batchNumber ) external view returns (bytes32); /// @notice Publishes the L2 withdrawals on L1. @@ -121,11 +121,11 @@ interface ICommonBridge { /// @param withdrawalProof the merkle path to the withdrawal log. /// @param withdrawalLogIndex the index of the message log in the block. /// This is the index of the withdraw transaction relative to the block's messages. - /// @param l2WithdrawalBatchNumber the batch number where the withdrawal log + /// @param withdrawalBatchNumber the batch number where the withdrawal log /// was emitted. function claimWithdrawal( uint256 claimedAmount, - uint256 l2WithdrawalBatchNumber, + uint256 withdrawalBatchNumber, uint256 withdrawalLogIndex, bytes32[] calldata withdrawalProof ) external; @@ -136,17 +136,29 @@ interface ICommonBridge { /// @param claimedAmount the amount that will be claimed. /// @param withdrawalProof the merkle path to the withdrawal log. /// @param withdrawalLogIndex the index of the message log in the batch. - /// @param l2WithdrawalBatchNumber the batch number where the withdrawal log + /// @param withdrawalBatchNumber the batch number where the withdrawal log /// was emitted. function claimWithdrawalERC20( address tokenL1, address tokenL2, uint256 claimedAmount, - uint256 l2WithdrawalBatchNumber, + uint256 withdrawalBatchNumber, uint256 withdrawalLogIndex, bytes32[] calldata withdrawalProof ) external; + /// @notice Sends a message to another chain via shared bridge router. + /// @dev This method should only be called by the OnChainProposer. + /// @param dstChainId The ID of the destination chain. + /// @param message The message details to send. + function sendMessage(uint256 dstChainId, SendValues memory message) external; + + /// @notice Receives a message from another chain via shared bridge router. + /// @dev This method should only be called by the shared bridge router, as this + /// method will not burn the L2 gas. + /// @param message The message details to receive. + function receiveMessage(SendValues memory message) external payable; + /// @notice Checks if the sequencer has exceeded it's processing deadlines function hasExpiredPrivilegedTransactions() external view returns (bool); diff --git a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol index be6696c86da..e798cf67433 100644 --- a/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol +++ b/crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol @@ -14,6 +14,14 @@ interface IOnChainProposer { /// @return The latest verified batch number as a uint256. function lastVerifiedBatch() external view returns (uint256); + struct L2toL2Message { + uint256 chainId; + address to; + uint256 value; + uint256 gasLimit; + bytes data; + } + /// @notice A batch has been committed. /// @dev Event emitted when a batch is committed. /// @param newStateRoot The new state root of the batch that was committed. @@ -62,6 +70,7 @@ interface IOnChainProposer { uint256 batchNumber, bytes32 newStateRoot, bytes32 withdrawalsLogsMerkleRoot, + L2toL2Message[] calldata l2CrossMessages, bytes32 processedPrivilegedTransactionsRollingHash, bytes32 lastBlockHash ) external; diff --git a/crates/l2/contracts/src/l1/interfaces/IRouter.sol b/crates/l2/contracts/src/l1/interfaces/IRouter.sol new file mode 100644 index 00000000000..dafeb0dbe02 --- /dev/null +++ b/crates/l2/contracts/src/l1/interfaces/IRouter.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import { ICommonBridge } from "./ICommonBridge.sol"; + +interface IRouter { + /// @notice Registers a new chain with its OnChainProposer and CommonBridge addresses. + /// @param chainId The ID of the chain to register. + /// @param commonBridge The address of the CommonBridge for the chain. + function register(uint256 chainId, address commonBridge) external; + + /// @notice Deregisters a chain + /// @param chainId The ID of the chain to deregister. + function deregister(uint256 chainId) external; + + /// @notice Sends a message to a specified chain via its CommonBridge. + /// @param chainId The ID of the destination chain. + /// @param message The message details to send. + function sendMessage(uint256 chainId, ICommonBridge.SendValues calldata message) external payable; + + /// @notice Emitted when a new chain is registered. + /// @param chainId The ID of the registered chain. + /// @param commonBridge The address of the CommonBridge for the registered chain. + event ChainRegistered(uint256 indexed chainId, address commonBridge); + + /// @notice Emitted when a chain is deregistered. + /// @param chainId The ID of the deregistered chain. + event ChainDeregistered(uint256 indexed chainId); + + /// @notice Error indicating an invalid address was provided. + /// @param addr The invalid address. + error InvalidAddress(address addr); + + /// @notice Error indicating a chain is already registered. + /// @param chainId The ID of the already registered chain. + error ChainAlreadyRegistered(uint256 chainId); + + /// @notice Error indicating a chain is not registered. + /// @param chainId The ID of the not registered chain. + error ChainNotRegistered(uint256 chainId); +} diff --git a/crates/l2/contracts/src/l2/CommonBridgeL2.sol b/crates/l2/contracts/src/l2/CommonBridgeL2.sol index 57f52c5fe06..7dd79766759 100644 --- a/crates/l2/contracts/src/l2/CommonBridgeL2.sol +++ b/crates/l2/contracts/src/l2/CommonBridgeL2.sol @@ -104,4 +104,16 @@ contract CommonBridgeL2 is ICommonBridgeL2 { keccak256(abi.encodePacked(tokenL1, tokenL2, destination, amount)) ); } + + /// @inheritdoc ICommonBridgeL2 + function sendToL2(uint256 chainId, address to, uint256 destGasLimit, bytes calldata data) external override payable { + _burnGas(destGasLimit); + IL2ToL1Messenger(L1_MESSENGER).sendMessageToL2{value: msg.value}(chainId, msg.sender, to, destGasLimit, data); + } + + /// Burns at least {amount} gas + function _burnGas(uint256 amount) private view { + uint256 startingGas = gasleft(); + while (startingGas - gasleft() < amount) {} + } } diff --git a/crates/l2/contracts/src/l2/L2ToL1Messenger.sol b/crates/l2/contracts/src/l2/L2ToL1Messenger.sol index 5e7f32dfae3..1bc4e502365 100644 --- a/crates/l2/contracts/src/l2/L2ToL1Messenger.sol +++ b/crates/l2/contracts/src/l2/L2ToL1Messenger.sol @@ -10,10 +10,16 @@ contract L2ToL1Messenger is IL2ToL1Messenger { /// @dev Message Id that should be incremented before a message is sent uint256 public lastMessageId; + /// @inheritdoc IL2ToL1Messenger function sendMessageToL1(bytes32 data) external { // This event gets pushed to L1, the sequencer monitors // them on every block. lastMessageId += 1; emit L1Message(msg.sender, data, lastMessageId); } + + /// @inheritdoc IL2ToL1Messenger + function sendMessageToL2(uint256 chainId, address from, address to, uint256 gasLimit, bytes calldata data) external payable { + emit L2ToL2Message(chainId, from, to, msg.value, gasLimit, data); + } } diff --git a/crates/l2/contracts/src/l2/interfaces/ICommonBridgeL2.sol b/crates/l2/contracts/src/l2/interfaces/ICommonBridgeL2.sol index 1330098acb4..455e8b0c056 100644 --- a/crates/l2/contracts/src/l2/interfaces/ICommonBridgeL2.sol +++ b/crates/l2/contracts/src/l2/interfaces/ICommonBridgeL2.sol @@ -82,4 +82,13 @@ interface ICommonBridgeL2 { /// @param destination Address on L1 that should receive the tokens /// @param amount Amount of tokens to withdraw function withdrawERC20(address tokenL1, address tokenL2, address destination, uint256 amount) external; + + /// @notice Sends an arbitrary message to the another chain. + /// @dev This can be used to call functions on contracts on, or transfer ETH to, + /// the destination chain. + /// @param chainId The chain ID of the destination chain. + /// @param to The address of the contract on the destination chain. + /// @param destGasLimit The gas limit for the destination chain execution. + /// @param data The calldata to send to the destination contract. + function sendToL2(uint256 chainId, address to, uint256 destGasLimit, bytes calldata data) external payable; } diff --git a/crates/l2/contracts/src/l2/interfaces/IL2ToL1Messenger.sol b/crates/l2/contracts/src/l2/interfaces/IL2ToL1Messenger.sol index 4d2d7015434..4c7294b0e88 100644 --- a/crates/l2/contracts/src/l2/interfaces/IL2ToL1Messenger.sol +++ b/crates/l2/contracts/src/l2/interfaces/IL2ToL1Messenger.sol @@ -16,7 +16,30 @@ interface IL2ToL1Messenger { uint256 indexed messageId ); + event L2ToL2Message( + uint256 chainId, + address from, + address to, + uint256 value, + uint256 gasLimit, + bytes data + ); + /// @notice Sends the given data to the L1 /// @param data data to be sent to L1 function sendMessageToL1(bytes32 data) external; + + /// @notice Sends a message to another L2 chain + /// @param chainId the destination chain id + /// @param from the sender address on the source chain + /// @param to the recipient address on the destination chain + /// @param gasLimit the gas limit for the message execution on the destination chain + /// @param data the calldata to be sent to the recipient on the destination chain + function sendMessageToL2( + uint256 chainId, + address from, + address to, + uint256 gasLimit, + bytes calldata data + ) external payable; } diff --git a/crates/l2/monitor/widget/batches.rs b/crates/l2/monitor/widget/batches.rs index 855298b1610..47d1cccf398 100644 --- a/crates/l2/monitor/widget/batches.rs +++ b/crates/l2/monitor/widget/batches.rs @@ -149,7 +149,7 @@ impl BatchesTable { BatchLine { number: batch.number, block_count: batch.last_block - batch.first_block + 1, - message_count: batch.message_hashes.len(), + message_count: batch.l1_message_hashes.len(), commit_tx: batch.commit_tx, verify_tx: batch.verify_tx, } diff --git a/crates/l2/sdk/src/sdk.rs b/crates/l2/sdk/src/sdk.rs index 5625b922439..f41b8562f29 100644 --- a/crates/l2/sdk/src/sdk.rs +++ b/crates/l2/sdk/src/sdk.rs @@ -36,10 +36,10 @@ pub use ethrex_sdk_contract_utils::*; use calldata::from_hex_string_to_h256_array; -// 0x36664d7c5031bd965bbb405b55495a90dd780740 +// 0xa92c24809e706ba464ca0028017229ec37367a3e pub const DEFAULT_BRIDGE_ADDRESS: Address = H160([ - 0x36, 0x66, 0x4d, 0x7c, 0x50, 0x31, 0xbd, 0x96, 0x5b, 0xbb, 0x40, 0x5b, 0x55, 0x49, 0x5a, 0x90, - 0xdd, 0x78, 0x07, 0x40, + 0xa9, 0x2c, 0x24, 0x80, 0x9e, 0x70, 0x6b, 0xa4, 0x64, 0xca, 0x00, 0x28, 0x01, 0x72, 0x29, 0xec, + 0x37, 0x36, 0x7a, 0x3e, ]); // 0x000000000000000000000000000000000000ffff diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index 93e3cbddc95..a4f7ff8e5af 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -9,18 +9,21 @@ use crate::{ }, }; -use bytes::Bytes; use ethrex_blockchain::{Blockchain, vm::StoreVmDatabase}; use ethrex_common::{ Address, H256, U256, types::{ AccountUpdate, BLOB_BASE_FEE_UPDATE_FRACTION, BlobsBundle, Block, BlockNumber, MIN_BASE_FEE_PER_BLOB_GAS, TxType, batch::Batch, blobs_bundle, fake_exponential_checked, + l2_to_l2_message::L2toL2Message, }, }; use ethrex_l2_common::{ calldata::Value, - l1_messages::{get_block_l1_messages, get_l1_message_hash}, + l1_messages::{ + get_block_l1_messages, get_l1_message_hash, get_l2_to_l2_messages, + value_from_l2_to_l2_message, + }, merkle_tree::compute_merkle_root, privileged_transactions::{ PRIVILEGED_TX_BUDGET, compute_privileged_transactions_hash, @@ -59,7 +62,7 @@ use spawned_concurrency::tasks::{ const COMMIT_FUNCTION_SIGNATURE_BASED: &str = "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32,bytes[])"; -const COMMIT_FUNCTION_SIGNATURE: &str = "commitBatch(uint256,bytes32,bytes32,bytes32,bytes32)"; +const COMMIT_FUNCTION_SIGNATURE: &str = "commitBatch(uint256,bytes32,bytes32,(uint256,address,uint256,uint256,bytes)[],bytes32,bytes32)"; /// Default wake up time for the committer to check if it should send a commit tx const COMMITTER_DEFAULT_WAKE_TIME_MS: u64 = 60_000; @@ -238,7 +241,8 @@ impl L1Committer { let ( blobs_bundle, new_state_root, - message_hashes, + l1_message_hashes, + l2_to_l2_messages, privileged_transactions_hash, last_block_of_batch, ) = self @@ -256,7 +260,8 @@ impl L1Committer { last_block: last_block_of_batch, state_root: new_state_root, privileged_transactions_hash, - message_hashes, + l1_message_hashes, + l2_to_l2_messages, blobs_bundle, commit_tx: None, verify_tx: None, @@ -328,11 +333,22 @@ impl L1Committer { &mut self, mut last_added_block_number: BlockNumber, batch_number: u64, - ) -> Result<(BlobsBundle, H256, Vec, H256, BlockNumber), CommitterError> { + ) -> Result< + ( + BlobsBundle, + H256, + Vec, + Vec, + H256, + BlockNumber, + ), + CommitterError, + > { let first_block_of_batch = last_added_block_number + 1; let mut blobs_bundle = BlobsBundle::default(); let mut acc_messages = vec![]; + let mut acc_l2_to_l2_messages = vec![]; let mut acc_privileged_txs = vec![]; let mut acc_account_updates: HashMap = HashMap::new(); let mut message_hashes = vec![]; @@ -406,6 +422,7 @@ impl L1Committer { ); // Get block messages and privileged transactions let messages = get_block_l1_messages(&receipts); + let l2_to_l2_messages = get_l2_to_l2_messages(&receipts); let privileged_transactions = get_block_privileged_transactions(&txs); // Get block account updates. @@ -431,6 +448,7 @@ impl L1Committer { // Accumulate block data with the rest of the batch. acc_messages.extend(messages.clone()); + acc_l2_to_l2_messages.extend(l2_to_l2_messages); acc_privileged_txs.extend(privileged_transactions.clone()); for account in account_updates { let address = account.address; @@ -551,6 +569,7 @@ impl L1Committer { blobs_bundle, new_state_root, message_hashes, + acc_l2_to_l2_messages, privileged_transactions_hash, last_added_block_number, )) @@ -619,20 +638,10 @@ impl L1Committer { } async fn send_commitment(&mut self, batch: &Batch) -> Result { - let messages_merkle_root = compute_merkle_root(&batch.message_hashes); + let messages_merkle_root = compute_merkle_root(&batch.l1_message_hashes); let last_block_hash = get_last_block_hash(&self.store, batch.last_block)?; - let mut calldata_values = vec![ - Value::Uint(U256::from(batch.number)), - Value::FixedBytes(batch.state_root.0.to_vec().into()), - Value::FixedBytes(messages_merkle_root.0.to_vec().into()), - Value::FixedBytes(batch.privileged_transactions_hash.0.to_vec().into()), - Value::FixedBytes(last_block_hash.0.to_vec().into()), - ]; - - let (commit_function_signature, values) = if self.based { - let mut encoded_blocks: Vec = Vec::new(); - + let calldata = if self.based { let (blocks, _) = fetch_blocks_with_respective_fee_configs::( batch.number, &self.store, @@ -640,21 +649,40 @@ impl L1Committer { ) .await?; - for block in blocks { - encoded_blocks.push(block.encode_to_vec().into()); - } - - calldata_values.push(Value::Array( - encoded_blocks.into_iter().map(Value::Bytes).collect(), - )); - - (COMMIT_FUNCTION_SIGNATURE_BASED, calldata_values) + let encoded_blocks = blocks + .into_iter() + .map(|block| Value::Bytes(block.encode_to_vec().into())) + .collect::>(); + + let calldata_values = vec![ + Value::Uint(U256::from(batch.number)), + Value::FixedBytes(batch.state_root.0.to_vec().into()), + Value::FixedBytes(messages_merkle_root.0.to_vec().into()), + Value::FixedBytes(batch.privileged_transactions_hash.0.to_vec().into()), + Value::FixedBytes(last_block_hash.0.to_vec().into()), + Value::Array(encoded_blocks), + ]; + + encode_calldata(COMMIT_FUNCTION_SIGNATURE_BASED, &calldata_values)? } else { - (COMMIT_FUNCTION_SIGNATURE, calldata_values) + let calldata_values = vec![ + Value::Uint(U256::from(batch.number)), + Value::FixedBytes(batch.state_root.0.to_vec().into()), + Value::FixedBytes(messages_merkle_root.0.to_vec().into()), + Value::Array( + batch + .l2_to_l2_messages + .iter() + .map(value_from_l2_to_l2_message) + .collect(), + ), + Value::FixedBytes(batch.privileged_transactions_hash.0.to_vec().into()), + Value::FixedBytes(last_block_hash.0.to_vec().into()), + ]; + + encode_calldata(COMMIT_FUNCTION_SIGNATURE, &calldata_values)? }; - let calldata = encode_calldata(commit_function_signature, &values)?; - let gas_price = self .eth_client .get_gas_price_with_extra(20) diff --git a/crates/l2/storage/src/api.rs b/crates/l2/storage/src/api.rs index 2f7a12023d1..fe860f56ee7 100644 --- a/crates/l2/storage/src/api.rs +++ b/crates/l2/storage/src/api.rs @@ -4,7 +4,10 @@ use std::fmt::Debug; use ethrex_common::{ H256, - types::{AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig}, + types::{ + AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig, + l2_to_l2_message::L2toL2Message, + }, }; use ethrex_l2_common::prover::{BatchProof, ProverInputData, ProverType}; @@ -148,6 +151,11 @@ pub trait StoreEngineRollup: Debug + Send + Sync { async fn revert_to_batch(&self, batch_number: u64) -> Result<(), RollupStoreError>; + async fn get_l2_to_l2_messages( + &self, + batch_number: u64, + ) -> Result>, RollupStoreError>; + async fn store_prover_input_by_batch_and_version( &self, batch_number: u64, diff --git a/crates/l2/storage/src/store.rs b/crates/l2/storage/src/store.rs index 3fad7e31a7e..9d23f144f63 100644 --- a/crates/l2/storage/src/store.rs +++ b/crates/l2/storage/src/store.rs @@ -7,7 +7,10 @@ use crate::store_db::in_memory::Store as InMemoryStore; use crate::store_db::sql::SQLStore; use ethrex_common::{ H256, - types::{AccountUpdate, Blob, BlobsBundle, BlockNumber, batch::Batch, fee_config::FeeConfig}, + types::{ + AccountUpdate, Blob, BlobsBundle, BlockNumber, batch::Batch, fee_config::FeeConfig, + l2_to_l2_message::L2toL2Message, + }, }; use ethrex_l2_common::prover::{BatchProof, ProverInputData, ProverType}; use tracing::info; @@ -56,7 +59,8 @@ impl Store { last_block: 0, state_root: H256::zero(), privileged_transactions_hash: H256::zero(), - message_hashes: Vec::new(), + l1_message_hashes: Vec::new(), + l2_to_l2_messages: Vec::new(), blobs_bundle: BlobsBundle::empty(), commit_tx: None, verify_tx: None, @@ -186,11 +190,19 @@ impl Store { RollupStoreError::Custom(format!("Failed to create blobs bundle from blob while getting batch from database: {e}. This is a bug")) })?; - let message_hashes = self + let l1_message_hashes = self .get_message_hashes_by_batch(batch_number) .await? .unwrap_or_default(); + let l2_to_l2_messages = self + .get_l2_to_l2_messages(batch_number) + .await? + .ok_or(RollupStoreError::Custom( + "Failed while trying to retrieve the L2->L2 messages of a known batch. This is a bug." + .to_owned(), + ))?; + let privileged_transactions_hash = self .get_privileged_transactions_hash_by_batch(batch_number) .await?.ok_or(RollupStoreError::Custom( @@ -208,7 +220,8 @@ impl Store { last_block, state_root, blobs_bundle, - message_hashes, + l1_message_hashes, + l2_to_l2_messages, privileged_transactions_hash, commit_tx, verify_tx, @@ -356,6 +369,13 @@ impl Store { .await } + pub async fn get_l2_to_l2_messages( + &self, + batch_number: u64, + ) -> Result>, RollupStoreError> { + self.engine.get_l2_to_l2_messages(batch_number).await + } + pub async fn store_prover_input_by_batch_and_version( &self, batch_number: u64, diff --git a/crates/l2/storage/src/store_db/in_memory.rs b/crates/l2/storage/src/store_db/in_memory.rs index 1a4405e36ac..c9850111c18 100644 --- a/crates/l2/storage/src/store_db/in_memory.rs +++ b/crates/l2/storage/src/store_db/in_memory.rs @@ -7,7 +7,10 @@ use std::{ use crate::error::RollupStoreError; use ethrex_common::{ H256, - types::{AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig}, + types::{ + AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig, + l2_to_l2_message::L2toL2Message, + }, }; use ethrex_l2_common::prover::{BatchProof, ProverInputData, ProverType}; @@ -46,6 +49,8 @@ struct StoreInner { commit_txs: HashMap, /// Map of batch number to verify transaction hash verify_txs: HashMap, + /// Map of batch number to L2->L2 messages + l2_to_l2_messages: HashMap>, /// Map of (batch_number, prover_version) to serialized prover input data batch_prover_input: HashMap<(u64, String), Vec>, /// Map of block number to FeeConfig @@ -309,7 +314,11 @@ impl StoreEngineRollup for Store { inner .message_hashes_by_batch - .insert(batch.number, batch.message_hashes); + .insert(batch.number, batch.l1_message_hashes); + + inner + .l2_to_l2_messages + .insert(batch.number, batch.l2_to_l2_messages); inner .privileged_transactions_hashes @@ -342,6 +351,13 @@ impl StoreEngineRollup for Store { Ok(self.inner()?.state_roots.keys().max().cloned()) } + async fn get_l2_to_l2_messages( + &self, + batch_number: u64, + ) -> Result>, RollupStoreError> { + Ok(self.inner()?.l2_to_l2_messages.get(&batch_number).cloned()) + } + async fn store_prover_input_by_batch_and_version( &self, batch_number: u64, diff --git a/crates/l2/storage/src/store_db/sql.rs b/crates/l2/storage/src/store_db/sql.rs index ed924273b09..e3616ea568e 100644 --- a/crates/l2/storage/src/store_db/sql.rs +++ b/crates/l2/storage/src/store_db/sql.rs @@ -4,7 +4,10 @@ use tokio::sync::Mutex; use crate::{RollupStoreError, api::StoreEngineRollup}; use ethrex_common::{ H256, - types::{AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig}, + types::{ + AccountUpdate, Blob, BlockNumber, batch::Batch, fee_config::FeeConfig, + l2_to_l2_message::L2toL2Message, + }, }; use ethrex_l2_common::prover::{BatchProof, ProverInputData, ProverType}; @@ -28,7 +31,7 @@ impl Debug for SQLStore { } } -const DB_SCHEMA: [&str; 17] = [ +const DB_SCHEMA: [&str; 18] = [ "CREATE TABLE blocks (block_number INT PRIMARY KEY, batch INT)", "CREATE TABLE messages (batch INT, idx INT, message_hash BLOB, PRIMARY KEY (batch, idx))", "CREATE TABLE privileged_transactions (batch INT PRIMARY KEY, transactions_hash BLOB)", @@ -46,6 +49,7 @@ const DB_SCHEMA: [&str; 17] = [ "CREATE TABLE batch_signatures (batch INT PRIMARY KEY, signature BLOB)", "CREATE TABLE batch_prover_input (batch INT, prover_version TEXT, prover_input BLOB, PRIMARY KEY (batch, prover_version))", "CREATE TABLE fee_config (block_number INT PRIMARY KEY, fee_config BLOB)", + "CREATE TABLE l2_to_l2_messages (batch INT, idx INT, message BLOB, PRIMARY KEY (batch, idx))", ]; impl SQLStore { @@ -304,6 +308,27 @@ impl SQLStore { self.execute_in_tx(queries, db_tx).await } + + async fn store_l2_to_l2_messages( + &self, + batch_number: u64, + messages: Vec, + db_tx: Option<&Transaction>, + ) -> Result<(), RollupStoreError> { + let mut queries = Vec::with_capacity(messages.len()); + + for (idx, message) in messages.iter().enumerate() { + let idx = u64::try_from(idx) + .map_err(|_| RollupStoreError::Custom("Message index out of range".to_string()))?; + queries.push(( + "INSERT INTO l2_to_l2_messages (batch, idx, message) VALUES (?1, ?2, ?3)", + libsql::params!(batch_number, idx, bincode::serialize(message)?).into_params()?, + )) + } + + self.execute_in_tx(queries, db_tx).await?; + Ok(()) + } } fn read_from_row_int(row: &Row, index: i32) -> Result { @@ -668,10 +693,12 @@ impl StoreEngineRollup for SQLStore { .await?; self.store_message_hashes_by_batch_in_tx( batch.number, - batch.message_hashes, + batch.l1_message_hashes, Some(&transaction), ) .await?; + self.store_l2_to_l2_messages(batch.number, batch.l2_to_l2_messages, Some(&transaction)) + .await?; self.store_privileged_transactions_hash_by_batch_number_in_tx( batch.number, batch.privileged_transactions_hash, @@ -809,6 +836,26 @@ impl StoreEngineRollup for SQLStore { .transpose() } + async fn get_l2_to_l2_messages( + &self, + batch_number: u64, + ) -> Result>, RollupStoreError> { + let mut rows = self + .query( + "SELECT message FROM l2_to_l2_messages WHERE batch = ?1", + vec![batch_number], + ) + .await?; + + let mut messages = Vec::new(); + while let Some(row) = rows.next().await? { + let val = read_from_row_blob(&row, 0)?; + messages.push(bincode::deserialize(&val)?); + } + + Ok(Some(messages)) + } + async fn store_prover_input_by_batch_and_version( &self, batch_number: u64, @@ -955,6 +1002,8 @@ mod tests { ("batch_prover_input", "batch") => "INT", ("batch_prover_input", "prover_version") => "TEXT", ("batch_prover_input", "prover_input") => "BLOB", + ("l2_to_l2_messages", "idx") => "INT", + ("l2_to_l2_messages", "message") => "BLOB", _ => { return Err(anyhow::Error::msg( "unexpected attribute {name} in table {table}", diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index e3d2bb62011..fa35fe9c87e 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -82,10 +82,10 @@ const DEFAULT_PROPOSER_COINBASE_ADDRESS: Address = H160([ 0xad, 0x62, 0x0c, 0x8d, ]); -// 0x44e09413ab37c3dae5663f2fd408e60ac2dbc7e2 +// 0xae75a1bd48bf0c66b12246cea81a6dccbe3e5373 const DEFAULT_ON_CHAIN_PROPOSER_ADDRESS: Address = H160([ - 0x44, 0xe0, 0x94, 0x13, 0xab, 0x37, 0xc3, 0xda, 0xe5, 0x66, 0x3f, 0x2f, 0xd4, 0x08, 0xe6, 0x0a, - 0xc2, 0xdb, 0xc7, 0xe2, + 0xae, 0x75, 0xa1, 0xbd, 0x48, 0xbf, 0x0c, 0x66, 0xb1, 0x22, 0x46, 0xce, 0xa8, 0x1a, 0x6d, 0xcc, + 0xbe, 0x3e, 0x53, 0x73, ]); // 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 diff --git a/crates/networking/p2p/rlpx/l2/messages.rs b/crates/networking/p2p/rlpx/l2/messages.rs index f1631b8bcd4..656c6fb4fb8 100644 --- a/crates/networking/p2p/rlpx/l2/messages.rs +++ b/crates/networking/p2p/rlpx/l2/messages.rs @@ -110,7 +110,7 @@ impl RLPxMessage for BatchSealed { .encode_field(&self.batch.last_block) .encode_field(&self.batch.state_root) .encode_field(&self.batch.privileged_transactions_hash) - .encode_field(&self.batch.message_hashes) + .encode_field(&self.batch.l1_message_hashes) .encode_field(&self.batch.blobs_bundle.blobs) .encode_field(&self.batch.blobs_bundle.commitments) .encode_field(&self.batch.blobs_bundle.proofs) @@ -132,7 +132,8 @@ impl RLPxMessage for BatchSealed { let (state_root, decoder) = decoder.decode_field("state_root")?; let (privileged_transactions_hash, decoder) = decoder.decode_field("privileged_transactions_hash")?; - let (message_hashes, decoder) = decoder.decode_field("message_hashes")?; + let (l1_message_hashes, decoder) = decoder.decode_field("message_hashes")?; + let (l2_to_l2_messages, decoder) = decoder.decode_field("l2_to_l2_messages")?; let (blobs, decoder) = decoder.decode_field("blobs")?; let (commitments, decoder) = decoder.decode_field("commitments")?; let (proofs, decoder) = decoder.decode_field("proofs")?; @@ -147,7 +148,8 @@ impl RLPxMessage for BatchSealed { last_block, state_root, privileged_transactions_hash, - message_hashes, + l1_message_hashes, + l2_to_l2_messages, blobs_bundle: ethrex_common::types::blobs_bundle::BlobsBundle { blobs, commitments, diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index eec9bf3c8aa..944065709b9 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -53,6 +53,7 @@ - [L1MessageSender]() - [Based sequencing](./l2/fundamentals/based.md) - [Transaction fees](./l2/fundamentals/transaction_fees.md) + - [Shared Bridge](./l2/fundamentals/shared_bridge.md) # Ethrex for developers diff --git a/docs/l2/fundamentals/shared_bridge.md b/docs/l2/fundamentals/shared_bridge.md new file mode 100644 index 00000000000..7780851f5df --- /dev/null +++ b/docs/l2/fundamentals/shared_bridge.md @@ -0,0 +1,48 @@ +# Shared Bridge + +If a user wants to transfer funds from an L2-A to an account on an L2-B, the conventional flow involves moving the assets from L2-A to Ethereum (withdraw), unlocking the funds on Ethereum (claim withdrawal), and then moving those assets from Ethereum to L2-B (deposit). This process requires several steps that ultimately degrade the user experience (UX), and two of them involve transactions on Ethereum, which are often expensive. This happens because there is currently no direct communication path between different L2s, forcing all communication to pass through their common point, Ethereum. + +The Shared Bridge feature enables the sending of messages between different L2s, allowing a user to perform the above meta-operation by only interacting with the source L2 (L2-A) and eventually seeing the operation's result reflected on the destination L2 (L2-B). + +Although the user only needs to perform a single interaction and wait for the result, a similar flow to the one described above occurs behind the scenes. Below, we will explain how it works. + +## High-Level Overview + +To understand the behind-the-scenes mechanics, we'll use the previous example. For a recap, here is a detailed breakdown of what happens: + +[Image Placeholder] + +1. On the source L2: The user calls the sendToL2 function on the L2Bridge contract, specifying the chain ID of the destination L2, the address of the recipient account, and optionally the calldata. The L2Bridge then emits an L2ToL2Message event. +2. On the source L2: The L1Committer collects L2ToL2Message events, builds a Merkle root, attaches it to the commit, and finally calls the commitBatch function on the corresponding OnChainProposer on L1. +3. On L1: The OnChainProposer for the source L2, in response to the commitBatch call, stores the Merkle root of the L2-to-L2 messages. +4. On the source L2: The L1ProofSender calls the verifyBatch function on the OnChainProposer on L1, attaching the L2ToL2Messages. +5. On L1: The OnChainProposer for the source L2, in response to the verifyBatch call, builds a Merkle root from the L2-to-L2 messages and compares it to the one stored earlier in step 3. If the Merkle root built from the messages matches (i.e., it is valid), the incoming messages are forwarded to the Router. +6. On L1: The Router redirects each message to the corresponding L1Bridges via the receiveMessage function. +7. On L1: The L1Bridge for the destination L2 processes the received message in the receiveMessage call and emits an event. +8. On the destination L2: The L1Watcher intercepts and processes the events emitted by the L1Bridge. + +## Protocol Details + +### How It Works + +- The user calls the sendToL2 function on the L2Bridge contract, specifying: + - The chain ID of the destination L2, + - The address of the message recipient on the destination L2, + - The gas limit that the sender is willing to consume for the final transaction on the destination L2. Note that this gas is burned on the source L2. It remains pending to define how the user should proceed to recover the burned gas if the transaction on the destination L2 reverts. + - (Optional) The calldata for the transaction to execute on the destination L2. +- The sendToL2 function on the L2Bridge contract burns the amount of gas specified by the user and then calls the sendMessageToL2 function on the L2ToL1Messanger contract, passing the same parameters provided by the user, along with the address of the user (msg.sender) who initiated the call. +- The sendMessageToL2 function on the L2ToL1Messanger contract increments the counter for sent L2 messages (L2 message IDs) and emits the L2ToL2Message event, which includes the parameters provided by the user plus the user's address. This event represents the message from the source L2 to the destination L2. +- The L1Committer collects the L2ToL2Message events emitted in the blocks of the batch it is about to commit, builds a Merkle root from them, attaches it to the commit, and finally calls the commitBatch function on the OnChainProposer corresponding to the source L2. +- The OnChainProposer corresponding to the source L2... + +## Gas + +TODO + +## Troubleshooting + +TODO + +## Proving + +TODO diff --git a/fixtures/genesis/l2.json b/fixtures/genesis/l2.json index f407c2d94b6..15e0a6197eb 100644 --- a/fixtures/genesis/l2.json +++ b/fixtures/genesis/l2.json @@ -20,19 +20,29 @@ "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa", "blobSchedule": { "cancun": { - "target": 3, + "baseFeeUpdateFraction": 3338477, "max": 6, - "baseFeeUpdateFraction": 3338477 + "target": 3 }, "prague": { - "target": 6, + "baseFeeUpdateFraction": 5007716, "max": 9, - "baseFeeUpdateFraction": 5007716 + "target": 6 }, "osaka": { - "target": 6, + "baseFeeUpdateFraction": 5007716, "max": 9, - "baseFeeUpdateFraction": 5007716 + "target": 6 + }, + "bpo1": { + "baseFeeUpdateFraction": 8346193, + "max": 15, + "target": 10 + }, + "bpo2": { + "baseFeeUpdateFraction": 11684671, + "max": 21, + "target": 14 } }, "mergeNetsplitBlock": 0, @@ -2109,8 +2119,8 @@ "0x000000000000000000000000000000000000ffff": { "code": "0x608060405261000c61000e565b005b610016610040565b565b60018060a01b031690565b61002c90610018565b90565b63ffffffff60e01b1690565b5f0190565b3361005a61005461004f6100c2565b610023565b91610023565b145f146100b35763ffffffff60e01b5f351661008561007f63278f794360e11b61002f565b9161002f565b14155f146100a9575f6334ad5dbb60e21b8152806100a56004820161003b565b0390fd5b6100b16102d9565b565b6100d6565b5f90565b61f00090565b6100ca6100b8565b506100d36100bc565b90565b6100de610318565b61032c565b90565b90565b90565b6101006100fb610105926100e3565b6100e9565b6100e6565b90565b60405190565b5f80fd5b5f80fd5b90939293848311610136578411610131576001820201920390565b610112565b61010e565b91565b5f80fd5b5f80fd5b61014f90610018565b90565b61015b81610146565b0361016257565b5f80fd5b9050359061017382610152565b565b5f80fd5b5f80fd5b601f801991011690565b634e487b7160e01b5f52604160045260245ffd5b906101a59061017d565b810190811067ffffffffffffffff8211176101bf57604052565b610187565b906101d76101d0610108565b928361019b565b565b67ffffffffffffffff81116101f7576101f360209161017d565b0190565b610187565b90825f939282370152565b9092919261021c610217826101d9565b6101c4565b9381855260208501908284011161023857610236926101fc565b565b610179565b9080601f8301121561025b5781602061025893359101610207565b90565b610175565b9190916040818403126102a057610279835f8301610166565b92602082013567ffffffffffffffff811161029b57610298920161023d565b90565b610142565b61013e565b6102b96102b46102be92610018565b6100e9565b610018565b90565b6102ca906102a5565b90565b6102d6906102c1565b90565b61031661031161030a6103026102fc5f366102f460046100ec565b908092610116565b9061013b565b810190610260565b91906102cd565b610379565b565b6103206100b8565b50610329610486565b90565b5f8091368280378136915af43d5f803e5f14610346573d5ff35b3d5ffd5b610353906102c1565b90565b5190565b90565b61037161036c6103769261035a565b6100e9565b6100e6565b90565b906103838261050c565b816103ae7fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b9161034a565b906103b7610108565b806103c18161003b565b0390a26103cd81610356565b6103df6103d95f61035d565b916100e6565b115f146103f3576103ef916105dc565b505b565b50506103fd610561565b6103f1565b90565b90565b5f1b90565b61042161041c61042692610402565b610408565b610405565b90565b6104527f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61040d565b90565b5f1c90565b60018060a01b031690565b61047161047691610455565b61045a565b90565b6104839054610465565b90565b61048e6100b8565b506104a95f6104a361049e610429565b61060b565b01610479565b90565b6104b590610023565b9052565b91906104cc905f602085019401906104ac565b565b906104df60018060a01b0391610408565b9181191691161790565b90565b906105016104fc6105089261034a565b6104e9565b82546104ce565b9055565b803b61052061051a5f61035d565b916100e6565b1461054257610540905f61053a610535610429565b61060b565b016104ec565b565b61055d905f918291634c9c8ce360e01b8352600483016104b9565b0390fd5b3461057461056e5f61035d565b916100e6565b1161057b57565b5f63b398979f60e01b8152806105936004820161003b565b0390fd5b606090565b906105ae6105a9836101d9565b6101c4565b918252565b3d5f146105ce576105c33d61059c565b903d5f602084013e5b565b6105d6610597565b906105cc565b5f80610608936105ea610597565b508390602081019051915af4906105ff6105b3565b90919091610613565b90565b90565b151590565b9061062790610620610597565b501561060e565b5f146106335750610697565b61063c82610356565b61064e6106485f61035d565b916100e6565b148061067c575b61065d575090565b610678905f918291639996b31560e01b8352600483016104b9565b0390fd5b50803b61069161068b5f61035d565b916100e6565b14610655565b6106a081610356565b6106b26106ac5f61035d565b916100e6565b115f146106c157602081519101fd5b5f63d6bda27560e01b8152806106d96004820161003b565b0390fd", "storage": { - "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0xf000", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0xefff" + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0xefff", + "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0xf000" }, "balance": "0x0", "nonce": "0x1" @@ -2134,4 +2144,4 @@ "nonce": "0x1" } } -} \ No newline at end of file +}