Repository: lambdaclass/ethrex
Pull request #2825: perf(l2): remove cloning state for limiting batch size
Author: login: tomip01
Labels: name: tech debt
description: Refactors, cleanups, etc
name: L2
description: Rollup client
Created: 2025-05-16T14:54:19Z | Merged: 2025-05-22T19:48:39Z

**Motivation**

In this PR we remove the cloning of the context before executing every transaction to check if it doesn't exceed the state diff size limit.

**Description**

* Add new functions specific for the L2 `apply_transaction_l2` and `execute_tx_l2`.
* Now `apply_transaction_l2` returns a CallFrameBackup that is needed for reverting the changes made by the transaction. This revert is different from the transaction revert, this has to undo every modification even the pre execute validation changes.
* Simplify the encoding of the structs `WithdrawalLog`, `DepositLog`, `BlockHeader` and `AccountStateDiff` when calculating the StateDiff. This leads to better consistency and being less error prone to future changes.
* Expose the VM function to restore the state from a `CallFrameBackup`.

**Comparison against main**
How to replicate:
Inside `crates/l2`
- Terminal 1: `init-l1`
- Terminal 2: `make deploy-l1 update-system-contracts init-l2`
- Terminal 3: `cargo run --manifest-path ../../cmd/load_test/Cargo.toml -- -k ../../test_data/private_keys.txt -t erc20 -N 50 -n http://localhost:1729`

For Terminal 3 if necessary run `ulimit -n 65536` before the command.

Gigagas comparison:
main: `[METRIC] BLOCK BUILDING THROUGHPUT: 0.0028166668076660267 Gigagas/s TIME SPENT: 30733 msecs`
this PR: `BLOCK BUILDING THROUGHPUT: 0.3342272162162162 Gigagas/s TIME SPENT: 259 msecs`

Loadtest comparision:
main: `Load test finished. Elapsed time: 254 seconds`
this PR: `Load test finished. Elapsed time: 34 seconds`

Closes #2413 
Closes #2655 



Linked issue #2413: l2/levm: add execute mode to levm that returns account updates size
For L2 we need after each transaction to know the size of `AccountUpdates` so that we know when to stop executing transactions and wrap up a block.
For this, we probably want to have a method in LEVM that executes a transaction (like the one we already have) but that also gives us as a result the size of the account updates (which have to be calculated at the end of the transaction). With that information we can then decide whether to continue executing transactions or not.

A possible approach would be to use `get_state_transitions()` at the end of the Tx without clearing the `CacheDB`.
Linked issue comments: user: login: avilagaston9
created_at: 2025-04-04T20:31:41Z
body: We also discuss the possibility of a method `Evm::execute_tx(tx, MAX_SIZE)` that excludes the transaction if the resulting `AccountUpdates` exceeds MAX_SIZE. This avoids the need to recover the previous state externally.
author_association: CONTRIBUTOR

