diff --git a/cmd/ethrex_l2/src/commands/stack.rs b/cmd/ethrex_l2/src/commands/stack.rs index 172039fa5c..e35f2cae61 100644 --- a/cmd/ethrex_l2/src/commands/stack.rs +++ b/cmd/ethrex_l2/src/commands/stack.rs @@ -202,7 +202,7 @@ impl Command { .await?; let rollup_store = StoreRollup::new( store_path - .join("../rollup_store") + .join("./rollup_store") .to_str() .expect("Invalid store path"), EngineTypeRollup::Libmdbx, @@ -224,11 +224,12 @@ impl Command { // Iterate over each blob let files: Vec = read_dir(blobs_dir)?.try_collect()?; - for (batch_number, file) in files + for (file_number, file) in files .into_iter() .sorted_by_key(|f| f.file_name()) .enumerate() { + let batch_number = file_number as u64 + 1; let blob = std::fs::read(file.path())?; if blob.len() != BYTES_PER_BLOB { @@ -294,7 +295,7 @@ impl Command { // Store batch info in L2 storage rollup_store .store_batch( - batch_number as u64, + batch_number, first_block_number, last_block_number, withdrawal_hashes, diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 1ee8104ff4..420219bb04 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -60,7 +60,7 @@ ethrex_L2_CONTRACTS_PATH=./contracts L1_RPC_URL=http://localhost:8545 L1_PRIVATE_KEY=0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 -ethrex_L2_DEV_LIBMDBX=dev_ethrex_l2 +ethrex_L2_DEV_LIBMDBX?=dev_ethrex_l2 ethrex_L1_DEV_LIBMDBX=dev_ethrex_l1 L1_PORT=8545 L2_PORT=1729 diff --git a/crates/l2/configs/sequencer_config_example.toml b/crates/l2/configs/sequencer_config_example.toml index e7e2a8d2b1..4f638ba4a7 100644 --- a/crates/l2/configs/sequencer_config_example.toml +++ b/crates/l2/configs/sequencer_config_example.toml @@ -23,7 +23,7 @@ maximum_allowed_max_fee_per_gas = 10000000000 maximum_allowed_max_fee_per_blob_gas = 10000000000 [watcher] -bridge_address = "0x266ffef34e21a7c4ce2e0e42dc780c2c273ca440" +bridge_address = "0x554a14cd047c485b3ac3edbd9fbb373d6f84ad3f" check_interval_ms = 1000 max_block_step = 5000 l2_proposer_private_key = "0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924" diff --git a/crates/l2/contracts/src/l1/CommonBridge.sol b/crates/l2/contracts/src/l1/CommonBridge.sol index 7a235f2219..08ebe537a9 100644 --- a/crates/l2/contracts/src/l1/CommonBridge.sol +++ b/crates/l2/contracts/src/l1/CommonBridge.sol @@ -76,30 +76,19 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard { require(msg.value > 0, "CommonBridge: amount to deposit is zero"); bytes32 l2MintTxHash = keccak256( - abi.encodePacked( - msg.sender, - depositValues.to, - depositValues.recipient, - msg.value, - depositValues.gasLimit, - depositId, - depositValues.data + bytes.concat( + bytes20(depositValues.to), + bytes32(msg.value), + bytes32(depositId), + bytes20(depositValues.recipient), + bytes20(msg.sender), + bytes32(depositValues.gasLimit), + bytes32(keccak256(depositValues.data)) ) ); - pendingDepositLogs.push( - keccak256( - bytes.concat( - bytes20(depositValues.to), - bytes32(msg.value), - bytes32(depositId), - bytes20(depositValues.recipient), - bytes20(msg.sender), - bytes32(depositValues.gasLimit), - bytes32(keccak256(depositValues.data)) - ) - ) - ); + pendingDepositLogs.push(l2MintTxHash); + emit DepositInitiated( msg.value, depositValues.to, diff --git a/crates/l2/sdk/src/sdk.rs b/crates/l2/sdk/src/sdk.rs index 11fa0beb56..b26fc9a75f 100644 --- a/crates/l2/sdk/src/sdk.rs +++ b/crates/l2/sdk/src/sdk.rs @@ -13,10 +13,10 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod calldata; pub mod merkle_tree; -// 0x6bf26397c5676a208d5c4e5f35cb479bacbbe454 +// 0x554a14cd047c485b3ac3edbd9fbb373d6f84ad3f pub const DEFAULT_BRIDGE_ADDRESS: Address = H160([ - 0x6b, 0xf2, 0x63, 0x97, 0xc5, 0x67, 0x6a, 0x20, 0x8d, 0x5c, 0x4e, 0x5f, 0x35, 0xcb, 0x47, 0x9b, - 0xac, 0xbb, 0xe4, 0x54, + 0x55, 0x4a, 0x14, 0xcd, 0x04, 0x7c, 0x48, 0x5b, 0x3a, 0xc3, 0xed, 0xbd, 0x9f, 0xbb, 0x37, 0x3d, + 0x6f, 0x84, 0xad, 0x3f, ]); pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([ @@ -32,7 +32,7 @@ pub enum SdkError { FailedToParseAddressFromHex, } -/// BRIDGE_ADDRESS or 0x6bf26397c5676a208d5c4e5f35cb479bacbbe454 +/// BRIDGE_ADDRESS or 0x554a14cd047c485b3ac3edbd9fbb373d6f84ad3f pub fn bridge_address() -> Result { std::env::var("L1_WATCHER_BRIDGE_ADDRESS") .unwrap_or(format!("{DEFAULT_BRIDGE_ADDRESS:#x}")) diff --git a/crates/l2/sequencer/l1_watcher.rs b/crates/l2/sequencer/l1_watcher.rs index 39f493f057..0fa0e9d255 100644 --- a/crates/l2/sequencer/l1_watcher.rs +++ b/crates/l2/sequencer/l1_watcher.rs @@ -151,37 +151,23 @@ impl L1Watcher { let mut deposit_txs = Vec::new(); for log in logs { - let (mint_value, to_address, deposit_id, recipient, from, gas_limit, calldata) = - parse_values_log(log.log)?; - - let value_bytes = mint_value.to_big_endian(); - let id_bytes = deposit_id.to_big_endian(); - let gas_limit_bytes = gas_limit.to_big_endian(); - let deposit_hash = keccak( - [ - to_address.as_bytes(), - &value_bytes, - &id_bytes, - recipient.as_bytes(), - from.as_bytes(), - &gas_limit_bytes, - keccak(&calldata).as_bytes(), - ] - .concat(), - ); - - let deposit_already_processed = store - .get_transaction_by_hash(deposit_hash) - .await - .map_err(L1WatcherError::FailedAccessingStore)? - .is_some(); + let deposit_data = DepositData::from_log(log.log)?; - if deposit_already_processed { - warn!("Deposit already processed (to: {recipient:#x}, value: {mint_value}, depositId: {deposit_id}), skipping."); + if self + .deposit_already_processed(deposit_data.deposit_tx_hash, store) + .await? + { + warn!( + "Deposit already processed (to: {:x}, value: {:x}, depositId: {:#}), skipping.", + deposit_data.recipient, deposit_data.mint_value, deposit_data.deposit_id + ); continue; } - info!("Initiating mint transaction for {recipient:#x} with value {mint_value:#x} and depositId: {deposit_id:#}",); + info!( + "Initiating mint transaction for {:x} with value {:x} and depositId: {:#}", + deposit_data.recipient, deposit_data.mint_value, deposit_data.deposit_id + ); let gas_price = self.l2_client.get_gas_price().await?; // Avoid panicking when using as_u64() @@ -192,10 +178,10 @@ impl L1Watcher { let mint_transaction = self .eth_client .build_privileged_transaction( - to_address, - recipient, - from, - Bytes::copy_from_slice(&calldata), + deposit_data.to_address, + deposit_data.recipient, + deposit_data.from, + Bytes::copy_from_slice(&deposit_data.calldata), Overrides { chain_id: Some( store @@ -208,9 +194,9 @@ impl L1Watcher { // Using the deposit_id as nonce. // If we make a transaction on the L2 with this address, we may break the // deposit workflow. - nonce: Some(deposit_id.as_u64()), - value: Some(mint_value), - gas_limit: Some(gas_limit.as_u64()), + nonce: Some(deposit_data.deposit_id.as_u64()), + value: Some(deposit_data.mint_value), + gas_limit: Some(deposit_data.gas_limit.as_u64()), // TODO(CHECK): Seems that when we start the L2, we need to set the gas. // Otherwise, the transaction is not included in the mempool. // We should override the blockchain to always include the transaction. @@ -239,100 +225,137 @@ impl L1Watcher { Ok(deposit_txs) } + + async fn deposit_already_processed( + &self, + deposit_hash: H256, + store: &Store, + ) -> Result { + if store + .get_transaction_by_hash(deposit_hash) + .await + .map_err(L1WatcherError::FailedAccessingStore)? + .is_some() + { + return Ok(true); + } + + // If we have a reconstructed state, we don't have the transaction in our store. + // Check if the deposit is marked as pending in the contract. + let pending_deposits = self + .eth_client + .get_pending_deposit_logs(self.address) + .await?; + Ok(!pending_deposits.contains(&deposit_hash)) + } +} + +struct DepositData { + pub mint_value: U256, + pub to_address: H160, + pub deposit_id: U256, + pub recipient: H160, + pub from: H160, + pub gas_limit: U256, + pub calldata: Vec, + pub deposit_tx_hash: H256, } -#[allow(clippy::type_complexity)] -fn parse_values_log( - log: RpcLogInfo, -) -> Result<(U256, H160, U256, H160, H160, U256, Vec), L1WatcherError> { - let mint_value = format!( - "{:#x}", - log.topics - .get(1) +impl DepositData { + fn from_log(log: RpcLogInfo) -> Result { + let mint_value = format!( + "{:#x}", + log.topics + .get(1) + .ok_or(L1WatcherError::FailedToDeserializeLog( + "Failed to parse mint value from log: log.topics[1] out of bounds".to_owned() + ))? + ) + .parse::() + .map_err(|e| { + L1WatcherError::FailedToDeserializeLog(format!( + "Failed to parse mint value from log: {e:#?}" + )) + })?; + let to_address_hash = log + .topics + .get(2) .ok_or(L1WatcherError::FailedToDeserializeLog( - "Failed to parse mint value from log: log.topics[1] out of bounds".to_owned() - ))? - ) - .parse::() - .map_err(|e| { - L1WatcherError::FailedToDeserializeLog(format!( - "Failed to parse mint value from log: {e:#?}" - )) - })?; - let to_address_hash = log - .topics - .get(2) - .ok_or(L1WatcherError::FailedToDeserializeLog( - "Failed to parse beneficiary from log: log.topics[2] out of bounds".to_owned(), - ))?; - let to_address = hash_to_address(*to_address_hash); + "Failed to parse beneficiary from log: log.topics[2] out of bounds".to_owned(), + ))?; + let to_address = hash_to_address(*to_address_hash); - let deposit_id = log - .topics - .get(3) - .ok_or(L1WatcherError::FailedToDeserializeLog( - "Failed to parse beneficiary from log: log.topics[3] out of bounds".to_owned(), - ))?; + let deposit_id = log + .topics + .get(3) + .ok_or(L1WatcherError::FailedToDeserializeLog( + "Failed to parse beneficiary from log: log.topics[3] out of bounds".to_owned(), + ))?; + + let deposit_id = format!("{deposit_id:#x}").parse::().map_err(|e| { + L1WatcherError::FailedToDeserializeLog(format!( + "Failed to parse depositId value from log: {e:#?}" + )) + })?; + + // The previous values are indexed in the topic of the log. Data contains the rest. + // DATA = recipient: Address || from: Address || gas_limit: uint256 || offset_calldata: uint256 || tx_hash: H256 || length_calldata: uint256 || calldata: bytes + // DATA = 0..32 || 32..64 || 64..96 || 96..128 || 128..160 || 160..192 || 192..(192+calldata_len) + // Any value that is not 32 bytes is padded with zeros. + + let recipient = log + .data + .get(12..32) + .ok_or(L1WatcherError::FailedToDeserializeLog( + "Failed to parse recipient from log: log.data[0..32] out of bounds".to_owned(), + ))?; + let recipient = Address::from_slice(recipient); - let deposit_id = format!("{deposit_id:#x}").parse::().map_err(|e| { - L1WatcherError::FailedToDeserializeLog(format!( - "Failed to parse depositId value from log: {e:#?}" - )) - })?; - - // The previous values are indexed in the topic of the log. Data contains the rest. - // DATA = recipient: Address || from: Address || gas_limit: uint256 || offset_calldata: uint256 || tx_hash: H256 || length_calldata: uint256 || calldata: bytes - // DATA = 0..32 || 32..64 || 64..96 || 96..128 || 128..160 || 160..192 || 192..(192+calldata_len) - // Any value that is not 32 bytes is padded with zeros. - - let recipient = log - .data - .get(12..32) - .ok_or(L1WatcherError::FailedToDeserializeLog( - "Failed to parse recipient from log: log.data[0..32] out of bounds".to_owned(), - ))?; - let recipient = Address::from_slice(recipient); + let from = log + .data + .get(44..64) + .ok_or(L1WatcherError::FailedToDeserializeLog( + "Failed to parse from from log: log.data[44..64] out of bounds".to_owned(), + ))?; + let from = Address::from_slice(from); + + let gas_limit = U256::from_big_endian(log.data.get(64..96).ok_or( + L1WatcherError::FailedToDeserializeLog( + "Failed to parse gas_limit from log: log.data[64..96] out of bounds".to_owned(), + ), + )?); + + let deposit_tx_hash = H256::from_slice( + log.data + .get(128..160) + .ok_or(L1WatcherError::FailedToDeserializeLog( + "Failed to parse deposit_tx_hash from log: log.data[64..96] out of bounds" + .to_owned(), + ))?, + ); - let from = log - .data - .get(44..64) - .ok_or(L1WatcherError::FailedToDeserializeLog( - "Failed to parse from from log: log.data[44..64] out of bounds".to_owned(), - ))?; - let from = Address::from_slice(from); - - let gas_limit = U256::from_big_endian(log.data.get(64..96).ok_or( - L1WatcherError::FailedToDeserializeLog( - "Failed to parse gas_limit from log: log.data[64..96] out of bounds".to_owned(), - ), - )?); - - let _deposit_tx_hash = H256::from_slice(log.data.get(128..160).ok_or( - L1WatcherError::FailedToDeserializeLog( - "Failed to parse deposit_tx_hash from log: log.data[64..96] out of bounds".to_owned(), - ), - )?); - - let calldata_len = U256::from_big_endian(log.data.get(160..192).ok_or( - L1WatcherError::FailedToDeserializeLog( - "Failed to parse calldata_len from log: log.data[96..128] out of bounds".to_owned(), - ), - )?); - let calldata = - log.data + let calldata_len = U256::from_big_endian(log.data.get(160..192).ok_or( + L1WatcherError::FailedToDeserializeLog( + "Failed to parse calldata_len from log: log.data[96..128] out of bounds".to_owned(), + ), + )?); + let calldata = log + .data .get(192..192 + calldata_len.as_usize()) .ok_or(L1WatcherError::FailedToDeserializeLog( "Failed to parse calldata from log: log.data[128..128 + calldata_len] out of bounds" .to_owned(), ))?; - Ok(( - mint_value, - to_address, - deposit_id, - recipient, - from, - gas_limit, - calldata.to_vec(), - )) + Ok(Self { + mint_value, + to_address, + deposit_id, + recipient, + from, + gas_limit, + calldata: calldata.to_vec(), + deposit_tx_hash, + }) + } } diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index b7cbf218a9..cabbf72edd 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -2,6 +2,7 @@ #![allow(clippy::expect_used)] use bytes::Bytes; use ethereum_types::{Address, H160, U256}; +use ethrex_common::types::BlockNumber; use ethrex_l2::utils::config::{read_env_file_by_config, ConfigMode}; use ethrex_l2_sdk::calldata::{self, Value}; use ethrex_rpc::clients::eth::{ @@ -33,13 +34,14 @@ const L2_GAS_COST_MAX_DELTA: U256 = U256([100_000_000_000_000, 0, 0, 0]); /// /// 1. Check balances on L1 and L2 /// 2. Deposit from L1 to L2 -/// 3. Check balances on L1 and L2 -/// 4. Transfer funds on L2 -/// 5. Check balances on L2 -/// 6. Withdraw funds from L2 to L1 -/// 7. Check balances on L1 and L2 -/// 8. Claim funds on L1 -/// 9. Check balances on L1 and L2 +/// 3. Check deposit receipt in L2 +/// 4. Check balances on L1 and L2 +/// 5. Transfer funds on L2 +/// 6. Check balances on L2 +/// 7. Withdraw funds from L2 to L1 +/// 8. Check balances on L1 and L2 +/// 9. Claim funds on L1 +/// 10. Check balances on L1 and L2 #[tokio::test] async fn l2_integration_test() -> Result<(), Box> { let eth_client = eth_client(); @@ -100,7 +102,16 @@ async fn l2_integration_test() -> Result<(), Box> { recoverable_fees_vault_balance ); - // 3. Check balances on L1 and L2 + // 3. Check deposit receipt on L2 + + wait_for_l2_deposit_receipt( + deposit_tx_receipt.block_info.block_number, + ð_client, + &proposer_client, + ) + .await?; + + // 4. Check balances on L1 and L2 println!("Checking balances on L1 and L2 after deposit"); @@ -159,7 +170,7 @@ async fn l2_integration_test() -> Result<(), Box> { "Recoverable Fees Balance: {}, This amount is given because of the L2 Privileged Transaction, a deposit shouldn't give a tip to the coinbase address if the gas sent as tip doesn't come from the L1.", first_deposit_recoverable_fees_vault_balance ); - // 4. Transfer funds on L2 + // 5. Transfer funds on L2 println!("Transferring funds on L2"); @@ -188,7 +199,7 @@ async fn l2_integration_test() -> Result<(), Box> { recoverable_fees_vault_balance ); - // 5. Check balances on L2 + // 6. Check balances on L2 println!("Checking balances on L2 after transfer"); @@ -214,7 +225,7 @@ async fn l2_integration_test() -> Result<(), Box> { "Random account balance should increase with transfer value" ); - // 6. Withdraw funds from L2 to L1 + // 7. Withdraw funds from L2 to L1 println!("Withdrawing funds from L2 to L1"); let withdraw_value = U256::from(100000000000000000000u128); @@ -230,7 +241,7 @@ async fn l2_integration_test() -> Result<(), Box> { .await .expect("Withdraw tx receipt not found"); - // 7. Check balances on L1 and L2 + // 8. Check balances on L1 and L2 println!("Checking balances on L1 and L2 after withdrawal"); @@ -254,7 +265,7 @@ async fn l2_integration_test() -> Result<(), Box> { "L2 balance should decrease with withdraw value + gas costs" ); - // 8. Claim funds on L1 + // 9. Claim funds on L1 println!("Claiming funds on L1"); @@ -301,7 +312,7 @@ async fn l2_integration_test() -> Result<(), Box> { let claim_tx_receipt = ethrex_l2_sdk::wait_for_transaction_receipt(claim_tx, ð_client, 15).await?; - // 9. Check balances on L1 and L2 + // 10. Check balances on L1 and L2 println!("Checking balances on L1 and L2 after claim"); @@ -496,12 +507,28 @@ async fn l2_deposit_with_contract_call() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box (Address, SecretKey) { let address = Address::from(keccak_hash::keccak(&pk.serialize_uncompressed()[1..])); (address, sk) } + +async fn wait_for_l2_deposit_receipt( + l1_receipt_block_number: BlockNumber, + eth_client: &EthClient, + proposer_client: &EthClient, +) -> Result<(), Box> { + let topic = + keccak(b"DepositInitiated(uint256,address,uint256,address,address,uint256,bytes,bytes32)"); + let logs = eth_client + .get_logs( + U256::from(l1_receipt_block_number), + U256::from(l1_receipt_block_number), + common_bridge_address(), + topic, + ) + .await?; + + let l2_deposit_tx_hash = + H256::from_slice(logs.first().unwrap().log.data.get(128..160).unwrap()); + + println!("Waiting for deposit transaction receipt on L2"); + + let _ = ethrex_l2_sdk::wait_for_transaction_receipt(l2_deposit_tx_hash, proposer_client, 1000) + .await?; + Ok(()) +} diff --git a/crates/networking/rpc/clients/eth/mod.rs b/crates/networking/rpc/clients/eth/mod.rs index baea1f7cd0..a223b6f361 100644 --- a/crates/networking/rpc/clients/eth/mod.rs +++ b/crates/networking/rpc/clients/eth/mod.rs @@ -950,10 +950,53 @@ impl EthClient { .await } + pub async fn get_pending_deposit_logs( + &self, + common_bridge_address: Address, + ) -> Result, EthClientError> { + let response = self + ._generic_call(b"getPendingDepositLogs()", common_bridge_address) + .await?; + Self::from_hex_string_to_h256_array(&response) + } + + pub fn from_hex_string_to_h256_array(hex_string: &str) -> Result, EthClientError> { + let bytes = hex::decode(hex_string.strip_prefix("0x").unwrap_or(hex_string)) + .map_err(|_| EthClientError::Custom("Invalid hex string".to_owned()))?; + + // The ABI encoding for dynamic arrays is: + // 1. Offset to data (32 bytes) + // 2. Length of array (32 bytes) + // 3. Array elements (each 32 bytes) + if bytes.len() < 64 { + return Err(EthClientError::Custom("Response too short".to_owned())); + } + + // Get the offset (should be 0x20 for simple arrays) + let offset = U256::from_big_endian(&bytes[0..32]).as_usize(); + + // Get the length of the array + let length = U256::from_big_endian(&bytes[offset..offset + 32]).as_usize(); + + // Calculate the start of the array data + let data_start = offset + 32; + let data_end = data_start + (length * 32); + + if data_end > bytes.len() { + return Err(EthClientError::Custom("Invalid array length".to_owned())); + } + + // Convert the slice directly to H256 array + bytes[data_start..data_end] + .chunks_exact(32) + .map(|chunk| Ok(H256::from_slice(chunk))) + .collect() + } + async fn _generic_call( &self, selector: &[u8], - on_chain_proposer_address: Address, + contract_address: Address, ) -> Result { let selector = keccak(selector) .as_bytes() @@ -968,11 +1011,7 @@ impl EthClient { calldata.extend(vec![0; leading_zeros]); let hex_string = self - .call( - on_chain_proposer_address, - calldata.into(), - Overrides::default(), - ) + .call(contract_address, calldata.into(), Overrides::default()) .await?; Ok(hex_string)