Skip to content

fix(l2): ignore deposits after state reconstruction #2642

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 5, 2025
Merged
7 changes: 4 additions & 3 deletions cmd/ethrex_l2/src/commands/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -224,11 +224,12 @@ impl Command {

// Iterate over each blob
let files: Vec<std::fs::DirEntry> = 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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/l2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 10 additions & 21 deletions crates/l2/contracts/src/l1/CommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 44 additions & 28 deletions crates/l2/sequencer/l1_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,33 +150,28 @@ impl L1Watcher {
) -> Result<Vec<H256>, L1WatcherError> {
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();
// Get the pending deposits from the contract.
let pending_deposits = self
.eth_client
.get_pending_deposit_logs(self.address)
.await?;

if deposit_already_processed {
for log in logs {
let (
mint_value,
to_address,
deposit_id,
recipient,
from,
gas_limit,
calldata,
deposit_hash,
) = parse_values_log(log.log)?;

if self
.deposit_already_processed(deposit_hash, &pending_deposits, store)
.await?
{
warn!("Deposit already processed (to: {recipient:#x}, value: {mint_value}, depositId: {deposit_id}), skipping.");
continue;
}
Expand Down Expand Up @@ -239,12 +234,32 @@ impl L1Watcher {

Ok(deposit_txs)
}

async fn deposit_already_processed(
&self,
deposit_hash: H256,
pending_deposits: &[H256],
store: &Store,
) -> Result<bool, L1WatcherError> {
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.
Ok(!pending_deposits.contains(&deposit_hash))
}
}

#[allow(clippy::type_complexity)]
fn parse_values_log(
log: RpcLogInfo,
) -> Result<(U256, H160, U256, H160, H160, U256, Vec<u8>), L1WatcherError> {
) -> Result<(U256, H160, U256, H160, H160, U256, Vec<u8>, H256), L1WatcherError> {
let mint_value = format!(
"{:#x}",
log.topics
Expand Down Expand Up @@ -307,7 +322,7 @@ fn parse_values_log(
),
)?);

let _deposit_tx_hash = H256::from_slice(log.data.get(128..160).ok_or(
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(),
),
Expand All @@ -334,5 +349,6 @@ fn parse_values_log(
from,
gas_limit,
calldata.to_vec(),
deposit_tx_hash,
))
}
59 changes: 42 additions & 17 deletions crates/l2/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,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<dyn std::error::Error>> {
let eth_client = eth_client();
Expand Down Expand Up @@ -100,7 +101,28 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
recoverable_fees_vault_balance
);

// 3. Check balances on L1 and L2
// 3. Check deposit receipt on L2

let topic =
keccak(b"DepositInitiated(uint256,address,uint256,address,address,uint256,bytes,bytes32)");
let logs = eth_client
.get_logs(
U256::from(deposit_tx_receipt.block_info.block_number),
U256::from(deposit_tx_receipt.block_info.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?;

// 4. Check balances on L1 and L2

println!("Checking balances on L1 and L2 after deposit");

Expand Down Expand Up @@ -159,7 +181,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
"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");

Expand Down Expand Up @@ -188,7 +210,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
recoverable_fees_vault_balance
);

// 5. Check balances on L2
// 6. Check balances on L2

println!("Checking balances on L2 after transfer");

Expand All @@ -214,7 +236,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
"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);
Expand All @@ -230,7 +252,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
.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");

Expand All @@ -254,7 +276,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
"L2 balance should decrease with withdraw value + gas costs"
);

// 8. Claim funds on L1
// 9. Claim funds on L1

println!("Claiming funds on L1");

Expand Down Expand Up @@ -301,7 +323,7 @@ async fn l2_integration_test() -> Result<(), Box<dyn std::error::Error>> {
let claim_tx_receipt =
ethrex_l2_sdk::wait_for_transaction_receipt(claim_tx, &eth_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");

Expand Down Expand Up @@ -496,6 +518,9 @@ async fn l2_deposit_with_contract_call() -> Result<(), Box<dyn std::error::Error
)
.await?;

// Gets the current block_number to search logs later.
let first_block = proposer_client.get_block_number().await?;

let deposit_tx_hash = eth_client
.send_eip1559_transaction(&deposit_tx, &l1_rich_wallet_private_key())
.await?;
Expand All @@ -519,10 +544,10 @@ async fn l2_deposit_with_contract_call() -> Result<(), Box<dyn std::error::Error
.await?;

// Wait for the event to be emitted
let mut blk_number = U256::zero();
let mut blk_number = first_block;
let topic = keccak(b"NumberSet(uint256)");
while proposer_client
.get_logs(U256::from(0), blk_number, contract_address, topic)
.get_logs(first_block, blk_number, contract_address, topic)
.await
.is_ok_and(|logs| logs.is_empty())
{
Expand All @@ -532,7 +557,7 @@ async fn l2_deposit_with_contract_call() -> Result<(), Box<dyn std::error::Error
}

let logs = proposer_client
.get_logs(U256::from(0), blk_number, contract_address, topic)
.get_logs(first_block, blk_number, contract_address, topic)
.await?;
println!("Logs: {logs:?}");

Expand Down
51 changes: 45 additions & 6 deletions crates/networking/rpc/clients/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -950,10 +950,53 @@ impl EthClient {
.await
}

pub async fn get_pending_deposit_logs(
&self,
common_bridge_address: Address,
) -> Result<Vec<H256>, 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<Vec<H256>, 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<String, EthClientError> {
let selector = keccak(selector)
.as_bytes()
Expand All @@ -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)
Expand Down
Loading