Diff:
diff --git a/Cargo.lock b/Cargo.lock
index d01172395fb..8dd2c76080f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3196,6 +3196,7 @@ dependencies = [
  "hex",
  "jsonwebtoken",
  "keccak-hash",
+ "lazy_static",
  "rand 0.8.5",
  "reqwest",
  "secp256k1",
diff --git a/crates/blockchain/error.rs b/crates/blockchain/error.rs
index 2183e880521..bb12103edb9 100644
--- a/crates/blockchain/error.rs
+++ b/crates/blockchain/error.rs
@@ -16,6 +16,8 @@ pub enum ChainError {
     StoreError(#[from] StoreError),
     #[error("EVM error: {0}")]
     EvmError(#[from] EvmError),
+    #[error("Invalid Transaction: {0}")]
+    InvalidTransaction(String),
     #[error("{0}")]
     Custom(String),
 }
diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs
index 6dd8795d3b0..64f1b209771 100644
--- a/crates/blockchain/payload.rs
+++ b/crates/blockchain/payload.rs
@@ -456,7 +456,7 @@ impl Blockchain {
 
     /// Executes the transaction, updates gas-related context values & return the receipt
     /// The payload build context should have enough remaining gas to cover the transaction's gas_limit
-    pub fn apply_transaction(
+    fn apply_transaction(
         &self,
         head: &HeadTransaction,
         context: &mut PayloadBuildContext,
@@ -569,7 +569,7 @@ pub struct TransactionQueue {
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct HeadTransaction {
     pub tx: MempoolTransaction,
-    tip: u64,
+    pub tip: u64,
 }
 
 impl std::ops::Deref for HeadTransaction {
diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml
index 12b46a08874..60b846ccdf6 100644
--- a/crates/l2/Cargo.toml
+++ b/crates/l2/Cargo.toml
@@ -35,6 +35,7 @@ rand.workspace = true
 thiserror.workspace = true
 directories = "5.0.1"
 bincode = "1.3.3"
+lazy_static.workspace = true
 
 zkvm_interface = { path = "./prover/zkvm/interface/" }
 
diff --git a/crates/l2/prover/zkvm/interface/sp1/Cargo.lock b/crates/l2/prover/zkvm/interface/sp1/Cargo.lock
index 664b5a3b397..2f9643f2299 100644
--- a/crates/l2/prover/zkvm/interface/sp1/Cargo.lock
+++ b/crates/l2/prover/zkvm/interface/sp1/Cargo.lock
@@ -1336,6 +1336,7 @@ dependencies = [
  "bytes",
  "cfg-if",
  "derive_more 1.0.0",
+ "dyn-clone",
  "ethereum-types",
  "ethrex-common",
  "ethrex-levm",
diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs
index b4a80bae792..7b866d4d61f 100644
--- a/crates/l2/sequencer/block_producer/payload_builder.rs
+++ b/crates/l2/sequencer/block_producer/payload_builder.rs
@@ -3,28 +3,30 @@ use std::sync::Arc;
 
 use ethrex_blockchain::{
     constants::TX_GAS_COST,
-    payload::{PayloadBuildContext, PayloadBuildResult},
+    error::ChainError,
+    payload::{HeadTransaction, PayloadBuildContext, PayloadBuildResult},
     Blockchain,
 };
 use ethrex_common::{
-    types::{AccountInfo, Block, Receipt, Transaction, SAFE_BYTES_PER_BLOB},
-    Address,
+    types::{Block, Receipt, Transaction, SAFE_BYTES_PER_BLOB},
+    Address, U256,
 };
 use ethrex_metrics::metrics;
 
 #[cfg(feature = "metrics")]
 use ethrex_metrics::metrics_transactions::{MetricsTxStatus, MetricsTxType, METRICS_TX};
 use ethrex_storage::Store;
+use ethrex_vm::{backends::CallFrameBackup, Evm, EvmError};
 use std::ops::Div;
 use tokio::time::Instant;
-use tracing::debug;
+use tracing::{debug, error};
 
 use crate::{
     sequencer::{
-        errors::BlockProducerError,
+        errors::{BlockProducerError, StateDiffError},
         state_diff::{
-            get_nonce_diff, L2_DEPOSIT_SIZE, L2_WITHDRAWAL_SIZE, LAST_HEADER_FIELDS_SIZE,
-            TX_STATE_DIFF_SIZE,
+            AccountStateDiff, BLOCK_HEADER_LEN, DEPOSITS_LOG_LEN, SIMPLE_TX_STATE_DIFF_SIZE,
+            WITHDRAWAL_LOG_LEN,
         },
     },
     utils::helpers::{is_deposit_l2, is_withdrawal_l2},
@@ -73,10 +75,10 @@ pub async fn fill_transactions(
     context: &mut PayloadBuildContext,
     store: &Store,
 ) -> Result<(), BlockProducerError> {
-    // Two bytes for the len
-    let (mut acc_withdrawals_size, mut acc_deposits_size): (usize, usize) = (2, 2);
-    let mut acc_state_diff_size = 0;
-    let mut accounts_info_cache = HashMap::new();
+    // version (u8) + header fields (struct) + withdrawals_len (u16) + deposits_len (u16) + accounts_diffs_len (u16)
+    let mut acc_size_without_accounts = 1 + *BLOCK_HEADER_LEN + 2 + 2 + 2;
+    let mut size_accounts_diffs = 0;
+    let mut account_diffs = HashMap::new();
 
     let chain_config = store.get_chain_config()?;
     let max_blob_number_per_block: usize = chain_config
@@ -98,7 +100,9 @@ pub async fn fill_transactions(
         };
 
         // Check if we have enough space for the StateDiff to run more transactions
-        if acc_state_diff_size + TX_STATE_DIFF_SIZE > SAFE_BYTES_PER_BLOB {
+        if acc_size_without_accounts + size_accounts_diffs + SIMPLE_TX_STATE_DIFF_SIZE
+            > SAFE_BYTES_PER_BLOB
+        {
             debug!("No more StateDiff space to run transactions");
             break;
         };
@@ -150,41 +154,14 @@ pub async fn fill_transactions(
         // or we want it before the return?
         metrics!(METRICS_TX.inc_tx());
 
-        let previous_context = context.clone();
-
         // Execute tx
-        let receipt = match blockchain.apply_transaction(&head_tx, context) {
-            Ok(receipt) => {
-                // This call is the part that differs from the original `fill_transactions`.
-                if !update_state_diff_size(
-                    &mut acc_withdrawals_size,
-                    &mut acc_deposits_size,
-                    &mut acc_state_diff_size,
-                    head_tx.clone().into(),
-                    &receipt,
-                    context,
-                    &mut accounts_info_cache,
-                )
-                .await?
-                {
-                    debug!(
-                        "Skipping transaction: {}, doesn't fit in blob_size",
-                        head_tx.tx.compute_hash()
-                    );
-                    // We don't have enough space in the blob for the transaction, so we skip all txs from this account
-                    txs.pop();
-                    *context = previous_context.clone();
-                    continue;
-                }
-                txs.shift()?;
-                // Pull transaction from the mempool
-                blockchain.remove_transaction_from_pool(&head_tx.tx.compute_hash())?;
-
+        let (receipt, transaction_backup) = match apply_transaction_l2(&head_tx, context) {
+            Ok((receipt, transaction_backup)) => {
                 metrics!(METRICS_TX.inc_tx_with_status_and_type(
                     MetricsTxStatus::Succeeded,
                     MetricsTxType(head_tx.tx_type())
                 ));
-                receipt
+                (receipt, transaction_backup)
             }
             // Ignore following txs from sender
             Err(e) => {
@@ -197,6 +174,42 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_account_diffs_in_tx(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let (tx_size_without_accounts, new_accounts_diff_size) = calculate_tx_diff_size(
+            &merged_diffs,
+            &head_tx,
+            &receipt,
+            *DEPOSITS_LOG_LEN,
+            *WITHDRAWAL_LOG_LEN,
+        )?;
+
+        if acc_size_without_accounts + tx_size_without_accounts + new_accounts_diff_size
+            > SAFE_BYTES_PER_BLOB
+        {
+            debug!(
+                "No more StateDiff space to run this transactions. Skipping transaction: {:?}",
+                tx_hash
+            );
+            txs.pop();
+
+            // This transaction is too big, we need to restore the state
+            context.vm.restore_cache_state(transaction_backup)?;
+
+            continue;
+        }
+
+        txs.shift()?;
+        // Pull transaction from the mempool
+        blockchain.remove_transaction_from_pool(&head_tx.tx.compute_hash())?;
+
+        // We only add the withdrawals and deposits length because the accounts diffs may change
+        acc_size_without_accounts += tx_size_without_accounts;
+        size_accounts_diffs = new_accounts_diff_size;
+        // Include the new accounts diffs
+        account_diffs = merged_diffs;
         // Add transaction to block
         debug!("Adding transaction: {} to payload", tx_hash);
         context.payload.body.transactions.push(head_tx.into());
@@ -206,97 +219,194 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+/// Returns the state diffs introduced by the transaction by comparing the call frame backup
+/// (which holds the state before executing the transaction) with the current state of the cache
+/// (which contains all the writes performed by the transaction).
+fn get_account_diffs_in_tx(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account =
+                    db.cache
+                        .get(address)
+                        .ok_or(BlockProducerError::FailedToGetDataFrom(
+                            "DB Cache".to_owned(),
+                        ))?;
+
+                let nonce_diff: u16 = (new_account.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account.info.balance != original_account.info.balance {
+                    Some(new_account.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account.code != original_account.code {
+                    Some(new_account.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info =
+                    db.cache
+                        .get(address)
+                        .ok_or(BlockProducerError::FailedToGetDataFrom(
+                            "DB Cache".to_owned(),
+                        ))?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info.storage.get(key).ok_or(
+                            BlockProducerError::FailedToGetDataFrom(
+                                "Account info Storage".to_owned(),
+                            ),
+                        )?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
-        }
+/// Combines the diffs from the current transaction with the existing block diffs.
+/// Transaction diffs represent state changes from the latest transaction execution,
+/// while previous diffs accumulate all changes included in the block so far.
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    tx_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in tx_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            // New balance could be None if a transaction didn't change the balance
+            // but we want to keep the previous changes made in a transaction included in the block
+            existing_diff.new_balance = diff.new_balance.or(existing_diff.new_balance);
 
-        let nonce_diff = get_nonce_diff(
-            &account_update,
-            &context.store,
-            Some(accounts_info_cache),
-            context.block_number(),
-        )
-        .await
-        .map_err(|e| {
-            BlockProducerError::Custom(format!("Block Producer failed to get nonce diff: {e}"))
-        })?;
-        if nonce_diff != 0 {
-            modified_accounts_size += 2; // 2bytes | nonce_diff(u16)
-        }
-        // for each added_storage: 32bytes + 32bytes | key(H256) + value(U256)
-        modified_accounts_size += account_update.added_storage.len() * 2 * 32;
+            // We add the nonce diff to the existing one to keep track of the total nonce diff
+            existing_diff.nonce_diff += diff.nonce_diff;
 
-        if let Some(bytecode) = &account_update.code {
-            modified_accounts_size += 2; // 2bytes | bytecode_len(u16)
-            modified_accounts_size += bytecode.len(); // (len)bytes | bytecode(Bytes)
+            // we need to overwrite only the new storage storage slot with the new values
+            existing_diff.storage.extend(diff.storage);
+
+            // Take the bytecode from the tx diff if present, avoiding clone if not needed
+            if diff.bytecode.is_some() {
+                existing_diff.bytecode = diff.bytecode;
+            }
+
+            // Take the new bytecode hash if it is present
+            existing_diff.bytecode_hash = diff.bytecode_hash.or(existing_diff.bytecode_hash);
+        } else {
+            merged_diffs.insert(address, diff);
         }
     }
-    Ok(modified_accounts_size)
+    merged_diffs
+}
+
+/// Calculates the size of the state diffs introduced by the transaction, including
+/// the size of withdrawals and deposits logs for this transaction, and the total
+/// size of all account diffs accumulated so far in the block.
+/// This is necessary because each transaction can modify accounts that were already
+/// changed by previous transactions, so we must recalculate the total diff size each time.
+fn calculate_tx_diff_size(
+    merged_diffs: &HashMap<Address, AccountStateDiff>,
+    head_tx: &HeadTransaction,
+    receipt: &Receipt,
+    deposits_log_len: usize,
+    withdrawals_log_len: usize,
+) -> Result<(usize, usize), BlockProducerError> {
+    let mut tx_state_diff_size = 0;
+    let mut new_accounts_diff_size = 0;
+
+    for (address, diff) in merged_diffs.iter() {
+        let encoded = match diff.encode(address) {
+            Ok(encoded) => encoded,
+            Err(StateDiffError::EmptyAccountDiff) => {
+                debug!("Skipping empty account diff for address: {address}");
+                continue;
+            }
+            Err(e) => {
+                error!("Failed to encode account state diff: {e}");
+                return Err(BlockProducerError::FailedToEncodeAccountStateDiff(e));
+            }
+        };
+        new_accounts_diff_size += encoded.len();
+    }
+
+    if is_deposit_l2(head_tx) {
+        tx_state_diff_size += deposits_log_len;
+    }
+    if is_withdrawal_l2(&head_tx.clone().into(), receipt)? {
+        tx_state_diff_size += withdrawals_log_len;
+    }
+
+    Ok((tx_state_diff_size, new_accounts_diff_size))
 }
diff --git a/crates/l2/sequencer/errors.rs b/crates/l2/sequencer/errors.rs
index 63aa51a173c..38f4b50c6a2 100644
--- a/crates/l2/sequencer/errors.rs
+++ b/crates/l2/sequencer/errors.rs
@@ -124,6 +124,10 @@ pub enum BlockProducerError {
     Custom(String),
     #[error("Failed to parse withdrawal: {0}")]
     FailedToParseWithdrawal(#[from] UtilsError),
+    #[error("Failed to encode AccountStateDiff: {0}")]
+    FailedToEncodeAccountStateDiff(#[from] StateDiffError),
+    #[error("Failed to get data from: {0}")]
+    FailedToGetDataFrom(String),
 }
 
 #[derive(Debug, thiserror::Error)]
diff --git a/crates/l2/sequencer/state_diff.rs b/crates/l2/sequencer/state_diff.rs
index 536c3fb0537..57ebd287e0d 100644
--- a/crates/l2/sequencer/state_diff.rs
+++ b/crates/l2/sequencer/state_diff.rs
@@ -9,24 +9,24 @@ use ethrex_trie::Trie;
 
 use super::errors::StateDiffError;
 
-// transactions_root(H256) + receipts_root(H256) + parent_hash(H256) + gas_limit(u64) + gas_used(u64) + timestamp(u64)
-// block_number(u64) + base_fee_per_gas(u64)
-// 32bytes + 32bytes + 32bytes + 8bytes + 8bytes + 8bytes + 8bytes + 8bytes
-pub const LAST_HEADER_FIELDS_SIZE: usize = 136;
+use lazy_static::lazy_static;
 
-// address(H160) + amount(U256) + tx_hash(H256).
-// 20bytes + 32bytes + 32bytes.
-pub const L2_WITHDRAWAL_SIZE: usize = 84;
+lazy_static! {
+    /// The serialized length of a default withdrawal log
+    pub static ref WITHDRAWAL_LOG_LEN: usize = WithdrawalLog::default().encode().len();
 
-// address(H160) + amount(U256).
-// 20bytes + 32bytes
-pub const L2_DEPOSIT_SIZE: usize = 52;
+    /// The serialized length of a default deposit log
+    pub static ref DEPOSITS_LOG_LEN: usize = DepositLog::default().encode().len();
+
+    /// The serialized lenght of a default block header
+    pub static ref BLOCK_HEADER_LEN: usize = encode_block_header(&BlockHeader::default()).len();
+}
 
 // State diff size for a simple transfer.
 // Two `AccountUpdates` with new_balance, one of which also has nonce_diff.
-pub const TX_STATE_DIFF_SIZE: usize = 116;
+pub const SIMPLE_TX_STATE_DIFF_SIZE: usize = 116;
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct AccountStateDiff {
     pub new_balance: Option<U256>,
     pub nonce_diff: u16,
@@ -44,20 +44,39 @@ pub enum AccountStateDiffType {
     BytecodeHash = 16,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct WithdrawalLog {
     pub address: Address,
     pub amount: U256,
     pub tx_hash: H256,
 }
 
-#[derive(Clone)]
+impl WithdrawalLog {
+    pub fn encode(&self) -> Vec<u8> {
+        let mut encoded = Vec::new();
+        encoded.extend(self.address.0);
+        encoded.extend_from_slice(&self.amount.to_big_endian());
+        encoded.extend(&self.tx_hash.0);
+        encoded
+    }
+}
+
+#[derive(Clone, Default)]
 pub struct DepositLog {
     pub address: Address,
     pub amount: U256,
     pub nonce: u64,
 }
 
+impl DepositLog {
+    pub fn encode(&self) -> Vec<u8> {
+        let mut encoded = Vec::new();
+        encoded.extend(self.address.0);
+        encoded.extend_from_slice(&self.amount.to_big_endian());
+        encoded
+    }
+}
+
 #[derive(Clone)]
 pub struct StateDiff {
     pub version: u8,
@@ -113,6 +132,20 @@ impl Default for StateDiff {
     }
 }
 
+pub fn encode_block_header(block_header: &BlockHeader) -> Vec<u8> {
+    let mut encoded = Vec::new();
+    encoded.extend(block_header.transactions_root.0);
+    encoded.extend(block_header.receipts_root.0);
+    encoded.extend(block_header.parent_hash.0);
+    encoded.extend(block_header.gas_limit.to_be_bytes());
+    encoded.extend(block_header.gas_used.to_be_bytes());
+    encoded.extend(block_header.timestamp.to_be_bytes());
+    encoded.extend(block_header.number.to_be_bytes());
+    encoded.extend(block_header.base_fee_per_gas.unwrap_or(0).to_be_bytes());
+
+    encoded
+}
+
 impl StateDiff {
     pub fn encode(&self) -> Result<Bytes, StateDiffError> {
         if self.version != 1 {
@@ -122,15 +155,8 @@ impl StateDiff {
         let mut encoded: Vec<u8> = Vec::new();
         encoded.push(self.version);
 
-        // Last header fields
-        encoded.extend(self.last_header.transactions_root.0);
-        encoded.extend(self.last_header.receipts_root.0);
-        encoded.extend(self.last_header.parent_hash.0);
-        encoded.extend(self.last_header.gas_limit.to_be_bytes());
-        encoded.extend(self.last_header.gas_used.to_be_bytes());
-        encoded.extend(self.last_header.timestamp.to_be_bytes());
-        encoded.extend(self.last_header.number.to_be_bytes());
-        encoded.extend(self.last_header.base_fee_per_gas.unwrap_or(0).to_be_bytes());
+        let header_encoded = encode_block_header(&self.last_header);
+        encoded.extend(header_encoded);
 
         let modified_accounts_len: u16 = self
             .modified_accounts
@@ -140,25 +166,22 @@ impl StateDiff {
         encoded.extend(modified_accounts_len.to_be_bytes());
 
         for (address, diff) in &self.modified_accounts {
-            let (r#type, diff_encoded) = diff.encode()?;
-            encoded.extend(r#type.to_be_bytes());
-            encoded.extend(address.0);
-            encoded.extend(diff_encoded);
+            let account_encoded = diff.encode(address)?;
+            encoded.extend(account_encoded);
         }
 
         let withdrawal_len: u16 = self.withdrawal_logs.len().try_into()?;
         encoded.extend(withdrawal_len.to_be_bytes());
         for withdrawal in self.withdrawal_logs.iter() {
-            encoded.extend(withdrawal.address.0);
-            encoded.extend_from_slice(&withdrawal.amount.to_big_endian());
-            encoded.extend(&withdrawal.tx_hash.0);
+            let withdrawal_encoded = withdrawal.encode();
+            encoded.extend(withdrawal_encoded);
         }
 
         let deposits_len: u16 = self.deposit_logs.len().try_into()?;
         encoded.extend(deposits_len.to_be_bytes());
         for deposit in self.deposit_logs.iter() {
-            encoded.extend(deposit.address.0);
-            encoded.extend_from_slice(&deposit.amount.to_big_endian());
+            let deposit_encoded = deposit.encode();
+            encoded.extend(deposit_encoded);
         }
 
         Ok(Bytes::from(encoded))
@@ -286,7 +309,7 @@ impl StateDiff {
 }
 
 impl AccountStateDiff {
-    pub fn encode(&self) -> Result<(u8, Bytes), StateDiffError> {
+    pub fn encode(&self, address: &Address) -> Result<Vec<u8>, StateDiffError> {
         if self.bytecode.is_some() && self.bytecode_hash.is_some() {
             return Err(StateDiffError::BytecodeAndBytecodeHashSet);
         }
@@ -339,7 +362,12 @@ impl AccountStateDiff {
             return Err(StateDiffError::EmptyAccountDiff);
         }
 
-        Ok((r#type, Bytes::from(encoded)))
+        let mut result = Vec::with_capacity(1 + address.0.len() + encoded.len());
+        result.extend(r#type.to_be_bytes());
+        result.extend(address.0);
+        result.extend(encoded);
+
+        Ok(result)
     }
 
     /// Returns a tuple of the number of bytes read, the address of the account
diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs
index ac20fba3b90..195c14745a4 100644
--- a/crates/networking/rpc/engine/payload.rs
+++ b/crates/networking/rpc/engine/payload.rs
@@ -759,6 +759,10 @@ async fn try_execute_payload(
             error!("{e} for block {block_hash}");
             Err(RpcErr::Internal(e.to_string()))
         }
+        Err(ChainError::InvalidTransaction(e)) => {
+            error!("{e} for block {block_hash}");
+            Err(RpcErr::Internal(e.to_string()))
+        }
         Ok(()) => {
             info!("Block with hash {block_hash} executed and added to storage succesfully");
             Ok(PayloadStatus::valid_with_hash(block_hash))
diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs
index d4849b34a43..147a3c86831 100644
--- a/crates/vm/backends/levm/mod.rs
+++ b/crates/vm/backends/levm/mod.rs
@@ -18,9 +18,11 @@ use ethrex_common::{
     },
     Address, H256, U256,
 };
+use ethrex_levm::call_frame::CallFrameBackup;
 use ethrex_levm::constants::{SYS_CALL_GAS_LIMIT, TX_BASE_COST};
 use ethrex_levm::db::gen_db::GeneralizedDatabase;
 use ethrex_levm::errors::TxValidationError;
+use ethrex_levm::utils::restore_cache_state;
 use ethrex_levm::EVMConfig;
 use ethrex_levm::{
     errors::{ExecutionReport, TxResult, VMError},
@@ -97,15 +99,12 @@ impl LEVM {
         Ok(BlockExecutionResult { receipts, requests })
     }
 
-    pub fn execute_tx(
-        // The transaction to execute.
+    fn setup_env(
         tx: &Transaction,
-        // The transactions recovered address
         tx_sender: Address,
-        // The block header for the current block.
         block_header: &BlockHeader,
         db: &mut GeneralizedDatabase,
-    ) -> Result<ExecutionReport, EvmError> {
+    ) -> Result<Environment, EvmError> {
         let chain_config = db.store.get_chain_config();
         let gas_price: U256 = tx
             .effective_gas_price(block_header.base_fee_per_gas)
@@ -138,10 +137,58 @@ impl LEVM {
             is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
         };
 
+        Ok(env)
+    }
+
+    pub fn execute_tx(
+        // The transaction to execute.
+        tx: &Transaction,
+        // The transactions recovered address
+        tx_sender: Address,
+        // The block header for the current block.
+        block_header: &BlockHeader,
+        db: &mut GeneralizedDatabase,
+    ) -> Result<ExecutionReport, EvmError> {
+        let env = Self::setup_env(tx, tx_sender, block_header, db)?;
         let mut vm = VM::new(env, db, tx);
 
         vm.execute().map_err(VMError::into)
     }
+
+    pub fn execute_tx_l2(
+        // The transaction to execute.
+        tx: &Transaction,
+        // The transactions recovered address
+        tx_sender: Address,
+        // The block header for the current block.
+        block_header: &BlockHeader,
+        db: &mut GeneralizedDatabase,
+    ) -> Result<(ExecutionReport, CallFrameBackup), EvmError> {
+        let env = Self::setup_env(tx, tx_sender, block_header, db)?;
+        let mut vm = VM::new(env, db, tx);
+
+        let report_result = vm.execute().map_err(EvmError::from)?;
+
+        // Here we differ from the execute_tx function from the L1.
+        // We need to check if the transaction exceeded the blob size limit.
+        // If it did, we need to revert the state changes made by the transaction and return the error.
+        let call_frame_backup = vm
+            .call_frames
+            .pop()
+            .ok_or(VMError::OutOfBounds)?
+            .call_frame_backup;
+
+        Ok((report_result, call_frame_backup))
+    }
+
+    pub fn restore_cache_state(
+        db: &mut GeneralizedDatabase,
+        call_frame_backup: CallFrameBackup,
+    ) -> Result<(), EvmError> {
+        restore_cache_state(db, call_frame_backup).map_err(VMError::from)?;
+        Ok(())
+    }
+
     pub fn simulate_tx_from_generic(
         // The transaction to execute.
         tx: &GenericTransaction,
diff --git a/crates/vm/backends/mod.rs b/crates/vm/backends/mod.rs
index 7522264c14e..3b08c8419cf 100644
--- a/crates/vm/backends/mod.rs
+++ b/crates/vm/backends/mod.rs
@@ -12,6 +12,7 @@ use ethrex_common::types::{
     AccessList, Block, BlockHeader, Fork, GenericTransaction, Receipt, Transaction, Withdrawal,
 };
 use ethrex_common::Address;
+pub use ethrex_levm::call_frame::CallFrameBackup;
 use ethrex_levm::db::gen_db::GeneralizedDatabase;
 use ethrex_levm::db::CacheDB;
 use ethrex_storage::AccountUpdate;
@@ -149,6 +150,47 @@ impl Evm {
         }
     }
 
+    pub fn execute_tx_l2(
+        &mut self,
+        tx: &Transaction,
+        block_header: &BlockHeader,
+        remaining_gas: &mut u64,
+        sender: Address,
+    ) -> Result<(Receipt, u64, CallFrameBackup), EvmError> {
+        match self {
+            Evm::REVM { .. } => Err(EvmError::InvalidEVM(
+                "L2 transactions are not supported in REVM".to_string(),
+            )),
+            Evm::LEVM { db } => {
+                let (execution_report, transaction_backup) =
+                    LEVM::execute_tx_l2(tx, sender, block_header, db)?;
+
+                *remaining_gas = remaining_gas.saturating_sub(execution_report.gas_used);
+
+                let receipt = Receipt::new(
+                    tx.tx_type(),
+                    execution_report.is_success(),
+                    block_header.gas_limit - *remaining_gas,
+                    execution_report.logs.clone(),
+                );
+
+                Ok((receipt, execution_report.gas_used, transaction_backup))
+            }
+        }
+    }
+
+    pub fn restore_cache_state(
+        &mut self,
+        call_frame_backup: CallFrameBackup,
+    ) -> Result<(), EvmError> {
+        match self {
+            Evm::REVM { .. } => Err(EvmError::InvalidEVM(
+                "Cache state is not supported in REVM".to_string(),
+            )),
+            Evm::LEVM { db } => LEVM::restore_cache_state(db, call_frame_backup),
+        }
+    }
+
     /// Wraps [REVM::beacon_root_contract_call], [REVM::process_block_hash_history]
     /// and [LEVM::beacon_root_contract_call], [LEVM::process_block_hash_history].
     /// This function is used to run/apply all the system contracts to the state.
diff --git a/crates/vm/levm/src/call_frame.rs b/crates/vm/levm/src/call_frame.rs
index bfd5002fd62..f4f10d4a15e 100644
--- a/crates/vm/levm/src/call_frame.rs
+++ b/crates/vm/levm/src/call_frame.rs
@@ -1,10 +1,9 @@
 use crate::{
     constants::STACK_LIMIT,
-    db::cache,
     errors::{InternalError, OutOfGasError, VMError},
     memory::Memory,
     opcodes::Opcode,
-    utils::get_valid_jump_destinations,
+    utils::{get_valid_jump_destinations, restore_cache_state},
     vm::VM,
 };
 use bytes::Bytes;
@@ -221,27 +220,7 @@ impl<'a> VM<'a> {
     /// Restores the cache state to the state before changes made during a callframe.
     pub fn restore_cache_state(&mut self) -> Result<(), VMError> {
         let callframe_backup = self.current_call_frame()?.call_frame_backup.clone();
-        for (address, account) in callframe_backup.original_accounts_info {
-            if let Some(current_account) = cache::get_account_mut(&mut self.db.cache, &address) {
-                current_account.info = account.info;
-                current_account.code = account.code;
-            }
-        }
-
-        for (address, storage) in callframe_backup.original_account_storage_slots {
-            // This call to `get_account_mut` should never return None, because we are looking up accounts
-            // that had their storage modified, which means they should be in the cache. That's why
-            // we return an internal error in case we haven't found it.
-            let account = cache::get_account_mut(&mut self.db.cache, &address).ok_or(
-                VMError::Internal(crate::errors::InternalError::AccountNotFound),
-            )?;
-
-            for (key, value) in storage {
-                account.storage.insert(key, value);
-            }
-        }
-
-        Ok(())
+        restore_cache_state(self.db, callframe_backup)
     }
 
     // The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
diff --git a/crates/vm/levm/src/db/gen_db.rs b/crates/vm/levm/src/db/gen_db.rs
index 29acab62a91..904e81b3a15 100644
--- a/crates/vm/levm/src/db/gen_db.rs
+++ b/crates/vm/levm/src/db/gen_db.rs
@@ -143,7 +143,7 @@ impl<'a> VM<'a> {
         Ok(account.info.nonce)
     }
 
-    /// Inserts account to cache backing up the previus state of it in the CacheBackup (if it wasn't already backed up)
+    /// Inserts account to cache backing up the previous state of it in the CacheBackup (if it wasn't already backed up)
     pub fn insert_account(&mut self, address: Address, account: Account) -> Result<(), VMError> {
         self.backup_account_info(address)?;
         let _ = cache::insert_account(&mut self.db.cache, address, account);
diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs
index f1539e6f7ea..687d3a06b7f 100644
--- a/crates/vm/levm/src/utils.rs
+++ b/crates/vm/levm/src/utils.rs
@@ -1,6 +1,7 @@
 use crate::{
+    call_frame::CallFrameBackup,
     constants::*,
-    db::gen_db::GeneralizedDatabase,
+    db::{cache, gen_db::GeneralizedDatabase},
     errors::{InternalError, OutOfGasError, TxValidationError, VMError},
     gas_cost::{
         self, fake_exponential, ACCESS_LIST_ADDRESS_COST, ACCESS_LIST_STORAGE_KEY_COST,
@@ -136,6 +137,36 @@ pub fn get_valid_jump_destinations(code: &Bytes) -> Result<HashSet<usize>, VMErr
     Ok(valid_jump_destinations)
 }
 
+// ================== Backup related functions =======================
+
+/// Restore the state of the cache to the state it in the callframe backup.
+pub fn restore_cache_state(
+    db: &mut GeneralizedDatabase,
+    callframe_backup: CallFrameBackup,
+) -> Result<(), VMError> {
+    for (address, account) in callframe_backup.original_accounts_info {
+        if let Some(current_account) = cache::get_account_mut(&mut db.cache, &address) {
+            current_account.info = account.info;
+            current_account.code = account.code;
+        }
+    }
+
+    for (address, storage) in callframe_backup.original_account_storage_slots {
+        // This call to `get_account_mut` should never return None, because we are looking up accounts
+        // that had their storage modified, which means they should be in the cache. That's why
+        // we return an internal error in case we haven't found it.
+        let account = cache::get_account_mut(&mut db.cache, &address).ok_or(VMError::Internal(
+            crate::errors::InternalError::AccountNotFound,
+        ))?;
+
+        for (key, value) in storage {
+            account.storage.insert(key, value);
+        }
+    }
+
+    Ok(())
+}
+
 // ================= Blob hash related functions =====================
 pub fn get_base_fee_per_blob_gas(
     block_excess_blob_gas: Option<U256>,
diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs
index 59083ceec73..3ff4bdde292 100644
--- a/crates/vm/levm/src/vm.rs
+++ b/crates/vm/levm/src/vm.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "l2")]
+use crate::call_frame::CallFrameBackup;
 use crate::{
     call_frame::CallFrame,
     db::gen_db::GeneralizedDatabase,
@@ -96,6 +98,11 @@ impl<'a> VM<'a> {
             return Err(e);
         }
 
+        // Here we need to backup the callframe because in the L2 we want to undo a transaction if it exceeds blob size
+        // even if the transaction succeeds.
+        #[cfg(feature = "l2")]
+        let callframe_backup = self.current_call_frame()?.call_frame_backup.clone();
+
         // Clear callframe backup so that changes made in prepare_execution are written in stone.
         // We want to apply these changes even if the Tx reverts. E.g. Incrementing sender nonce
         self.current_call_frame_mut()?.call_frame_backup.clear();
@@ -112,6 +119,20 @@ impl<'a> VM<'a> {
         let mut report = self.run_execution()?;
 
         self.finalize_execution(&mut report)?;
+
+        // We want to restore to the initial state, this includes reverting the changes made by the prepare execution
+        // and the changes made by the execution itself.
+        #[cfg(feature = "l2")]
+        {
+            let current_backup: &mut CallFrameBackup =
+                &mut self.current_call_frame_mut()?.call_frame_backup;
+            current_backup
+                .original_accounts_info
+                .extend(callframe_backup.original_accounts_info);
+            current_backup
+                .original_account_storage_slots
+                .extend(callframe_backup.original_account_storage_slots);
+        }
         Ok(report)
     }
 


Reviews:
user: login: avilagaston9
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:36:35Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:02:20Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:07:24Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T17:28:23Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T18:13:28Z
user: login: avilagaston9
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T18:38:23Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:01:04Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:05:09Z
user: login: JereSalo
body: I suggested some changes. I'm mostly worried about the merging strategy used but it's very likely that I don't completely understand it.
As a suggestion, I'd leave rustdocs on every function and any place in which you feel the code is not 100% intuitive for someone not very experienced
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:07:59Z
user: login: iovoid
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:49:44Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T20:58:24Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:07:04Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:08:58Z
user: login: iovoid
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:01:21Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:25:36Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T12:33:02Z
user: login: JereSalo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:00:15Z
user: login: JereSalo
body: I'd clarify somewhere that this calculation of state diff size is not 100% accurate because it isn't contemplating that for example: 
Storage of an account can go from 1 to 2 to 1 again and it won't have changed but we say that it did because we are not comparing against the original value.
I'd just leave that written down somewhere in a comment or `.md`, even create an issue if you want to.

I left some other suggestions but they are not critical.

Great PR! 🎉 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:25:13Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:47:45Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T13:58:47Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:01:43Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:02:08Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:02:13Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:13:02Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:13:10Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:20:59Z
user: login: avilagaston9
body: Some minor comments
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:36:18Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:45:04Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:45:12Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:45:15Z
user: login: tomip01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:35:14Z
user: login: JereSalo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:48:59Z
user: login: avilagaston9
body: Tested with:
```
make init
cargo run --release --manifest-path ./cmd/load_test/Cargo.toml -- -k ./test_data/private_keys.txt -t eth-transfers -n http://localhost:1729
```
Got the following throughput:
```
[METRIC] BLOCK BUILDING THROUGHPUT: 0.1061027397260274 Gigagas/s TIME SPENT: 438 msecs
```

Also tested the state reconstruction and it works!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T17:48:22Z
user: login: avilagaston9
body: Blocking until @jrchatruc reviews it!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T17:53:47Z
user: login: jrchatruc
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T19:38:06Z

Review comments:
path: crates/blockchain/payload.rs
user: login: avilagaston9
body: Let's move all this to`crates/l2/sequencer/block_producer/payload_builder.rs`
created_at: 2025-05-19T15:05:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,6 +304,20 @@ impl BlockHeader {
         self.encode(&mut buf);
         keccak(buf)
     }
+
+    pub fn encode_for_state_diff(&self) -> Vec<u8> {
+        let mut encoded = Vec::new();
+        encoded.extend(self.transactions_root.0);
+        encoded.extend(self.receipts_root.0);
+        encoded.extend(self.parent_hash.0);
+        encoded.extend(self.gas_limit.to_be_bytes());
+        encoded.extend(self.gas_used.to_be_bytes());
+        encoded.extend(self.timestamp.to_be_bytes());
+        encoded.extend(self.number.to_be_bytes());
+        encoded.extend(self.base_fee_per_gas.unwrap_or(0).to_be_bytes());
+
+        encoded
+    }
path: crates/common/types/block.rs
user: login: avilagaston9
body: Given that all the header fields are public, let's move this to `state_diff.rs`
created_at: 2025-05-19T15:08:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +176,58 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let mut tx_state_diff_size = 0;
+        let mut new_accounts_diff_size = 0;
+
+        for (address, diff) in merged_diffs.iter() {
+            let encoded = match diff.encode(address) {
+                Ok(encoded) => encoded,
+                Err(StateDiffError::EmptyAccountDiff) => {
+                    debug!("Skipping empty account diff for address: {address}");
+                    continue;
+                }
+                Err(e) => {
+                    error!("Failed to encode account state diff: {e}");
+                    return Err(BlockProducerError::FailedToEncodeAccountStateDiff(e));
+                }
+            };
+            new_accounts_diff_size += encoded.len();
+        }
+
+        if is_deposit_l2(&head_tx) {
+            tx_state_diff_size += deposits_log_len;
+        }
+        if is_withdrawal_l2(&head_tx.clone().into(), &receipt)? {
+            tx_state_diff_size += withdrawals_log_len;
+        }
+
+        if size_without_accounts + tx_state_diff_size + new_accounts_diff_size > SAFE_BYTES_PER_BLOB
+        {
+            debug!(
+                "No more StateDiff space to run this transactions. Skipping transaction: {:?}",
+                tx_hash
+            );
+            txs.pop();
+
+            // This transaction is too big, we need to restore the state
+            context.vm.restore_cache_state(transaction_backup)?;
+
+            continue;
+        }
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: Can we move all of this calculation above into a separate function?
created_at: 2025-05-19T15:54:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +237,107 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
+        }
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
-        }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
-        }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
-    }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
-}
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
-        }
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: Can't we reuse something of [this logic ](https://github.com/lambdaclass/ethrex/blob/437a4eea001e763280cd25aae6bdf862a0038c92/crates/l2/sequencer/l1_committer.rs#L403) to build the `AccountStateDiffs`?
created_at: 2025-05-19T16:11:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +144,72 @@ impl LEVM {
 
         vm.execute().map_err(VMError::into)
     }
+
+    pub fn execute_tx_l2(
+        // The transaction to execute.
+        tx: &Transaction,
+        // The transactions recovered address
+        tx_sender: Address,
+        // The block header for the current block.
+        block_header: &BlockHeader,
+        db: &mut GeneralizedDatabase,
+    ) -> Result<(ExecutionReport, CallFrameBackup), EvmError> {
+        let chain_config = db.store.get_chain_config();
+        let gas_price: U256 = tx
+            .effective_gas_price(block_header.base_fee_per_gas)
+            .ok_or(VMError::TxValidation(
+                TxValidationError::InsufficientMaxFeePerGas,
+            ))?
+            .into();
+
+        let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
+        let env = Environment {
+            origin: tx_sender,
+            gas_limit: tx.gas_limit(),
+            config,
+            block_number: block_header.number.into(),
+            coinbase: block_header.coinbase,
+            timestamp: block_header.timestamp.into(),
+            prev_randao: Some(block_header.prev_randao),
+            chain_id: chain_config.chain_id.into(),
+            base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(),
+            gas_price,
+            block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from),
+            block_blob_gas_used: block_header.blob_gas_used.map(U256::from),
+            tx_blob_hashes: tx.blob_versioned_hashes(),
+            tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from),
+            tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from),
+            tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas().map(U256::from),
+            tx_nonce: tx.nonce(),
+            block_gas_limit: block_header.gas_limit,
+            difficulty: block_header.difficulty,
+            is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
+        };
+
+        let mut vm = VM::new(env, db, tx);
+
+        let report_result = vm.execute().map_err(EvmError::from)?;
path: crates/vm/backends/levm/mod.rs
user: login: avilagaston9
body: Does this part differ from the original `execute_tx()`? If not, it can simply be a wrapper.
created_at: 2025-05-19T16:20:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,6 +137,47 @@ pub fn get_valid_jump_destinations(code: &Bytes) -> Result<HashSet<usize>, VMErr
     Ok(valid_jump_destinations)
 }
 
+// ================== Backup related functions =======================
+
+/// Merge the a previous callframe backup into the current callframe backup.
path: crates/vm/levm/src/utils.rs
user: login: avilagaston9
body: ```suggestion
/// Merge a previous callframe backup into the current callframe backup.
```
created_at: 2025-05-19T16:23:52Z
author_association: CONTRIBUTOR
path: crates/blockchain/payload.rs
user: login: tomip01
body: Done [71c6547](https://github.com/lambdaclass/ethrex/pull/2825/commits/71c654761b4595910b40f132fe530eb557bd7b25)
created_at: 2025-05-19T17:02:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -304,6 +304,20 @@ impl BlockHeader {
         self.encode(&mut buf);
         keccak(buf)
     }
+
+    pub fn encode_for_state_diff(&self) -> Vec<u8> {
+        let mut encoded = Vec::new();
+        encoded.extend(self.transactions_root.0);
+        encoded.extend(self.receipts_root.0);
+        encoded.extend(self.parent_hash.0);
+        encoded.extend(self.gas_limit.to_be_bytes());
+        encoded.extend(self.gas_used.to_be_bytes());
+        encoded.extend(self.timestamp.to_be_bytes());
+        encoded.extend(self.number.to_be_bytes());
+        encoded.extend(self.base_fee_per_gas.unwrap_or(0).to_be_bytes());
+
+        encoded
+    }
path: crates/common/types/block.rs
user: login: tomip01
body: Nice, done here [dac8d95](https://github.com/lambdaclass/ethrex/pull/2825/commits/dac8d95ed6e457d2dd0ce219767bd6a38194b27e)
created_at: 2025-05-19T17:07:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,6 +144,72 @@ impl LEVM {
 
         vm.execute().map_err(VMError::into)
     }
+
+    pub fn execute_tx_l2(
+        // The transaction to execute.
+        tx: &Transaction,
+        // The transactions recovered address
+        tx_sender: Address,
+        // The block header for the current block.
+        block_header: &BlockHeader,
+        db: &mut GeneralizedDatabase,
+    ) -> Result<(ExecutionReport, CallFrameBackup), EvmError> {
+        let chain_config = db.store.get_chain_config();
+        let gas_price: U256 = tx
+            .effective_gas_price(block_header.base_fee_per_gas)
+            .ok_or(VMError::TxValidation(
+                TxValidationError::InsufficientMaxFeePerGas,
+            ))?
+            .into();
+
+        let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
+        let env = Environment {
+            origin: tx_sender,
+            gas_limit: tx.gas_limit(),
+            config,
+            block_number: block_header.number.into(),
+            coinbase: block_header.coinbase,
+            timestamp: block_header.timestamp.into(),
+            prev_randao: Some(block_header.prev_randao),
+            chain_id: chain_config.chain_id.into(),
+            base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(),
+            gas_price,
+            block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from),
+            block_blob_gas_used: block_header.blob_gas_used.map(U256::from),
+            tx_blob_hashes: tx.blob_versioned_hashes(),
+            tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from),
+            tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from),
+            tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas().map(U256::from),
+            tx_nonce: tx.nonce(),
+            block_gas_limit: block_header.gas_limit,
+            difficulty: block_header.difficulty,
+            is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
+        };
+
+        let mut vm = VM::new(env, db, tx);
+
+        let report_result = vm.execute().map_err(EvmError::from)?;
path: crates/vm/backends/levm/mod.rs
user: login: tomip01
body: Great, done here [6e89591](https://github.com/lambdaclass/ethrex/pull/2825/commits/6e895918f8b2854afad0e5144550c0fa54b7ac38)
created_at: 2025-05-19T17:28:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +176,58 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let mut tx_state_diff_size = 0;
+        let mut new_accounts_diff_size = 0;
+
+        for (address, diff) in merged_diffs.iter() {
+            let encoded = match diff.encode(address) {
+                Ok(encoded) => encoded,
+                Err(StateDiffError::EmptyAccountDiff) => {
+                    debug!("Skipping empty account diff for address: {address}");
+                    continue;
+                }
+                Err(e) => {
+                    error!("Failed to encode account state diff: {e}");
+                    return Err(BlockProducerError::FailedToEncodeAccountStateDiff(e));
+                }
+            };
+            new_accounts_diff_size += encoded.len();
+        }
+
+        if is_deposit_l2(&head_tx) {
+            tx_state_diff_size += deposits_log_len;
+        }
+        if is_withdrawal_l2(&head_tx.clone().into(), &receipt)? {
+            tx_state_diff_size += withdrawals_log_len;
+        }
+
+        if size_without_accounts + tx_state_diff_size + new_accounts_diff_size > SAFE_BYTES_PER_BLOB
+        {
+            debug!(
+                "No more StateDiff space to run this transactions. Skipping transaction: {:?}",
+                tx_hash
+            );
+            txs.pop();
+
+            // This transaction is too big, we need to restore the state
+            context.vm.restore_cache_state(transaction_backup)?;
+
+            continue;
+        }
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [54eac78](https://github.com/lambdaclass/ethrex/pull/2825/commits/54eac78433eb0bbce023acc4374acc578a65c86c)
created_at: 2025-05-19T18:13:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,41 +159,14 @@ pub async fn fill_transactions(
         // or we want it before the return?
         metrics!(METRICS_TX.inc_tx());
 
-        let previous_context = context.clone();
-
         // Execute tx
-        let receipt = match blockchain.apply_transaction(&head_tx, context) {
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: We can now make `blockchain.apply_transaction` private again
created_at: 2025-05-19T18:38:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +237,107 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
+        }
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
-        }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
-        }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
-    }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
-}
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
-        }
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: The function from the L1 committer receives a `Vec<AccountUpdate>`, but we have a `HashMap<Address, AccountStateDiff>`. Therefore, we either need to convert the `AccountStateDiff` into `AccountUpdate` or save the account updates during each iteration of the block producer.

The second option would require a function to convert `CallFrameBackup` into `[AccountUpdate]`, which would duplicate the logic already present in the `get_state_transitions` function of the EVM backends.

Another option could be to modify `prepare_state_diff` to accept a `HashMap<Address, AccountStateDiff>`. However, this would also result in duplicated logic for calculating the HashMap between the L1 committer and the block producer.

So I think in same place we will have to have some duplicated logic
created_at: 2025-05-19T19:01:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,41 +159,14 @@ pub async fn fill_transactions(
         // or we want it before the return?
         metrics!(METRICS_TX.inc_tx());
 
-        let previous_context = context.clone();
-
         // Execute tx
-        let receipt = match blockchain.apply_transaction(&head_tx, context) {
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Nice catch! done here [b2e17a4](https://github.com/lambdaclass/ethrex/pull/2825/commits/b2e17a47b78b384cf4412d4486ffe5632d88771a)
created_at: 2025-05-19T19:05:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,168 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    new_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in new_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            existing_diff.new_balance = diff.new_balance;
+            existing_diff.nonce_diff = diff.nonce_diff;
+            // we need to overwrite the storage with the new values
+            existing_diff.storage.extend(diff.storage);
+            existing_diff.bytecode = diff.bytecode;
+            existing_diff.bytecode_hash = diff.bytecode_hash;
+        } else {
+            merged_diffs.insert(address, diff);
         }
+    }
+    merged_diffs
+}
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: I don't understand this merging strategy.
I imagine a case in which `previous_diffs` has the diffs of the first transaction and `new_diffs` has the diffs of the second transaction compared to the state after the first transaction, is that right?

What happens if the nonce increased in the first transaction but in the second one it didn't?
Like, `nonce_diff` was 1 but then it gets overwritten with 0 because it didn't increase?
I understand everything was tested and works fine but the code looks weird. What I see is that `get_tx_diffs()` gets the diff of the transaction relative to the previous transaction, and then in `merge_diffs()` we overwrite the `nonce_diff` instead of adding to the previous it seems.

If I'm wrong (which is likely) I'd suggest adding rustdocs in some parts explaining things that can clarify this. Thanks :)

created_at: 2025-05-19T19:26:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +99,11 @@ impl<'a> VM<'a> {
             return Err(e);
         }
 
+        // Here we need to backup the callframe because in the L2 we want to revert a transaction if it exceeds blob size
path: crates/vm/levm/src/vm.rs
user: login: JereSalo
body: Maybe "revert the changes made by a transaction" or something else would be more accurate. Don't want the reader to think on actual `Revert` hehe. Whatever you consider appropriate just for making that differentiation.
Probably I'm exaggerating a bit, if that's the case feel free to keep it as is :)
created_at: 2025-05-19T19:50:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,6 +137,47 @@ pub fn get_valid_jump_destinations(code: &Bytes) -> Result<HashSet<usize>, VMErr
     Ok(valid_jump_destinations)
 }
 
+// ================== Backup related functions =======================
+
+/// Merge a previous callframe backup into the current callframe backup.
+/// This prevails the previous values over the current ones.
+pub fn merge_callframe_backup(current: &mut CallFrameBackup, previous: CallFrameBackup) {
path: crates/vm/levm/src/utils.rs
user: login: JereSalo
body: Function name is misleading because a similar one exists and the behavior could be replaced by 2 lines so maybe we can remove this
created_at: 2025-05-19T20:04:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +120,15 @@ impl<'a> VM<'a> {
         let mut report = self.run_execution()?;
 
         self.finalize_execution(&mut report)?;
+
+        // We want to restore to the initial state, this includes reverting the changes made by the prepare execution
+        // and the changes made by the execution itself.
+        #[cfg(feature = "l2")]
+        {
+            let current_backup: &mut CallFrameBackup =
+                &mut self.current_call_frame_mut()?.call_frame_backup;
+            merge_callframe_backup(current_backup, callframe_backup);
path: crates/vm/levm/src/vm.rs
user: login: JereSalo
body: ```suggestion
current.original_accounts_info.extend(previous.original_accounts_info);
current.original_account_storage_slots.extend(previous.original_account_storage_slots);
```
created_at: 2025-05-19T20:04:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,6 +137,47 @@ pub fn get_valid_jump_destinations(code: &Bytes) -> Result<HashSet<usize>, VMErr
     Ok(valid_jump_destinations)
 }
 
+// ================== Backup related functions =======================
+
+/// Merge a previous callframe backup into the current callframe backup.
+/// This prevails the previous values over the current ones.
+pub fn merge_callframe_backup(current: &mut CallFrameBackup, previous: CallFrameBackup) {
path: crates/vm/levm/src/utils.rs
user: login: iovoid
body: There is significant overlap with VM.merge_call_frame_backup_with_parent. Maybe it could make use of this function?

In that case it might make sense to have it as a a method of CallFrameBackup.
created_at: 2025-05-19T20:48:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +99,11 @@ impl<'a> VM<'a> {
             return Err(e);
         }
 
+        // Here we need to backup the callframe because in the L2 we want to revert a transaction if it exceeds blob size
path: crates/vm/levm/src/vm.rs
user: login: tomip01
body: I agree! What do you think of `undo` a transaction? changed here [3859f49](https://github.com/lambdaclass/ethrex/pull/2825/commits/3859f49408f84065a2362e078b635801d71df8e9) but free to new suggestions

created_at: 2025-05-19T20:58:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +120,15 @@ impl<'a> VM<'a> {
         let mut report = self.run_execution()?;
 
         self.finalize_execution(&mut report)?;
+
+        // We want to restore to the initial state, this includes reverting the changes made by the prepare execution
+        // and the changes made by the execution itself.
+        #[cfg(feature = "l2")]
+        {
+            let current_backup: &mut CallFrameBackup =
+                &mut self.current_call_frame_mut()?.call_frame_backup;
+            merge_callframe_backup(current_backup, callframe_backup);
path: crates/vm/levm/src/vm.rs
user: login: tomip01
body: That's better! donde here [ad206b1](https://github.com/lambdaclass/ethrex/pull/2825/commits/ad206b103fbc59dc296324619be0e83b9806e8c7)
created_at: 2025-05-19T21:07:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,6 +137,47 @@ pub fn get_valid_jump_destinations(code: &Bytes) -> Result<HashSet<usize>, VMErr
     Ok(valid_jump_destinations)
 }
 
+// ================== Backup related functions =======================
+
+/// Merge a previous callframe backup into the current callframe backup.
+/// This prevails the previous values over the current ones.
+pub fn merge_callframe_backup(current: &mut CallFrameBackup, previous: CallFrameBackup) {
path: crates/vm/levm/src/utils.rs
user: login: tomip01
body: After the suggestion of @JereSalo in the next comment, I removed the function and now we use `.extend()` method from the `HashMap`. I think it's clearer
Done here [ad206b1](https://github.com/lambdaclass/ethrex/pull/2825/commits/ad206b103fbc59dc296324619be0e83b9806e8c7)

Wha do you think?
created_at: 2025-05-19T21:08:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,168 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    new_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in new_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            existing_diff.new_balance = diff.new_balance;
+            existing_diff.nonce_diff = diff.nonce_diff;
+            // we need to overwrite the storage with the new values
+            existing_diff.storage.extend(diff.storage);
+            existing_diff.bytecode = diff.bytecode;
+            existing_diff.bytecode_hash = diff.bytecode_hash;
+        } else {
+            merged_diffs.insert(address, diff);
         }
+    }
+    merged_diffs
+}
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: You were right! I was overwritting the diffs without looking if there was an actual update or not. This causes that if the diff in the last transaction was None, it would be for the entire block

I changed that here [969b7f](https://github.com/lambdaclass/ethrex/pull/2825/commits/2969b7ff8c7b3cc8838390eb8edd7b615f09f433)
created_at: 2025-05-20T12:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,168 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    new_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in new_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            existing_diff.new_balance = diff.new_balance;
+            existing_diff.nonce_diff = diff.nonce_diff;
+            // we need to overwrite the storage with the new values
+            existing_diff.storage.extend(diff.storage);
+            existing_diff.bytecode = diff.bytecode;
+            existing_diff.bytecode_hash = diff.bytecode_hash;
+        } else {
+            merged_diffs.insert(address, diff);
         }
+    }
+    merged_diffs
+}
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Added some comments here [19e12b6](https://github.com/lambdaclass/ethrex/pull/2825/commits/19e12b6b35c787d52e62544140d3884d7c37bcab)
created_at: 2025-05-20T12:33:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,168 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
-    }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    new_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in new_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            existing_diff.new_balance = diff.new_balance;
+            existing_diff.nonce_diff = diff.nonce_diff;
+            // we need to overwrite the storage with the new values
+            existing_diff.storage.extend(diff.storage);
+            existing_diff.bytecode = diff.bytecode;
+            existing_diff.bytecode_hash = diff.bytecode_hash;
+        } else {
+            merged_diffs.insert(address, diff);
         }
+    }
+    merged_diffs
+}
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Looks better now!
created_at: 2025-05-20T13:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,7 +105,9 @@ pub async fn fill_transactions(
         };
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Maybe out of scope: Should we delete this gas check here? I mean, it can be directly replaced by the check that's below that checks against `head_tx.tx.gas_limit()`, right?
created_at: 2025-05-20T13:06:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Should we make an appropriate error for this?
created_at: 2025-05-20T13:11:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Just in case, you could access the bytecode hash with `new_account.info.code_hash` if you want to
created_at: 2025-05-20T13:14:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: I'd rename this to `new_account` just because the account info is another thing. That'd be `new_account.info`
created_at: 2025-05-20T13:14:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: ditto
created_at: 2025-05-20T13:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: ditto
created_at: 2025-05-20T13:15:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: I don't know why in `prepare_state_diff` you assign None to it also. Is it meant to be used? Maybe leave an issue?
created_at: 2025-05-20T13:17:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: I'd add rustdocs to this function just in case
created_at: 2025-05-20T13:18:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
-        }
+/// Combines the diffs from the current transaction with the existing block diffs.
+/// Transaction diffs represent state changes from the latest transaction execution,
+/// while previous diffs accumulate all changes included in the block so far.
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    tx_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in tx_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            // New balance could be None if a transaction didn't change the balance
+            // but we want to keep the previous changes made in a transaction included in the block
+            existing_diff.new_balance = diff.new_balance.or(existing_diff.new_balance);
 
-        let nonce_diff = get_nonce_diff(
-            &account_update,
-            &context.store,
-            Some(accounts_info_cache),
-            context.block_number(),
-        )
-        .await
-        .map_err(|e| {
-            BlockProducerError::Custom(format!("Block Producer failed to get nonce diff: {e}"))
-        })?;
-        if nonce_diff != 0 {
-            modified_accounts_size += 2; // 2bytes | nonce_diff(u16)
-        }
-        // for each added_storage: 32bytes + 32bytes | key(H256) + value(U256)
-        modified_accounts_size += account_update.added_storage.len() * 2 * 32;
+            // We add the nonce diff to the existing one to keep track of the total nonce diff
+            existing_diff.nonce_diff += diff.nonce_diff;
+
+            // we need to overwrite only the new storage storage slot with the new values
+            existing_diff.storage.extend(diff.storage);
 
-        if let Some(bytecode) = &account_update.code {
-            modified_accounts_size += 2; // 2bytes | bytecode_len(u16)
-            modified_accounts_size += bytecode.len(); // (len)bytes | bytecode(Bytes)
+            // Take the bytecode from the tx diff if present, avoiding clone if not needed
+            if diff.bytecode.is_some() {
+                existing_diff.bytecode = diff.bytecode;
+            }
+
+            // Take the new bytecode hash if it is present
+            existing_diff.bytecode_hash = diff.bytecode_hash.or(existing_diff.bytecode_hash);
+        } else {
+            merged_diffs.insert(address, diff);
         }
     }
-    Ok(modified_accounts_size)
+    merged_diffs
+}
+
+fn calculate_tx_diff_size(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Self explanatory but it wouldn't be bad to add rustdocs here :)
created_at: 2025-05-20T13:22:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [f69ae7a](https://github.com/lambdaclass/ethrex/pull/2825/commits/f69ae7a879b9ad272fc526ddcf55220c7055df2f)
created_at: 2025-05-20T13:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Changed that here [f6205f0](https://github.com/lambdaclass/ethrex/pull/2825/commits/f6205f033c54f18f55ec17f78da5237faa5d03d3)
created_at: 2025-05-20T13:58:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Right! done [d90281e](https://github.com/lambdaclass/ethrex/pull/2825/commits/d90281e193f327fd38a2a0b41a447a7cc109e382)
created_at: 2025-05-20T14:01:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [f69ae7a](https://github.com/lambdaclass/ethrex/pull/2825/commits/f69ae7a879b9ad272fc526ddcf55220c7055df2f)
created_at: 2025-05-20T14:02:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [f69ae7a](https://github.com/lambdaclass/ethrex/pull/2825/commits/f69ae7a879b9ad272fc526ddcf55220c7055df2f)
created_at: 2025-05-20T14:02:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,10 +75,15 @@ pub async fn fill_transactions(
     context: &mut PayloadBuildContext,
     store: &Store,
 ) -> Result<(), BlockProducerError> {
-    // Two bytes for the len
-    let (mut acc_withdrawals_size, mut acc_deposits_size): (usize, usize) = (2, 2);
-    let mut acc_state_diff_size = 0;
-    let mut accounts_info_cache = HashMap::new();
+    // Maybe we can cache this to not calculate it every time
+    let header_encoded_len = encode_block_header(&context.payload.header).len();
+    let withdrawals_log_len = WithdrawalLog::default().encode().len();
+    let deposits_log_len = DepositLog::default().encode().len();
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: What about using [lazy_static](https://docs.rs/lazy_static/latest/lazy_static/) here?
created_at: 2025-05-20T14:12:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
+    call_frame_backup: &CallFrameBackup,
+    context: &PayloadBuildContext,
+) -> Result<HashMap<Address, AccountStateDiff>, BlockProducerError> {
+    let mut modified_accounts = HashMap::new();
+    match &context.vm {
+        Evm::REVM { .. } => {
+            return Err(BlockProducerError::EvmError(EvmError::InvalidEVM(
+                "REVM not supported for L2".to_string(),
+            )))
         }
-        if is_deposit_l2(&tx) {
-            *acc_deposits_size -= L2_DEPOSIT_SIZE;
+        Evm::LEVM { db } => {
+            // First we add the account info
+            for (address, original_account) in call_frame_backup.original_accounts_info.iter() {
+                let new_account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let nonce_diff: u16 = (new_account_info.info.nonce - original_account.info.nonce)
+                    .try_into()
+                    .map_err(BlockProducerError::TryIntoError)?;
+
+                let new_balance = if new_account_info.info.balance != original_account.info.balance
+                {
+                    Some(new_account_info.info.balance)
+                } else {
+                    None
+                };
+
+                let bytecode = if new_account_info.code != original_account.code {
+                    Some(new_account_info.code.clone())
+                } else {
+                    None
+                };
+
+                let account_state_diff = AccountStateDiff {
+                    new_balance,
+                    nonce_diff,
+                    storage: HashMap::new(), // We add the storage later
+                    bytecode,
+                    bytecode_hash: None,
+                };
+
+                modified_accounts.insert(*address, account_state_diff);
+            }
+
+            // Then if there is any storage change, we add it to the account state diff
+            for (address, original_storage_slots) in
+                call_frame_backup.original_account_storage_slots.iter()
+            {
+                let account_info = db
+                    .cache
+                    .get(address)
+                    .ok_or(BlockProducerError::StorageDataIsNone)?;
+
+                let mut added_storage = HashMap::new();
+                for key in original_storage_slots.keys() {
+                    added_storage.insert(
+                        *key,
+                        *account_info
+                            .storage
+                            .get(key)
+                            .ok_or(BlockProducerError::StorageDataIsNone)?,
+                    );
+                }
+                if let Some(account_state_diff) = modified_accounts.get_mut(address) {
+                    account_state_diff.storage = added_storage;
+                } else {
+                    // If the account is not in the modified accounts, we create a new one
+                    let account_state_diff = AccountStateDiff {
+                        new_balance: None,
+                        nonce_diff: 0,
+                        storage: added_storage,
+                        bytecode: None,
+                        bytecode_hash: None,
+                    };
+                    modified_accounts.insert(*address, account_state_diff);
+                }
+            }
         }
-        debug!(
-            "Blob size limit exceeded. current_state_diff_size: {}",
-            current_state_diff_size
-        );
-        return Ok(false);
     }
-    *acc_state_diff_size = current_state_diff_size;
-    Ok(true)
+    Ok(modified_accounts)
 }
 
-async fn calc_modified_accounts_size(
-    context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<usize, BlockProducerError> {
-    let mut modified_accounts_size: usize = 2; // 2bytes | modified_accounts_len(u16)
-
-    // We use a temporary_context because `get_state_transitions` mutates it.
-    let mut temporary_context = context.clone();
-
-    let account_updates = temporary_context.vm.get_state_transitions()?;
-    for account_update in account_updates {
-        modified_accounts_size += 1 + 20; // 1byte + 20bytes | r#type(u8) + address(H160)
-        if account_update.info.is_some() {
-            modified_accounts_size += 32; // 32bytes | new_balance(U256)
-        }
+/// Combines the diffs from the current transaction with the existing block diffs.
+/// Transaction diffs represent state changes from the latest transaction execution,
+/// while previous diffs accumulate all changes included in the block so far.
+fn merge_diffs(
+    previous_diffs: &HashMap<Address, AccountStateDiff>,
+    tx_diffs: HashMap<Address, AccountStateDiff>,
+) -> HashMap<Address, AccountStateDiff> {
+    let mut merged_diffs = previous_diffs.clone();
+    for (address, diff) in tx_diffs {
+        if let Some(existing_diff) = merged_diffs.get_mut(&address) {
+            // New balance could be None if a transaction didn't change the balance
+            // but we want to keep the previous changes made in a transaction included in the block
+            existing_diff.new_balance = diff.new_balance.or(existing_diff.new_balance);
 
-        let nonce_diff = get_nonce_diff(
-            &account_update,
-            &context.store,
-            Some(accounts_info_cache),
-            context.block_number(),
-        )
-        .await
-        .map_err(|e| {
-            BlockProducerError::Custom(format!("Block Producer failed to get nonce diff: {e}"))
-        })?;
-        if nonce_diff != 0 {
-            modified_accounts_size += 2; // 2bytes | nonce_diff(u16)
-        }
-        // for each added_storage: 32bytes + 32bytes | key(H256) + value(U256)
-        modified_accounts_size += account_update.added_storage.len() * 2 * 32;
+            // We add the nonce diff to the existing one to keep track of the total nonce diff
+            existing_diff.nonce_diff += diff.nonce_diff;
+
+            // we need to overwrite only the new storage storage slot with the new values
+            existing_diff.storage.extend(diff.storage);
 
-        if let Some(bytecode) = &account_update.code {
-            modified_accounts_size += 2; // 2bytes | bytecode_len(u16)
-            modified_accounts_size += bytecode.len(); // (len)bytes | bytecode(Bytes)
+            // Take the bytecode from the tx diff if present, avoiding clone if not needed
+            if diff.bytecode.is_some() {
+                existing_diff.bytecode = diff.bytecode;
+            }
+
+            // Take the new bytecode hash if it is present
+            existing_diff.bytecode_hash = diff.bytecode_hash.or(existing_diff.bytecode_hash);
+        } else {
+            merged_diffs.insert(address, diff);
         }
     }
-    Ok(modified_accounts_size)
+    merged_diffs
+}
+
+fn calculate_tx_diff_size(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [ee402d4](https://github.com/lambdaclass/ethrex/pull/2825/commits/ee402d4201c157058e8034164ca9c5be365e5868)
created_at: 2025-05-20T14:13:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -206,97 +223,182 @@ pub async fn fill_transactions(
     Ok(())
 }
 
-/// Calculates the size of the current `StateDiff` of the block.
-/// If the current size exceeds the blob size limit, returns `Ok(false)`.
-/// If there is still space in the blob, returns `Ok(true)`.
-/// Updates the following mutable variables in the process:
-/// - `acc_withdrawals_size`: Accumulated size of withdrawals (incremented by L2_WITHDRAWAL_SIZE if tx is withdrawal)
-/// - `acc_deposits_size`: Accumulated size of deposits (incremented by L2_DEPOSIT_SIZE if tx is deposit)
-/// - `acc_state_diff_size`: Set to current total state diff size if within limit
-/// - `context`: Must be mutable because `get_state_transitions` requires mutable access
-/// - `accounts_info_cache`: When calculating account updates, we store account info in the cache if it's not already present
-///
-///  StateDiff:
-/// +-------------------+
-/// | Version           |
-/// | HeaderFields      |
-/// | AccountsStateDiff |
-/// | Withdrawals       |
-/// | Deposits          |
-/// +-------------------+
-async fn update_state_diff_size(
-    acc_withdrawals_size: &mut usize,
-    acc_deposits_size: &mut usize,
-    acc_state_diff_size: &mut usize,
-    tx: Transaction,
-    receipt: &Receipt,
+fn apply_transaction_l2(
+    head: &HeadTransaction,
     context: &mut PayloadBuildContext,
-    accounts_info_cache: &mut HashMap<Address, Option<AccountInfo>>,
-) -> Result<bool, BlockProducerError> {
-    if is_withdrawal_l2(&tx, receipt)? {
-        *acc_withdrawals_size += L2_WITHDRAWAL_SIZE;
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    match **head {
+        Transaction::EIP4844Transaction(_) => Err(ChainError::InvalidTransaction(
+            "Blob transactions not supported in the L2".to_string(),
+        )),
+        _ => apply_plain_transaction_l2(head, context),
     }
-    if is_deposit_l2(&tx) {
-        *acc_deposits_size += L2_DEPOSIT_SIZE;
-    }
-    let modified_accounts_size = calc_modified_accounts_size(context, accounts_info_cache).await?;
+}
 
-    let current_state_diff_size = 1 /* version (u8) */ + LAST_HEADER_FIELDS_SIZE + *acc_withdrawals_size + *acc_deposits_size + modified_accounts_size;
+fn apply_plain_transaction_l2(
+    head: &HeadTransaction,
+    context: &mut PayloadBuildContext,
+) -> Result<(Receipt, CallFrameBackup), ChainError> {
+    let (report, gas_used, transaction_backup) = context.vm.execute_tx_l2(
+        &head.tx,
+        &context.payload.header,
+        &mut context.remaining_gas,
+        head.tx.sender(),
+    )?;
+    context.block_value += U256::from(gas_used) * head.tip;
+    Ok((report, transaction_backup))
+}
 
-    if current_state_diff_size > SAFE_BYTES_PER_BLOB {
-        // Restore the withdrawals and deposits counters.
-        if is_withdrawal_l2(&tx, receipt)? {
-            *acc_withdrawals_size -= L2_WITHDRAWAL_SIZE;
+fn get_tx_diffs(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [ee402d4](https://github.com/lambdaclass/ethrex/pull/2825/commits/ee402d4201c157058e8034164ca9c5be365e5868)
created_at: 2025-05-20T14:13:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: ```suggestion
        let account_diffs_in_tx = get_account_diffs_in_tx(&transaction_backup, context)?;
```
created_at: 2025-05-20T14:16:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,7 +105,9 @@ pub async fn fill_transactions(
         };
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Maybe yes, but the two checks are not exactly equivalent. The first one checks if the smallest transaction can fit with the remaining gas. The second one checks if that specific transaction, which could have a gas limit higher than the minimum (21000 gas), can fit in the remaining gas.

I think this approach was chosen for performance reasons. But let's wait for someone who has a clearer understanding of this.
created_at: 2025-05-20T14:20:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let (tx_state_diff_size, new_accounts_diff_size) = calculate_tx_diff_size(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: ```suggestion
        let (tx_size_without_accounts, new_accounts_diff_size) = calculate_tx_diff_size(
```
created_at: 2025-05-20T14:24:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let (tx_state_diff_size, new_accounts_diff_size) = calculate_tx_diff_size(
+            &merged_diffs,
+            &head_tx,
+            &receipt,
+            deposits_log_len,
+            withdrawals_log_len,
+        )?;
+
+        if size_without_accounts + tx_state_diff_size + new_accounts_diff_size > SAFE_BYTES_PER_BLOB
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: avilagaston9
body: ```suggestion
        if acc_size_without_accounts + tx_state_diff_size + new_accounts_diff_size > SAFE_BYTES_PER_BLOB
```
created_at: 2025-05-20T14:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [26ea607](https://github.com/lambdaclass/ethrex/pull/2825/commits/26ea607ea075b0d46f37b08732a43c5a0e902c89)
created_at: 2025-05-20T14:45:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let (tx_state_diff_size, new_accounts_diff_size) = calculate_tx_diff_size(
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [26ea607](https://github.com/lambdaclass/ethrex/pull/2825/commits/26ea607ea075b0d46f37b08732a43c5a0e902c89)
created_at: 2025-05-20T14:45:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +179,41 @@ pub async fn fill_transactions(
                 continue;
             }
         };
+
+        let account_diffs_in_tx = get_tx_diffs(&transaction_backup, context)?;
+        let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx);
+
+        let (tx_state_diff_size, new_accounts_diff_size) = calculate_tx_diff_size(
+            &merged_diffs,
+            &head_tx,
+            &receipt,
+            deposits_log_len,
+            withdrawals_log_len,
+        )?;
+
+        if size_without_accounts + tx_state_diff_size + new_accounts_diff_size > SAFE_BYTES_PER_BLOB
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Done [26ea607](https://github.com/lambdaclass/ethrex/pull/2825/commits/26ea607ea075b0d46f37b08732a43c5a0e902c89)
created_at: 2025-05-20T14:45:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,10 +75,15 @@ pub async fn fill_transactions(
     context: &mut PayloadBuildContext,
     store: &Store,
 ) -> Result<(), BlockProducerError> {
-    // Two bytes for the len
-    let (mut acc_withdrawals_size, mut acc_deposits_size): (usize, usize) = (2, 2);
-    let mut acc_state_diff_size = 0;
-    let mut accounts_info_cache = HashMap::new();
+    // Maybe we can cache this to not calculate it every time
+    let header_encoded_len = encode_block_header(&context.payload.header).len();
+    let withdrawals_log_len = WithdrawalLog::default().encode().len();
+    let deposits_log_len = DepositLog::default().encode().len();
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: tomip01
body: Nice! done here [1690871](https://github.com/lambdaclass/ethrex/pull/2825/commits/1690871fe53447897684025083100b7003ea51a7)
created_at: 2025-05-20T15:35:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,7 +105,9 @@ pub async fn fill_transactions(
         };
path: crates/l2/sequencer/block_producer/payload_builder.rs
user: login: JereSalo
body: Ok!
created_at: 2025-05-21T15:48:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-16T14:55:26Z
body: <h2>Lines of code report</h2>

Total lines added: `238`
Total lines removed: `15`
Total lines changed: `253`

<details>
<summary>Detailed view</summary>

```
+--------------------------------------------------------------+-------+------+
| File                                                         | Lines | Diff |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/error.rs                            | 101   | +2   |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/block_producer/payload_builder.rs | 319   | +94  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/errors.rs                         | 215   | +4   |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/state_diff.rs                     | 496   | +27  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/engine/payload.rs               | 723   | +4   |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs                        | 665   | +34  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/mod.rs                             | 271   | +37  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/call_frame.rs                      | 211   | -15  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/utils.rs                           | 510   | +21  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/vm.rs                              | 179   | +15  |
+--------------------------------------------------------------+-------+------+

```
</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T15:05:54Z
body: # Benchmark Results Comparison
## PR Results
#### Benchmark Results: Factorial
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Factorial` | 241.2 ± 1.0 | 239.9 | 242.8 | 1.00 |
| `levm_Factorial` | 818.2 ± 3.9 | 812.7 | 826.7 | 3.39 ± 0.02 |
#### Benchmark Results: Factorial - Recursive
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_FactorialRecursive` | 1.525 ± 0.081 | 1.440 | 1.673 | 1.00 |
| `levm_FactorialRecursive` | 13.004 ± 0.140 | 12.888 | 13.284 | 8.53 ± 0.46 |
#### Benchmark Results: Fibonacci
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Fibonacci` | 209.1 ± 0.7 | 207.5 | 209.9 | 1.00 |
| `levm_Fibonacci` | 827.3 ± 47.6 | 798.7 | 961.1 | 3.96 ± 0.23 |
#### Benchmark Results: ManyHashes
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ManyHashes` | 8.9 ± 0.1 | 8.8 | 9.1 | 1.00 |
| `levm_ManyHashes` | 17.5 ± 0.2 | 17.3 | 18.0 | 1.97 ± 0.03 |
#### Benchmark Results: BubbleSort
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_BubbleSort` | 3.261 ± 0.025 | 3.240 | 3.325 | 1.00 |
| `levm_BubbleSort` | 5.560 ± 0.034 | 5.501 | 5.619 | 1.70 ± 0.02 |
#### Benchmark Results: ERC20 - Transfer
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Transfer` | 251.6 ± 1.6 | 250.0 | 254.4 | 1.00 |
| `levm_ERC20Transfer` | 505.4 ± 12.0 | 497.8 | 535.2 | 2.01 ± 0.05 |
#### Benchmark Results: ERC20 - Mint
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Mint` | 142.4 ± 0.6 | 141.8 | 143.5 | 1.00 |
| `levm_ERC20Mint` | 319.9 ± 9.7 | 316.1 | 347.6 | 2.25 ± 0.07 |
#### Benchmark Results: ERC20 - Approval
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Approval` | 1.044 ± 0.009 | 1.034 | 1.062 | 1.00 |
| `levm_ERC20Approval` | 1.899 ± 0.009 | 1.887 | 1.917 | 1.82 ± 0.02 |
## Main Results
#### Benchmark Results: Factorial
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Factorial` | 249.7 ± 27.2 | 240.0 | 327.1 | 1.00 |
| `levm_Factorial` | 829.5 ± 20.8 | 813.6 | 879.3 | 3.32 ± 0.37 |
#### Benchmark Results: Factorial - Recursive
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_FactorialRecursive` | 1.517 ± 0.092 | 1.407 | 1.661 | 1.00 |
| `levm_FactorialRecursive` | 13.096 ± 0.107 | 13.001 | 13.354 | 8.63 ± 0.53 |
#### Benchmark Results: Fibonacci
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_Fibonacci` | 213.5 ± 1.5 | 211.7 | 217.0 | 1.00 |
| `levm_Fibonacci` | 823.7 ± 9.1 | 813.7 | 836.1 | 3.86 ± 0.05 |
#### Benchmark Results: ManyHashes
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ManyHashes` | 8.8 ± 0.0 | 8.7 | 8.8 | 1.00 |
| `levm_ManyHashes` | 17.3 ± 0.2 | 17.0 | 17.8 | 1.97 ± 0.03 |
#### Benchmark Results: BubbleSort
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_BubbleSort` | 3.253 ± 0.022 | 3.217 | 3.287 | 1.00 |
| `levm_BubbleSort` | 5.519 ± 0.139 | 5.440 | 5.903 | 1.70 ± 0.04 |
#### Benchmark Results: ERC20 - Transfer
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Transfer` | 252.3 ± 1.2 | 251.2 | 254.6 | 1.00 |
| `levm_ERC20Transfer` | 497.9 ± 4.1 | 492.6 | 504.4 | 1.97 ± 0.02 |
#### Benchmark Results: ERC20 - Mint
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Mint` | 143.7 ± 0.8 | 142.6 | 145.0 | 1.00 |
| `levm_ERC20Mint` | 318.5 ± 1.4 | 316.5 | 320.5 | 2.22 ± 0.02 |
#### Benchmark Results: ERC20 - Approval
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `revm_ERC20Approval` | 1.047 ± 0.004 | 1.042 | 1.056 | 1.00 |
| `levm_ERC20Approval` | 1.882 ± 0.019 | 1.864 | 1.921 | 1.80 ± 0.02 |

author_association: NONE
user: login: tomip01
created_at: 2025-05-20T16:03:53Z
body: > I'd clarify somewhere that this calculation of state diff size is not 100% accurate because it isn't contemplating that for example: Storage of an account can go from 1 to 2 to 1 again and it won't have changed but we say that it did because we are not comparing against the original value. I'd just leave that written down somewhere in a comment or `.md`, even create an issue if you want to.
> 
> I left some other suggestions but they are not critical.
> 
> Great PR! 🎉

Created one here https://github.com/lambdaclass/ethrex/issues/2850
author_association: CONTRIBUTOR