diff --git a/.github/workflows/main_prover.yaml b/.github/workflows/main_prover.yaml index 8ec581de538..5aeeca1908d 100644 --- a/.github/workflows/main_prover.yaml +++ b/.github/workflows/main_prover.yaml @@ -89,6 +89,7 @@ jobs: ETHREX_BLOCK_PRODUCER_BASE_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_VAULT_ADDRESS=0xd5d2a85751b6F158e5b9B8cD509206A865672362 \ ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS=1000000000 \ + ETHREX_BLOCK_PRODUCER_L1_FEE_VAULT_ADDRESS=0x45681AE1768a8936FB87aB11453B4755e322ceec \ docker compose up --build --detach --no-deps ethrex_l2 - name: Copy env to host diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index e6456137c9e..45b83653a7b 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -284,6 +284,7 @@ jobs: export ETHREX_BLOCK_PRODUCER_BASE_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 export ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_VAULT_ADDRESS=0xd5d2a85751b6F158e5b9B8cD509206A865672362 export ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS=1000000000 + export ETHREX_BLOCK_PRODUCER_L1_FEE_VAULT_ADDRESS=0x45681AE1768a8936FB87aB11453B4755e322ceec fi cd crates/l2 DOCKER_ETHREX_WORKDIR=/usr/local/bin \ @@ -393,6 +394,7 @@ jobs: ETHREX_BLOCK_PRODUCER_BASE_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_VAULT_ADDRESS=0xd5d2a85751b6F158e5b9B8cD509206A865672362 \ ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS=1000000000 \ + ETHREX_BLOCK_PRODUCER_L1_FEE_VAULT_ADDRESS=0x45681AE1768a8936FB87aB11453B4755e322ceec \ ETHREX_PROOF_COORDINATOR_ADDRESS=0.0.0.0 \ docker compose -f docker-compose.yaml -f docker-compose-l2-tdx.yaml up --detach --no-deps ethrex_l2 diff --git a/cmd/ethrex/cli.rs b/cmd/ethrex/cli.rs index 65710e31b73..c1f597ef81b 100644 --- a/cmd/ethrex/cli.rs +++ b/cmd/ethrex/cli.rs @@ -6,8 +6,8 @@ use std::{ }; use clap::{ArgAction, Parser as ClapParser, Subcommand as ClapSubcommand}; -use ethrex_blockchain::{BlockchainOptions, BlockchainType, error::ChainError}; -use ethrex_common::types::{Block, DEFAULT_BUILDER_GAS_CEIL, Genesis, fee_config::FeeConfig}; +use ethrex_blockchain::{BlockchainOptions, BlockchainType, L2Config, error::ChainError}; +use ethrex_common::types::{Block, DEFAULT_BUILDER_GAS_CEIL, Genesis}; use ethrex_p2p::{ discv4::peer_table::TARGET_PEERS, sync::SyncMode, tx_broadcaster::BROADCAST_INTERVAL_MS, types::Node, @@ -402,7 +402,7 @@ impl Subcommand { let network = get_network(opts); let genesis = network.get_genesis()?; let blockchain_type = if l2 { - BlockchainType::L2(FeeConfig::default()) + BlockchainType::L2(L2Config::default()) } else { BlockchainType::L1 }; diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index c28e2e1a4ea..2682c249a97 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -7,8 +7,8 @@ use crate::l2::{L2Options, SequencerOptions}; use crate::utils::{ NodeConfigFile, get_client_version, init_datadir, read_jwtsecret_file, store_node_config_file, }; -use ethrex_blockchain::{Blockchain, BlockchainType}; -use ethrex_common::types::fee_config::{FeeConfig, OperatorFeeConfig}; +use ethrex_blockchain::{Blockchain, BlockchainType, L2Config}; +use ethrex_common::types::fee_config::{FeeConfig, L1FeeConfig, OperatorFeeConfig}; use ethrex_common::{Address, types::DEFAULT_BUILDER_GAS_CEIL}; use ethrex_l2::SequencerConfig; use ethrex_p2p::{ @@ -157,6 +157,7 @@ pub async fn init_l2( let rollup_store = init_rollup_store(&rollup_store_dir).await; let operator_fee_config = get_operator_fee_config(&opts.sequencer_opts).await?; + let l1_fee_config = get_l1_fee_config(&opts.sequencer_opts); let fee_config = FeeConfig { base_fee_vault: opts @@ -164,11 +165,18 @@ pub async fn init_l2( .block_producer_opts .base_fee_vault_address, operator_fee_config, + l1_fee_config, + }; + + // We wrap fee_config in an Arc to let the watcher + // update the L1 fee periodically. + let l2_config = L2Config { + fee_config: Arc::new(tokio::sync::RwLock::new(fee_config)), }; let blockchain_opts = ethrex_blockchain::BlockchainOptions { max_mempool_size: opts.node_opts.mempool_max_size, - r#type: BlockchainType::L2(fee_config), + r#type: BlockchainType::L2(l2_config), perf_logs_enabled: true, }; @@ -296,11 +304,26 @@ pub async fn init_l2( Ok(()) } +pub fn get_l1_fee_config(sequencer_opts: &SequencerOptions) -> Option { + if sequencer_opts.based { + // If based is enabled, skip L1 fee configuration + return None; + } + + sequencer_opts + .block_producer_opts + .l1_fee_vault_address + .map(|addr| L1FeeConfig { + l1_fee_vault: addr, + l1_fee_per_blob_gas: 0, // This is set by the L1 watcher + }) +} + pub async fn get_operator_fee_config( sequencer_opts: &SequencerOptions, ) -> eyre::Result> { if sequencer_opts.based { - // If based is enabled, operator fee is not applicable + // If based is enabled, skip operator fee configuration return Ok(None); } diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 83b79b07dc6..6fbec5af46d 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -198,6 +198,7 @@ impl TryFrom for SequencerConfig { check_interval_ms: opts.watcher_opts.watch_interval_ms, max_block_step: opts.watcher_opts.max_block_step.into(), watcher_block_delay: opts.watcher_opts.watcher_block_delay, + l1_blob_base_fee_update_interval: opts.watcher_opts.l1_fee_update_interval_ms, }, proof_coordinator: ProofCoordinatorConfig { listen_ip: opts.proof_coordinator_opts.listen_ip, @@ -399,6 +400,14 @@ pub struct WatcherOptions { help_heading = "L1 Watcher options" )] pub watcher_block_delay: u64, + #[arg( + long = "watcher.l1-fee-update-interval-ms", + value_name = "ADDRESS", + default_value = "60000", + env = "ETHREX_WATCHER_L1_FEE_UPDATE_INTERVAL_MS", + help_heading = "Block producer options" + )] + pub l1_fee_update_interval_ms: u64, } impl Default for WatcherOptions { @@ -408,6 +417,7 @@ impl Default for WatcherOptions { watch_interval_ms: 1000, max_block_step: 5000, watcher_block_delay: 0, + l1_fee_update_interval_ms: 60000, } } } @@ -453,7 +463,7 @@ pub struct BlockProducerOptions { )] pub operator_fee_vault_address: Option
, #[arg( - long, + long = "block-producer.operator-fee-per-gas", value_name = "UINT64", env = "ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS", requires = "operator_fee_vault_address", @@ -461,6 +471,13 @@ pub struct BlockProducerOptions { help = "Fee that the operator will receive for each unit of gas consumed in a block." )] pub operator_fee_per_gas: Option, + #[arg( + long = "block-producer.l1-fee-vault-address", + value_name = "ADDRESS", + env = "ETHREX_BLOCK_PRODUCER_L1_FEE_VAULT_ADDRESS", + help_heading = "Block producer options" + )] + pub l1_fee_vault_address: Option
, #[arg( long, default_value = "2", @@ -492,6 +509,7 @@ impl Default for BlockProducerOptions { base_fee_vault_address: None, operator_fee_vault_address: None, operator_fee_per_gas: None, + l1_fee_vault_address: None, elasticity_multiplier: 2, block_gas_limit: DEFAULT_BUILDER_GAS_CEIL, } diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index d9550c3e663..383a9b2f891 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -37,7 +37,7 @@ use std::collections::{BTreeMap, HashMap}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Instant; -use tokio::sync::Mutex as TokioMutex; +use tokio::sync::{Mutex as TokioMutex, RwLock}; use tokio_util::sync::CancellationToken; use vm::StoreVmDatabase; @@ -58,7 +58,13 @@ const MAX_MEMPOOL_SIZE_DEFAULT: usize = 10_000; pub enum BlockchainType { #[default] L1, - L2(FeeConfig), + L2(L2Config), +} + +#[derive(Debug, Clone, Default)] +pub struct L2Config { + /// We use a RwLock because the Watcher updates the L1 fee config periodically + pub fee_config: Arc>, } #[derive(Debug)] @@ -150,7 +156,7 @@ impl Blockchain { validate_block(block, &parent_header, &chain_config, ELASTICITY_MULTIPLIER)?; let vm_db = StoreVmDatabase::new(self.storage.clone(), block.header.parent_hash); - let mut vm = self.new_evm(vm_db)?; + let mut vm = self.new_evm(vm_db).await?; let execution_result = vm.execute_block(block)?; let account_updates = vm.get_state_transitions()?; @@ -185,6 +191,15 @@ impl Blockchain { pub async fn generate_witness_for_blocks( &self, blocks: &[Block], + ) -> Result { + self.generate_witness_for_blocks_with_fee_configs(blocks, None) + .await + } + + pub async fn generate_witness_for_blocks_with_fee_configs( + &self, + blocks: &[Block], + fee_configs: Option<&[FeeConfig]>, ) -> Result { let first_block_header = blocks .first() @@ -215,15 +230,25 @@ impl Blockchain { let mut block_hashes = HashMap::new(); let mut codes = Vec::new(); - for block in blocks { + for (i, block) in blocks.iter().enumerate() { let parent_hash = block.header.parent_hash; let vm_db: DynVmDatabase = Box::new(StoreVmDatabase::new(self.storage.clone(), parent_hash)); let logger = Arc::new(DatabaseLogger::new(Arc::new(Mutex::new(Box::new(vm_db))))); - let mut vm = match self.options.r#type { + let mut vm = match &self.options.r#type { BlockchainType::L1 => Evm::new_from_db_for_l1(logger.clone()), - BlockchainType::L2(fee_config) => { - Evm::new_from_db_for_l2(logger.clone(), fee_config) + BlockchainType::L2(_) => { + let l2_config = match fee_configs { + Some(fee_configs) => { + fee_configs.get(i).ok_or(ChainError::WitnessGeneration( + "FeeConfig not found for witness generation".to_string(), + ))? + } + None => Err(ChainError::WitnessGeneration( + "L2Config not found for witness generation".to_string(), + ))?, + }; + Evm::new_from_db_for_l2(logger.clone(), *l2_config) } }; @@ -540,7 +565,7 @@ impl Blockchain { first_block_header.parent_hash, block_hash_cache, ); - let mut vm = self.new_evm(vm_db).map_err(|e| (e.into(), None))?; + let mut vm = self.new_evm(vm_db).await.map_err(|e| (e.into(), None))?; let blocks_len = blocks.len(); let mut all_receipts: Vec<(BlockHash, Vec)> = Vec::with_capacity(blocks_len); @@ -917,10 +942,12 @@ impl Blockchain { Ok(result) } - pub fn new_evm(&self, vm_db: StoreVmDatabase) -> Result { - let evm = match self.options.r#type { + pub async fn new_evm(&self, vm_db: StoreVmDatabase) -> Result { + let evm = match &self.options.r#type { BlockchainType::L1 => Evm::new_for_l1(vm_db), - BlockchainType::L2(fee_config) => Evm::new_for_l2(vm_db, fee_config)?, + BlockchainType::L2(l2_config) => { + Evm::new_for_l2(vm_db, *l2_config.fee_config.read().await)? + } }; Ok(evm) } diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index d3842787b0e..da8e763616e 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -219,7 +219,7 @@ pub struct PayloadBuildContext { } impl PayloadBuildContext { - pub fn new( + pub async fn new( payload: Block, storage: &Store, blockchain_type: BlockchainType, @@ -238,7 +238,9 @@ impl PayloadBuildContext { let vm_db = StoreVmDatabase::new(storage.clone(), payload.header.parent_hash); let vm = match blockchain_type { BlockchainType::L1 => Evm::new_for_l1(vm_db), - BlockchainType::L2(fee_config) => Evm::new_for_l2(vm_db, fee_config)?, + BlockchainType::L2(l2_config) => { + Evm::new_for_l2(vm_db, *l2_config.fee_config.read().await)? + } }; Ok(PayloadBuildContext { @@ -392,7 +394,7 @@ impl Blockchain { debug!("Building payload"); let base_fee = payload.header.base_fee_per_gas.unwrap_or_default(); let mut context = - PayloadBuildContext::new(payload, &self.storage, self.options.r#type.clone())?; + PayloadBuildContext::new(payload, &self.storage, self.options.r#type.clone()).await?; if let BlockchainType::L1 = self.options.r#type { self.apply_system_operations(&mut context)?; diff --git a/crates/blockchain/tracing.rs b/crates/blockchain/tracing.rs index 445b98ffe84..958577d0926 100644 --- a/crates/blockchain/tracing.rs +++ b/crates/blockchain/tracing.rs @@ -107,7 +107,7 @@ impl Blockchain { parent_hash, block_hash_cache, ); - let mut vm = self.new_evm(vm_db)?; + let mut vm = self.new_evm(vm_db).await?; // Run parents to rebuild pre-state for block in blocks_to_re_execute.iter().rev() { vm.rerun_block(block, None)?; diff --git a/crates/common/types/l2.rs b/crates/common/types/l2.rs index d1764247099..59f559d0906 100644 --- a/crates/common/types/l2.rs +++ b/crates/common/types/l2.rs @@ -1,2 +1,3 @@ +pub mod account_diff; pub mod batch; pub mod fee_config; diff --git a/crates/common/types/l2/account_diff.rs b/crates/common/types/l2/account_diff.rs new file mode 100644 index 00000000000..f7589a60eac --- /dev/null +++ b/crates/common/types/l2/account_diff.rs @@ -0,0 +1,323 @@ +// This file needs to be accessible from both the `vm` and `L2` crates. + +use bytes::Bytes; +use ethereum_types::{Address, H256, U256}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use tracing::debug; + +#[derive(Debug, thiserror::Error)] +pub enum DecoderError { + #[error("Decoder failed to deserialize: {0}")] + FailedToDeserialize(String), + #[error("StateDiff failed to deserialize: {0}")] + FailedToDeserializeStateDiff(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum AccountDiffError { + #[error("StateDiff invalid account state diff type: {0}")] + InvalidAccountStateDiffType(u8), + #[error("Both bytecode and bytecode hash are set")] + BytecodeAndBytecodeHashSet, + #[error("The length of the vector is too big to fit in u16: {0}")] + LengthTooBig(#[from] core::num::TryFromIntError), + #[error("Empty account diff")] + EmptyAccountDiff, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct AccountStateDiff { + pub new_balance: Option, + pub nonce_diff: u16, + pub storage: BTreeMap, + pub bytecode: Option, + pub bytecode_hash: Option, +} + +#[derive(Debug, Clone, Copy)] +pub enum AccountStateDiffType { + NewBalance = 1, + NonceDiff = 2, + Storage = 4, + Bytecode = 8, + BytecodeHash = 16, +} + +impl TryFrom for AccountStateDiffType { + type Error = AccountDiffError; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(AccountStateDiffType::NewBalance), + 2 => Ok(AccountStateDiffType::NonceDiff), + 4 => Ok(AccountStateDiffType::Storage), + 8 => Ok(AccountStateDiffType::Bytecode), + 16 => Ok(AccountStateDiffType::BytecodeHash), + _ => Err(AccountDiffError::InvalidAccountStateDiffType(value)), + } + } +} + +impl From for u8 { + fn from(value: AccountStateDiffType) -> Self { + match value { + AccountStateDiffType::NewBalance => 1, + AccountStateDiffType::NonceDiff => 2, + AccountStateDiffType::Storage => 4, + AccountStateDiffType::Bytecode => 8, + AccountStateDiffType::BytecodeHash => 16, + } + } +} + +impl AccountStateDiffType { + // Checks if the type is present in the given value + pub fn is_in(&self, value: u8) -> bool { + value & u8::from(*self) == u8::from(*self) + } +} + +pub fn get_accounts_diff_size( + account_diffs: &HashMap, +) -> Result { + let mut new_accounts_diff_size = 0; + + for (address, diff) in account_diffs.iter() { + let encoded = match diff.encode(address) { + Ok(encoded) => encoded, + Err(AccountDiffError::EmptyAccountDiff) => { + debug!("Skipping empty account diff for address: {address}"); + continue; + } + Err(e) => { + return Err(e); + } + }; + let encoded_len: u64 = encoded.len().try_into()?; + new_accounts_diff_size += encoded_len; + } + Ok(new_accounts_diff_size) +} + +impl AccountStateDiff { + pub fn encode(&self, address: &Address) -> Result, AccountDiffError> { + if self.bytecode.is_some() && self.bytecode_hash.is_some() { + return Err(AccountDiffError::BytecodeAndBytecodeHashSet); + } + + let mut r#type = 0; + let mut encoded: Vec = Vec::new(); + + if let Some(new_balance) = self.new_balance { + let r_type: u8 = AccountStateDiffType::NewBalance.into(); + r#type += r_type; + encoded.extend_from_slice(&new_balance.to_big_endian()); + } + + if self.nonce_diff != 0 { + let r_type: u8 = AccountStateDiffType::NonceDiff.into(); + r#type += r_type; + encoded.extend(self.nonce_diff.to_be_bytes()); + } + + if !self.storage.is_empty() { + let r_type: u8 = AccountStateDiffType::Storage.into(); + let storage_len: u16 = self.storage.len().try_into()?; + r#type += r_type; + encoded.extend(storage_len.to_be_bytes()); + for (key, value) in &self.storage { + encoded.extend_from_slice(&key.0); + encoded.extend_from_slice(&value.to_big_endian()); + } + } + + if let Some(bytecode) = &self.bytecode { + let r_type: u8 = AccountStateDiffType::Bytecode.into(); + let bytecode_len: u16 = bytecode.len().try_into()?; + r#type += r_type; + encoded.extend(bytecode_len.to_be_bytes()); + encoded.extend(bytecode); + } + + if let Some(bytecode_hash) = &self.bytecode_hash { + let r_type: u8 = AccountStateDiffType::BytecodeHash.into(); + r#type += r_type; + encoded.extend(&bytecode_hash.0); + } + + if r#type == 0 { + return Err(AccountDiffError::EmptyAccountDiff); + } + + 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 + /// and the decoded `AccountStateDiff` + pub fn decode(bytes: &[u8]) -> Result<(usize, Address, Self), DecoderError> { + let mut decoder = Decoder::new(bytes); + + let update_type = decoder.get_u8()?; + + let address = decoder.get_address()?; + + let new_balance = if AccountStateDiffType::NewBalance.is_in(update_type) { + Some(decoder.get_u256()?) + } else { + None + }; + + let nonce_diff = if AccountStateDiffType::NonceDiff.is_in(update_type) { + Some(decoder.get_u16()?) + } else { + None + }; + + let mut storage_diff = BTreeMap::new(); + if AccountStateDiffType::Storage.is_in(update_type) { + let storage_slots_updated = decoder.get_u16()?; + + for _ in 0..storage_slots_updated { + let key = decoder.get_h256()?; + let new_value = decoder.get_u256()?; + + storage_diff.insert(key, new_value); + } + } + + let bytecode = if AccountStateDiffType::Bytecode.is_in(update_type) { + let bytecode_len = decoder.get_u16()?; + Some(decoder.get_bytes(bytecode_len.into())?) + } else { + None + }; + + let bytecode_hash = if AccountStateDiffType::BytecodeHash.is_in(update_type) { + Some(decoder.get_h256()?) + } else { + None + }; + + Ok(( + decoder.consumed(), + address, + AccountStateDiff { + new_balance, + nonce_diff: nonce_diff.unwrap_or(0), + storage: storage_diff, + bytecode, + bytecode_hash, + }, + )) + } +} + +pub struct Decoder { + bytes: Bytes, + offset: usize, +} + +impl Decoder { + pub fn new(bytes: &[u8]) -> Self { + Decoder { + bytes: Bytes::copy_from_slice(bytes), + offset: 0, + } + } + + pub fn consumed(&self) -> usize { + self.offset + } + + pub fn advance(&mut self, size: usize) { + self.offset += size; + } + + pub fn get_address(&mut self) -> Result { + let res = Address::from_slice(self.bytes.get(self.offset..self.offset + 20).ok_or( + DecoderError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), + )?); + self.offset += 20; + + Ok(res) + } + + pub fn get_u256(&mut self) -> Result { + let res = U256::from_big_endian(self.bytes.get(self.offset..self.offset + 32).ok_or( + DecoderError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), + )?); + self.offset += 32; + + Ok(res) + } + + pub fn get_h256(&mut self) -> Result { + let res = H256::from_slice(self.bytes.get(self.offset..self.offset + 32).ok_or( + DecoderError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), + )?); + self.offset += 32; + + Ok(res) + } + + pub fn get_u8(&mut self) -> Result { + let res = self + .bytes + .get(self.offset) + .ok_or(DecoderError::FailedToDeserializeStateDiff( + "Not enough bytes".to_string(), + ))?; + self.offset += 1; + + Ok(*res) + } + + pub fn get_u16(&mut self) -> Result { + let res = u16::from_be_bytes( + self.bytes + .get(self.offset..self.offset + 2) + .ok_or(DecoderError::FailedToDeserializeStateDiff( + "Not enough bytes".to_string(), + ))? + .try_into() + .map_err(|_| { + DecoderError::FailedToDeserializeStateDiff("Cannot parse u16".to_string()) + })?, + ); + self.offset += 2; + + Ok(res) + } + + pub fn get_u64(&mut self) -> Result { + let res = u64::from_be_bytes( + self.bytes + .get(self.offset..self.offset + 8) + .ok_or(DecoderError::FailedToDeserializeStateDiff( + "Not enough bytes".to_string(), + ))? + .try_into() + .map_err(|_| { + DecoderError::FailedToDeserializeStateDiff("Cannot parse u64".to_string()) + })?, + ); + self.offset += 8; + + Ok(res) + } + + pub fn get_bytes(&mut self, size: usize) -> Result { + let res = self.bytes.get(self.offset..self.offset + size).ok_or( + DecoderError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), + )?; + self.offset += size; + + Ok(Bytes::copy_from_slice(res)) + } +} diff --git a/crates/common/types/l2/fee_config.rs b/crates/common/types/l2/fee_config.rs index 9aecc43444c..148b89c5977 100644 --- a/crates/common/types/l2/fee_config.rs +++ b/crates/common/types/l2/fee_config.rs @@ -12,6 +12,7 @@ pub struct FeeConfig { #[rkyv(with=OptionH160Wrapper)] pub base_fee_vault: Option
, pub operator_fee_config: Option, + pub l1_fee_config: Option, } /// Configuration for operator fees on L2 @@ -24,3 +25,12 @@ pub struct OperatorFeeConfig { pub operator_fee_vault: Address, pub operator_fee_per_gas: u64, } + +/// L1 Fee is used to pay for the cost of +/// posting data to L1 (e.g. blob data). +#[derive(Serialize, Deserialize, RDeserialize, RSerialize, Archive, Clone, Copy, Debug)] +pub struct L1FeeConfig { + #[rkyv(with=H160Wrapper)] + pub l1_fee_vault: Address, + pub l1_fee_per_blob_gas: u64, +} diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 7e1e9fcece6..55180040828 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -135,7 +135,7 @@ init-l2: ## 🚀 Initializes an L2 Lambda ethrex Client --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ --block-producer.base-fee-vault-address 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ --block-producer.operator-fee-vault-address 0xd5d2a85751b6F158e5b9B8cD509206A865672362 \ - --block-producer.operator-fee ${ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS} \ + --block-producer.operator-fee-per-gas ${ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS} \ --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ --proof-coordinator.addr ${PROOF_COORDINATOR_ADDRESS} diff --git a/crates/l2/based/block_fetcher.rs b/crates/l2/based/block_fetcher.rs index 50235ac9b60..982756a0a22 100644 --- a/crates/l2/based/block_fetcher.rs +++ b/crates/l2/based/block_fetcher.rs @@ -358,7 +358,7 @@ impl BlockFetcher { let mut acc_account_updates: HashMap = HashMap::new(); for block in batch { let vm_db = StoreVmDatabase::new(self.store.clone(), block.header.parent_hash); - let mut vm = self.blockchain.new_evm(vm_db)?; + let mut vm = self.blockchain.new_evm(vm_db).await?; vm.execute_block(block) .map_err(BlockFetcherError::EvmError)?; let account_updates = vm diff --git a/crates/l2/common/src/state_diff.rs b/crates/l2/common/src/state_diff.rs index 0bf278ccb7d..3a921a80f23 100644 --- a/crates/l2/common/src/state_diff.rs +++ b/crates/l2/common/src/state_diff.rs @@ -1,9 +1,10 @@ use std::collections::{BTreeMap, HashMap}; use bytes::Bytes; -use ethereum_types::{Address, H256, U256}; +use ethereum_types::Address; use ethrex_common::types::{ AccountInfo, AccountState, AccountUpdate, BlockHeader, Code, PrivilegedL2Transaction, TxKind, + account_diff::{AccountDiffError, AccountStateDiff, Decoder, DecoderError}, code_hash, }; use ethrex_rlp::decode::RLPDecode; @@ -33,14 +34,8 @@ pub enum StateDiffError { FailedToDeserializeStateDiff(String), #[error("StateDiff failed to serialize: {0}")] FailedToSerializeStateDiff(String), - #[error("StateDiff invalid account state diff type: {0}")] - InvalidAccountStateDiffType(u8), #[error("StateDiff unsupported version: {0}")] UnsupportedVersion(u8), - #[error("Both bytecode and bytecode hash are set")] - BytecodeAndBytecodeHashSet, - #[error("Empty account diff")] - EmptyAccountDiff, #[error("The length of the vector is too big to fit in u16: {0}")] LengthTooBig(#[from] core::num::TryFromIntError), #[error("DB Error: {0}")] @@ -53,24 +48,10 @@ pub enum StateDiffError { InternalError(String), #[error("Evm Error: {0}")] EVMError(#[from] EvmError), -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct AccountStateDiff { - pub new_balance: Option, - pub nonce_diff: u16, - pub storage: BTreeMap, - pub bytecode: Option, - pub bytecode_hash: Option, -} - -#[derive(Debug, Clone, Copy)] -pub enum AccountStateDiffType { - NewBalance = 1, - NonceDiff = 2, - Storage = 4, - Bytecode = 8, - BytecodeHash = 16, + #[error("Decoder Error: {0}")] + DecoderError(#[from] DecoderError), + #[error("AccountDiff Error: {0}")] + AccountDiffError(#[from] AccountDiffError), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -82,40 +63,6 @@ pub struct StateDiff { pub privileged_transactions: Vec, } -impl TryFrom for AccountStateDiffType { - type Error = StateDiffError; - - fn try_from(value: u8) -> Result { - match value { - 1 => Ok(AccountStateDiffType::NewBalance), - 2 => Ok(AccountStateDiffType::NonceDiff), - 4 => Ok(AccountStateDiffType::Storage), - 8 => Ok(AccountStateDiffType::Bytecode), - 16 => Ok(AccountStateDiffType::BytecodeHash), - _ => Err(StateDiffError::InvalidAccountStateDiffType(value)), - } - } -} - -impl From for u8 { - fn from(value: AccountStateDiffType) -> Self { - match value { - AccountStateDiffType::NewBalance => 1, - AccountStateDiffType::NonceDiff => 2, - AccountStateDiffType::Storage => 4, - AccountStateDiffType::Bytecode => 8, - AccountStateDiffType::BytecodeHash => 16, - } - } -} - -impl AccountStateDiffType { - // Checks if the type is present in the given value - pub fn is_in(&self, value: u8) -> bool { - value & u8::from(*self) == u8::from(*self) - } -} - impl Default for StateDiff { fn default() -> Self { StateDiff { @@ -308,232 +255,6 @@ impl StateDiff { } } -impl AccountStateDiff { - pub fn encode(&self, address: &Address) -> Result, StateDiffError> { - if self.bytecode.is_some() && self.bytecode_hash.is_some() { - return Err(StateDiffError::BytecodeAndBytecodeHashSet); - } - - let mut r#type = 0; - let mut encoded: Vec = Vec::new(); - - if let Some(new_balance) = self.new_balance { - let r_type: u8 = AccountStateDiffType::NewBalance.into(); - r#type += r_type; - encoded.extend_from_slice(&new_balance.to_big_endian()); - } - - if self.nonce_diff != 0 { - let r_type: u8 = AccountStateDiffType::NonceDiff.into(); - r#type += r_type; - encoded.extend(self.nonce_diff.to_be_bytes()); - } - - if !self.storage.is_empty() { - let r_type: u8 = AccountStateDiffType::Storage.into(); - let storage_len: u16 = self - .storage - .len() - .try_into() - .map_err(StateDiffError::from)?; - r#type += r_type; - encoded.extend(storage_len.to_be_bytes()); - for (key, value) in &self.storage { - encoded.extend_from_slice(&key.0); - encoded.extend_from_slice(&value.to_big_endian()); - } - } - - if let Some(bytecode) = &self.bytecode { - let r_type: u8 = AccountStateDiffType::Bytecode.into(); - let bytecode_len: u16 = bytecode.len().try_into().map_err(StateDiffError::from)?; - r#type += r_type; - encoded.extend(bytecode_len.to_be_bytes()); - encoded.extend(bytecode); - } - - if let Some(bytecode_hash) = &self.bytecode_hash { - let r_type: u8 = AccountStateDiffType::BytecodeHash.into(); - r#type += r_type; - encoded.extend(&bytecode_hash.0); - } - - if r#type == 0 { - return Err(StateDiffError::EmptyAccountDiff); - } - - 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 - /// and the decoded `AccountStateDiff` - pub fn decode(bytes: &[u8]) -> Result<(usize, Address, Self), StateDiffError> { - let mut decoder = Decoder::new(bytes); - - let update_type = decoder.get_u8()?; - - let address = decoder.get_address()?; - - let new_balance = if AccountStateDiffType::NewBalance.is_in(update_type) { - Some(decoder.get_u256()?) - } else { - None - }; - - let nonce_diff = if AccountStateDiffType::NonceDiff.is_in(update_type) { - Some(decoder.get_u16()?) - } else { - None - }; - - let mut storage_diff = BTreeMap::new(); - if AccountStateDiffType::Storage.is_in(update_type) { - let storage_slots_updated = decoder.get_u16()?; - - for _ in 0..storage_slots_updated { - let key = decoder.get_h256()?; - let new_value = decoder.get_u256()?; - - storage_diff.insert(key, new_value); - } - } - - let bytecode = if AccountStateDiffType::Bytecode.is_in(update_type) { - let bytecode_len = decoder.get_u16()?; - Some(decoder.get_bytes(bytecode_len.into())?) - } else { - None - }; - - let bytecode_hash = if AccountStateDiffType::BytecodeHash.is_in(update_type) { - Some(decoder.get_h256()?) - } else { - None - }; - - Ok(( - decoder.consumed(), - address, - AccountStateDiff { - new_balance, - nonce_diff: nonce_diff.unwrap_or(0), - storage: storage_diff, - bytecode, - bytecode_hash, - }, - )) - } -} - -struct Decoder { - bytes: Bytes, - offset: usize, -} - -impl Decoder { - fn new(bytes: &[u8]) -> Self { - Decoder { - bytes: Bytes::copy_from_slice(bytes), - offset: 0, - } - } - - fn consumed(&self) -> usize { - self.offset - } - - fn advance(&mut self, size: usize) { - self.offset += size; - } - - fn get_address(&mut self) -> Result { - let res = Address::from_slice(self.bytes.get(self.offset..self.offset + 20).ok_or( - StateDiffError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), - )?); - self.offset += 20; - - Ok(res) - } - - fn get_u256(&mut self) -> Result { - let res = U256::from_big_endian(self.bytes.get(self.offset..self.offset + 32).ok_or( - StateDiffError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), - )?); - self.offset += 32; - - Ok(res) - } - - fn get_h256(&mut self) -> Result { - let res = H256::from_slice(self.bytes.get(self.offset..self.offset + 32).ok_or( - StateDiffError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), - )?); - self.offset += 32; - - Ok(res) - } - - fn get_u8(&mut self) -> Result { - let res = - self.bytes - .get(self.offset) - .ok_or(StateDiffError::FailedToDeserializeStateDiff( - "Not enough bytes".to_string(), - ))?; - self.offset += 1; - - Ok(*res) - } - - fn get_u16(&mut self) -> Result { - let res = u16::from_be_bytes( - self.bytes - .get(self.offset..self.offset + 2) - .ok_or(StateDiffError::FailedToDeserializeStateDiff( - "Not enough bytes".to_string(), - ))? - .try_into() - .map_err(|_| { - StateDiffError::FailedToDeserializeStateDiff("Cannot parse u16".to_string()) - })?, - ); - self.offset += 2; - - Ok(res) - } - - fn get_u64(&mut self) -> Result { - let res = u64::from_be_bytes( - self.bytes - .get(self.offset..self.offset + 8) - .ok_or(StateDiffError::FailedToDeserializeStateDiff( - "Not enough bytes".to_string(), - ))? - .try_into() - .map_err(|_| { - StateDiffError::FailedToDeserializeStateDiff("Cannot parse u64".to_string()) - })?, - ); - self.offset += 8; - - Ok(res) - } - - fn get_bytes(&mut self, size: usize) -> Result { - let res = self.bytes.get(self.offset..self.offset + size).ok_or( - StateDiffError::FailedToDeserializeStateDiff("Not enough bytes".to_string()), - )?; - self.offset += size; - - Ok(Bytes::copy_from_slice(res)) - } -} - /// Calculates nonce_diff between current and previous block. pub fn get_nonce_diff( account_update: &AccountUpdate, @@ -613,6 +334,8 @@ pub fn prepare_state_diff( #[cfg(test)] #[allow(clippy::as_conversions)] mod tests { + use ethrex_common::U256; + use super::*; #[test] fn test_l1_message_size() { diff --git a/crates/l2/docker-compose.yaml b/crates/l2/docker-compose.yaml index 35961885ff9..3498d3c30aa 100644 --- a/crates/l2/docker-compose.yaml +++ b/crates/l2/docker-compose.yaml @@ -89,6 +89,8 @@ services: - ETHREX_BLOCK_PRODUCER_BASE_FEE_VAULT_ADDRESS - ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_VAULT_ADDRESS - ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS + - ETHREX_BLOCK_PRODUCER_L1_FEE_VAULT_ADDRESS + - ETHREX_WATCHER_L1_FEE_VAULT_ADDRESS - ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=${ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY:-0x0000000000000000000000000000000000000000} - ETHREX_COMMITTER_COMMIT_TIME=${ETHREX_COMMITTER_COMMIT_TIME:-60000} - ETHREX_WATCHER_WATCH_INTERVAL=${ETHREX_WATCHER_WATCH_INTERVAL:-12000} diff --git a/crates/l2/networking/rpc/clients.rs b/crates/l2/networking/rpc/clients.rs index 29a01717d4f..170e7b91ac0 100644 --- a/crates/l2/networking/rpc/clients.rs +++ b/crates/l2/networking/rpc/clients.rs @@ -3,6 +3,7 @@ use ethrex_common::Address; use ethrex_common::H256; use ethrex_common::U256; use ethrex_l2_common::l1_messages::L1MessageProof; +use ethrex_rpc::clients::eth::errors::GetL1BlobBaseFeeRequestError; use ethrex_rpc::clients::eth::errors::GetOperatorFeeError; use ethrex_rpc::clients::eth::errors::GetOperatorFeeVaultAddressError; use ethrex_rpc::types::block_identifier::BlockIdentifier; @@ -103,3 +104,20 @@ pub async fn get_operator_fee( } } } + +pub async fn get_l1_blob_base_fee_per_gas( + client: &EthClient, + block_number: u64, +) -> Result { + let params = Some(vec![json!(format!("{block_number:#x}"))]); + let request = RpcRequest::new("ethrex_getL1BlobBaseFee", params); + + match client.send_request(request).await? { + RpcResponse::Success(result) => serde_json::from_value(result.result) + .map_err(GetL1BlobBaseFeeRequestError::SerdeJSONError) + .map_err(EthClientError::from), + RpcResponse::Error(error_response) => { + Err(GetL1BlobBaseFeeRequestError::RPCError(error_response.error.message).into()) + } + } +} diff --git a/crates/l2/networking/rpc/l2/fees.rs b/crates/l2/networking/rpc/l2/fees.rs index eb5136c783b..bc61f51ccb8 100644 --- a/crates/l2/networking/rpc/l2/fees.rs +++ b/crates/l2/networking/rpc/l2/fees.rs @@ -159,3 +159,49 @@ impl RpcHandler for GetOperatorFee { Ok(serde_json::Value::String(operator_fee_hex)) } } + +pub struct GetL1BlobBaseFeeRequest { + pub block_number: u64, +} + +impl RpcHandler for GetL1BlobBaseFeeRequest { + fn parse(params: &Option>) -> Result { + let params = params.as_ref().ok_or(ethrex_rpc::RpcErr::BadParams( + "No params provided".to_owned(), + ))?; + if params.len() != 1 { + return Err(ethrex_rpc::RpcErr::BadParams( + "Expected 1 params".to_owned(), + ))?; + }; + // Parse BlockNumber + let hex_str = serde_json::from_value::(params[0].clone()) + .map_err(|e| ethrex_rpc::RpcErr::BadParams(e.to_string()))?; + + // Check that the BlockNumber is 0x prefixed + let hex_str = hex_str + .strip_prefix("0x") + .ok_or(ethrex_rpc::RpcErr::BadHexFormat(0))?; + + // Parse hex string + let block_number = + u64::from_str_radix(hex_str, 16).map_err(|_| ethrex_rpc::RpcErr::BadHexFormat(0))?; + + Ok(GetL1BlobBaseFeeRequest { block_number }) + } + + async fn handle(&self, context: RpcApiContext) -> Result { + debug!("Requested L1BlobBaseFee for block {}", self.block_number); + + let l1_blob_base_fee = context + .rollup_store + .get_fee_config_by_block(self.block_number) + .await? + .and_then(|fc| fc.l1_fee_config) + .map(|cfg| cfg.l1_fee_per_blob_gas) + .unwrap_or_default(); + + serde_json::to_value(l1_blob_base_fee) + .map_err(|err| RpcErr::Internal(format!("Failed to serialize L1BlobBaseFee: {}", err))) + } +} diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index aeeacc9ebea..126dd51b50c 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -1,5 +1,7 @@ use crate::l2::batch::{BatchNumberRequest, GetBatchByBatchNumberRequest}; -use crate::l2::fees::{GetBaseFeeVaultAddress, GetOperatorFee, GetOperatorFeeVaultAddress}; +use crate::l2::fees::{ + GetBaseFeeVaultAddress, GetL1BlobBaseFeeRequest, GetOperatorFee, GetOperatorFeeVaultAddress, +}; use crate::l2::l1_message::GetL1MessageProof; use crate::utils::{RpcErr, RpcNamespace, resolve_namespace}; use axum::extract::State; @@ -220,6 +222,7 @@ pub async fn map_l2_requests(req: &RpcRequest, context: RpcApiContext) -> Result "ethrex_getBaseFeeVaultAddress" => GetBaseFeeVaultAddress::call(req, context).await, "ethrex_getOperatorFeeVaultAddress" => GetOperatorFeeVaultAddress::call(req, context).await, "ethrex_getOperatorFee" => GetOperatorFee::call(req, context).await, + "ethrex_getL1BlobBaseFee" => GetL1BlobBaseFeeRequest::call(req, context).await, unknown_ethrex_l2_method => { Err(ethrex_rpc::RpcErr::MethodNotFound(unknown_ethrex_l2_method.to_owned()).into()) } diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index eddfcddfab2..b4d4e36ff71 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -241,11 +241,13 @@ impl BlockProducer { Ok(()) } async fn store_fee_config_by_block(&self, block_number: u64) -> Result<(), BlockProducerError> { - let BlockchainType::L2(fee_config) = self.blockchain.options.r#type else { + let BlockchainType::L2(l2_config) = &self.blockchain.options.r#type else { error!("Invalid blockchain type. Expected L2."); return Err(BlockProducerError::Custom("Invalid blockchain type".into())); }; + let fee_config = *l2_config.fee_config.read().await; + self.rollup_store .store_fee_config_by_block(block_number, fee_config) .await?; diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs index 76b6f9e0c09..3d906cf1102 100644 --- a/crates/l2/sequencer/block_producer/payload_builder.rs +++ b/crates/l2/sequencer/block_producer/payload_builder.rs @@ -9,15 +9,18 @@ use ethrex_blockchain::{ }; use ethrex_common::{ Address, U256, - types::{Block, Receipt, SAFE_BYTES_PER_BLOB, Transaction, TxType}, + types::{ + Block, Receipt, SAFE_BYTES_PER_BLOB, Transaction, TxType, + account_diff::{AccountStateDiff, get_accounts_diff_size}, + }, }; use ethrex_l2_common::state_diff::{ - AccountStateDiff, BLOCK_HEADER_LEN, L1MESSAGE_LOG_LEN, PRIVILEGED_TX_LOG_LEN, - SIMPLE_TX_STATE_DIFF_SIZE, StateDiffError, + BLOCK_HEADER_LEN, L1MESSAGE_LOG_LEN, PRIVILEGED_TX_LOG_LEN, SIMPLE_TX_STATE_DIFF_SIZE, }; use ethrex_l2_common::{ l1_messages::get_block_l1_messages, privileged_transactions::PRIVILEGED_TX_BUDGET, }; +use ethrex_levm::utils::get_account_diffs_in_tx; use ethrex_metrics::metrics; #[cfg(feature = "metrics")] use ethrex_metrics::{ @@ -25,11 +28,11 @@ use ethrex_metrics::{ metrics_transactions::{METRICS_TX, MetricsTxType}, }; use ethrex_storage::Store; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::ops::Div; use std::sync::Arc; use tokio::time::Instant; -use tracing::{debug, error}; +use tracing::debug; /// L2 payload builder /// Completes the payload building process, return the block value @@ -45,7 +48,8 @@ pub async fn build_payload( let gas_limit = payload.header.gas_limit; debug!("Building payload"); - let mut context = PayloadBuildContext::new(payload, store, blockchain.options.r#type.clone())?; + let mut context = + PayloadBuildContext::new(payload, store, blockchain.options.r#type.clone()).await?; fill_transactions( blockchain.clone(), @@ -203,7 +207,13 @@ pub async fn fill_transactions( } }; - let account_diffs_in_tx = get_account_diffs_in_tx(context)?; + let tx_backup = context.vm.db.get_tx_backup().map_err(|e| { + BlockProducerError::FailedToGetDataFrom(format!("transaction backup: {e}")) + })?; + let account_diffs_in_tx = + get_account_diffs_in_tx(&context.vm.db, tx_backup).map_err(|e| { + BlockProducerError::Custom(format!("Failed to get account diffs from tx: {e}")) + })?; let merged_diffs = merge_diffs(&account_diffs, account_diffs_in_tx); let (tx_size_without_accounts, new_accounts_diff_size) = @@ -284,97 +294,6 @@ fn fetch_mempool_transactions( Ok(plain_txs) } -/// 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( - context: &PayloadBuildContext, -) -> Result, BlockProducerError> { - let mut modified_accounts = HashMap::new(); - - let db = &context.vm.db; - let transaction_backup = db - .get_tx_backup() - .map_err(|e| BlockProducerError::FailedToGetDataFrom(format!("TransactionBackup: {e}")))?; - // First we add the account info - for (address, original_account) in transaction_backup.original_accounts_info.iter() { - let new_account = db.current_accounts_state.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.info.code_hash != original_account.info.code_hash { - // After execution the code should be in db.codes - let code = db.codes.get(&new_account.info.code_hash).ok_or_else(|| { - BlockProducerError::FailedToGetDataFrom("Code DB Cache".to_owned()) - })?; - Some(code.clone()) - } else { - None - }; - - let account_state_diff = AccountStateDiff { - new_balance, - nonce_diff, - storage: BTreeMap::new(), // We add the storage later - bytecode: bytecode.map(|c| c.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 - transaction_backup.original_account_storage_slots.iter() - { - let account_info = db.current_accounts_state.get(address).ok_or( - BlockProducerError::FailedToGetDataFrom("DB Cache".to_owned()), - )?; - - let mut added_storage = BTreeMap::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, - }; - - // If account state diff is NOT empty - if account_state_diff != AccountStateDiff::default() { - modified_accounts.insert(*address, account_state_diff); - } - } - } - - Ok(modified_accounts) -} - /// 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. @@ -419,24 +338,11 @@ fn calculate_tx_diff_size( head_tx: &HeadTransaction, receipt: &Receipt, ) -> Result<(u64, u64), BlockProducerError> { - let mut tx_state_diff_size = 0; - let mut new_accounts_diff_size = 0; + let new_accounts_diff_size = get_accounts_diff_size(merged_diffs).map_err(|e| { + BlockProducerError::Custom(format!("Failed to calculate account diffs size: {}", e)) + })?; - 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)); - } - }; - let encoded_len: u64 = encoded.len().try_into()?; - new_accounts_diff_size += encoded_len; - } + let mut tx_state_diff_size = 0; if is_privileged_tx(head_tx) { tx_state_diff_size += PRIVILEGED_TX_LOG_LEN; diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index 87d13a97eb2..65e4d1c0c1d 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -57,6 +57,7 @@ pub struct L1WatcherConfig { pub check_interval_ms: u64, pub max_block_step: U256, pub watcher_block_delay: u64, + pub l1_blob_base_fee_update_interval: u64, } #[derive(Clone, Debug)] diff --git a/crates/l2/sequencer/errors.rs b/crates/l2/sequencer/errors.rs index 324672278e3..884f953c545 100644 --- a/crates/l2/sequencer/errors.rs +++ b/crates/l2/sequencer/errors.rs @@ -201,12 +201,12 @@ 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), #[error("Internal Error: {0}")] InternalError(#[from] GenServerError), + #[error("EthClientError error: {0}")] + EthClientError(#[from] EthClientError), } #[derive(Debug, thiserror::Error)] diff --git a/crates/l2/sequencer/l1_committer.rs b/crates/l2/sequencer/l1_committer.rs index 4bf37f6f135..2f911ea6f3a 100644 --- a/crates/l2/sequencer/l1_committer.rs +++ b/crates/l2/sequencer/l1_committer.rs @@ -424,7 +424,7 @@ impl L1Committer { let vm_db = StoreVmDatabase::new(self.store.clone(), block_to_commit.header.parent_hash); - let mut vm = self.blockchain.new_evm(vm_db)?; + let mut vm = self.blockchain.new_evm(vm_db).await?; vm.execute_block(&block_to_commit)?; vm.get_state_transitions()? }; @@ -569,7 +569,7 @@ impl L1Committer { let batch_witness = self .blockchain - .generate_witness_for_blocks(&blocks) + .generate_witness_for_blocks_with_fee_configs(&blocks, Some(&fee_configs)) .await .map_err(CommitterError::FailedToGenerateBatchWitness)?; diff --git a/crates/l2/sequencer/l1_watcher.rs b/crates/l2/sequencer/l1_watcher.rs index d13e9157012..cdd7404a839 100644 --- a/crates/l2/sequencer/l1_watcher.rs +++ b/crates/l2/sequencer/l1_watcher.rs @@ -4,7 +4,7 @@ use crate::{EthConfig, L1WatcherConfig, SequencerConfig}; use crate::{sequencer::errors::L1WatcherError, utils::parse::hash_to_address}; use bytes::Bytes; use ethereum_types::{Address, H256, U256}; -use ethrex_blockchain::Blockchain; +use ethrex_blockchain::{Blockchain, BlockchainType}; use ethrex_common::types::{PrivilegedL2Transaction, TxType}; use ethrex_common::utils::keccak; use ethrex_common::{H160, types::Transaction}; @@ -12,6 +12,7 @@ use ethrex_l2_sdk::{ build_generic_tx, get_last_fetched_l1_block, get_pending_privileged_transactions, }; use ethrex_rpc::clients::EthClientError; +use ethrex_rpc::types::block_identifier::{BlockIdentifier, BlockTag}; use ethrex_rpc::types::receipt::RpcLog; use ethrex_rpc::{ clients::eth::{EthClient, Overrides}, @@ -23,6 +24,7 @@ use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, InitResult, Success, send_after, }; use std::collections::BTreeMap; +use std::time::Duration; use std::{cmp::min, sync::Arc}; use tracing::{debug, error, info, warn}; @@ -33,7 +35,8 @@ pub enum CallMessage { #[derive(Clone)] pub enum InMessage { - Watch, + WatchLogs, + UpdateL1BlobBaseFee, } #[derive(Clone)] @@ -54,6 +57,7 @@ pub struct L1Watcher { pub check_interval: u64, pub l1_block_delay: u64, pub sequencer_state: SequencerState, + pub l1_blob_base_fee_update_interval: u64, } #[derive(Clone, Serialize)] @@ -90,6 +94,7 @@ impl L1Watcher { check_interval: watcher_config.check_interval_ms, l1_block_delay: watcher_config.watcher_block_delay, sequencer_state, + l1_blob_base_fee_update_interval: watcher_config.l1_blob_base_fee_update_interval, }) } @@ -307,9 +312,16 @@ impl GenServer for L1Watcher { // Perform the check and suscribe a periodic Watch. handle .clone() - .cast(Self::CastMsg::Watch) + .cast(Self::CastMsg::WatchLogs) .await .map_err(Self::Error::InternalError)?; + + // Perform the first L1 blob base fee update and schedule periodic updates. + handle + .clone() + .cast(InMessage::UpdateL1BlobBaseFee) + .await + .map_err(L1WatcherError::InternalError)?; Ok(Success(self)) } @@ -319,12 +331,50 @@ impl GenServer for L1Watcher { handle: &GenServerHandle, ) -> CastResponse { match message { - Self::CastMsg::Watch => { + Self::CastMsg::WatchLogs => { if let SequencerStatus::Sequencing = self.sequencer_state.status().await { self.watch().await; } let check_interval = random_duration(self.check_interval); - send_after(check_interval, handle.clone(), Self::CastMsg::Watch); + send_after(check_interval, handle.clone(), Self::CastMsg::WatchLogs); + CastResponse::NoReply + } + Self::CastMsg::UpdateL1BlobBaseFee => { + info!("Updating L1 blob base fee"); + let Ok(blob_base_fee) = self + .eth_client + .get_blob_base_fee(BlockIdentifier::Tag(BlockTag::Latest)) + .await + .inspect_err(|e| { + error!("Failed to fetch L1 blob base fee: {e}"); + }) + else { + return CastResponse::NoReply; + }; + + info!("Fetched L1 blob base fee: {blob_base_fee}"); + + let BlockchainType::L2(l2_config) = &self.blockchain.options.r#type else { + error!("Invalid blockchain type. Expected L2."); + return CastResponse::NoReply; + }; + + let mut fee_config_guard = l2_config.fee_config.write().await; + + let Some(l1_fee_config) = fee_config_guard.l1_fee_config.as_mut() else { + warn!("L1 fee config is not set. Skipping L1 blob base fee update."); + return CastResponse::NoReply; + }; + + info!( + "Updating L1 blob base fee from {} to {}", + l1_fee_config.l1_fee_per_blob_gas, blob_base_fee + ); + + l1_fee_config.l1_fee_per_blob_gas = blob_base_fee; + + let interval = Duration::from_millis(self.l1_blob_base_fee_update_interval); + send_after(interval, handle.clone(), Self::CastMsg::UpdateL1BlobBaseFee); CastResponse::NoReply } } diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index e3d2bb62011..3433cd99e70 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -2,7 +2,9 @@ #![allow(clippy::expect_used)] use anyhow::{Context, Result}; use bytes::Bytes; -use ethrex_common::types::TxType; +use ethrex_common::constants::GAS_PER_BLOB; +use ethrex_common::types::account_diff::{AccountStateDiff, get_accounts_diff_size}; +use ethrex_common::types::{SAFE_BYTES_PER_BLOB, TxType}; use ethrex_common::utils::keccak; use ethrex_common::{Address, H160, H256, U256}; use ethrex_l2::monitor::widget::l2_to_l1_messages::{L2ToL1MessageKind, L2ToL1MessageStatus}; @@ -10,7 +12,9 @@ use ethrex_l2::monitor::widget::{L2ToL1MessagesTable, l2_to_l1_messages::L2ToL1M use ethrex_l2::sequencer::l1_watcher::PrivilegedTransactionData; use ethrex_l2_common::calldata::Value; use ethrex_l2_common::l1_messages::L1MessageProof; +use ethrex_l2_common::state_diff::SIMPLE_TX_STATE_DIFF_SIZE; use ethrex_l2_common::utils::get_address_from_secret_key; +use ethrex_l2_rpc::clients::get_l1_blob_base_fee_per_gas; use ethrex_l2_rpc::clients::get_operator_fee; use ethrex_l2_rpc::signer::{LocalSigner, Signer}; use ethrex_l2_sdk::{ @@ -32,6 +36,7 @@ use ethrex_rpc::{ use hex::FromHexError; use secp256k1::SecretKey; use std::cmp::min; +use std::collections::{BTreeMap, HashMap}; use std::ops::{Add, AddAssign}; use std::{ fs::{File, read_to_string}, @@ -59,6 +64,7 @@ use tokio::task::JoinSet; /// INTEGRATION_TEST_PROPOSER_COINBASE_ADDRESS: The address of the l2 coinbase /// INTEGRATION_TEST_PROPOSER_BASE_FEE_VAULT_ADDRESS: The address of the l2 base_fee_vault /// INTEGRATION_TEST_PROPOSER_OPERATOR_FEE_VAULT_ADDRESS: The address of the l2 operator_fee_vault +/// INTEGRATION_TEST_PROPOSER_L1_FEE_VAULT_ADDRESS: The address of the l2 l1_fee_vault /// /// Test parameters: /// @@ -102,6 +108,13 @@ const DEFAULT_OPERATOR_FEE_VAULT_ADDRESS: Address = H160([ 0x65, 0x67, 0x23, 0x62, ]); +// 0x45681AE1768a8936FB87aB11453B4755e322ceec +// pk: 0x3a7b2002e24304d2dfa39e37a10b44585d10395d1c18f127dfa7b90232cfc5e4 +const DEFAULT_L1_FEE_VAULT_ADDRESS: Address = H160([ + 0x45, 0x68, 0x1a, 0xe1, 0x76, 0x8a, 0x89, 0x36, 0xfb, 0x87, 0xab, 0x11, 0x45, 0x3b, 0x47, 0x55, + 0xe3, 0x22, 0xce, 0xec, +]); + const DEFAULT_RICH_KEYS_FILE_PATH: &str = "../../fixtures/keys/private_keys_l1.txt"; const DEFAULT_TEST_KEYS_FILE_PATH: &str = "../../fixtures/keys/private_keys_tests.txt"; @@ -138,6 +151,9 @@ async fn l2_integration_test() -> Result<(), Box> { let operator_fee_vault_balance_before_tests = l2_client .get_balance(operator_fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; + let l1_fee_vault_balance_before_tests = l2_client + .get_balance(l1_fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .await?; let mut set = JoinSet::new(); @@ -215,11 +231,13 @@ async fn l2_integration_test() -> Result<(), Box> { let mut acc_priority_fees = 0; let mut acc_base_fees = 0; let mut acc_operator_fee = 0; + let mut acc_l1_fees = 0; while let Some(res) = set.join_next().await { let fees_details = res??; acc_priority_fees += fees_details.priority_fees; acc_base_fees += fees_details.base_fees; acc_operator_fee += fees_details.operator_fees; + acc_l1_fees += fees_details.l1_fees; } let coinbase_balance_after_tests = l2_client @@ -234,6 +252,10 @@ async fn l2_integration_test() -> Result<(), Box> { .get_balance(operator_fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; + let l1_fee_vault_balance_after_tests = l2_client + .get_balance(l1_fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .await?; + println!("Checking coinbase, base and operator fee vault balances"); assert_eq!( @@ -256,6 +278,12 @@ async fn l2_integration_test() -> Result<(), Box> { "Operator fee vault is not correct after tests" ); + assert_eq!( + l1_fee_vault_balance_after_tests, + l1_fee_vault_balance_before_tests + acc_l1_fees, + "L1 fee vault is not correct after tests" + ); + // Not thread-safe (coinbase and bridge balance checks) test_n_withdraws( &l1_client, @@ -311,6 +339,7 @@ async fn test_upgrade(l1_client: EthClient, l2_client: EthClient) -> Result, ) -> Result<(Address, FeesDetails)> { println!("{test_name}: Deploying contract on L2"); @@ -1928,7 +1996,14 @@ async fn test_deploy( "{test_name}: Deploy transaction failed" ); - let deploy_fees = get_fees_details_l2(&deploy_tx_receipt, l2_client).await; + let contract_bytecode = l2_client + .get_code(contract_address, BlockIdentifier::Tag(BlockTag::Latest)) + .await?; + + let account_diff_size = + get_account_diff_size_for_deploy(&contract_bytecode, storage_after_deploy); + + let deploy_fees = get_fees_details_l2(&deploy_tx_receipt, l2_client, account_diff_size).await?; let deployer_balance_after_deploy = l2_client .get_balance(deployer.address(), BlockIdentifier::Tag(BlockTag::Latest)) @@ -2068,11 +2143,12 @@ struct FeesDetails { base_fees: u64, priority_fees: u64, operator_fees: u64, + l1_fees: u64, } impl FeesDetails { fn total(&self) -> u64 { - self.base_fees + self.priority_fees + self.operator_fees + self.base_fees + self.priority_fees + self.operator_fees + self.l1_fees } } @@ -2084,6 +2160,7 @@ impl Add for FeesDetails { base_fees: self.base_fees + other.base_fees, priority_fees: self.priority_fees + other.priority_fees, operator_fees: self.operator_fees + other.operator_fees, + l1_fees: self.l1_fees + other.l1_fees, } } } @@ -2093,18 +2170,39 @@ impl AddAssign for FeesDetails { self.base_fees += other.base_fees; self.priority_fees += other.priority_fees; self.operator_fees += other.operator_fees; + self.l1_fees += other.l1_fees; } } -async fn get_fees_details_l2(tx_receipt: &RpcReceipt, l2_client: &EthClient) -> FeesDetails { +fn calculate_tx_gas_price( + max_fee_per_gas: u64, + max_priority_fee_per_gas: u64, + base_fee_per_gas: u64, + operator_fee_per_gas: u64, +) -> u64 { + let fee_per_gas = base_fee_per_gas + operator_fee_per_gas; + min(max_priority_fee_per_gas + fee_per_gas, max_fee_per_gas) +} + +async fn get_fees_details_l2( + tx_receipt: &RpcReceipt, + l2_client: &EthClient, + tx_account_diff_size: u64, +) -> Result { let rpc_tx = l2_client .get_transaction_by_hash(tx_receipt.tx_info.transaction_hash) .await .unwrap() .unwrap(); - let gas_used = tx_receipt.tx_info.gas_used; + let tx_gas_used = tx_receipt.tx_info.gas_used; let max_fee_per_gas = rpc_tx.tx.max_fee_per_gas().unwrap(); let max_priority_fee_per_gas: u64 = rpc_tx.tx.max_priority_fee().unwrap(); + let block_number = tx_receipt.block_info.block_number; + + let l1_blob_base_fee_per_gas = get_l1_blob_base_fee_per_gas(l2_client, block_number).await?; + let l1_fee_per_blob: u64 = l1_blob_base_fee_per_gas * u64::from(GAS_PER_BLOB); + let l1_fee_per_blob_byte = l1_fee_per_blob / u64::try_from(SAFE_BYTES_PER_BLOB).unwrap(); + let calculated_l1_fee = l1_fee_per_blob_byte * tx_account_diff_size; let base_fee_per_gas = l2_client .get_block_by_number( @@ -2126,19 +2224,36 @@ async fn get_fees_details_l2(tx_receipt: &RpcReceipt, l2_client: &EthClient) -> .try_into() .unwrap(); + let gas_price = calculate_tx_gas_price( + max_fee_per_gas, + max_priority_fee_per_gas, + base_fee_per_gas, + operator_fee_per_gas, + ); + + let mut l1_gas = calculated_l1_fee / gas_price; + + if l1_gas == 0 && calculated_l1_fee > 0 { + l1_gas = 1; + } + + let actual_gas_used = tx_gas_used - l1_gas; + let priority_fees = min( max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas - operator_fee_per_gas, - ) * gas_used; + ) * actual_gas_used; - let operator_fees = operator_fee_per_gas * gas_used; - let base_fees = base_fee_per_gas * gas_used; + let operator_fees = operator_fee_per_gas * actual_gas_used; + let base_fees = base_fee_per_gas * actual_gas_used; + let l1_fees = l1_gas * gas_price; - FeesDetails { + Ok(FeesDetails { base_fees, priority_fees, operator_fees, - } + l1_fees, + }) } fn l1_client() -> EthClient { @@ -2169,6 +2284,12 @@ fn operator_fee_vault() -> Address { .unwrap_or(DEFAULT_OPERATOR_FEE_VAULT_ADDRESS) } +fn l1_fee_vault() -> Address { + std::env::var("INTEGRATION_TEST_PROPOSER_L1_FEE_VAULT_ADDRESS") + .map(|address| address.parse().expect("Invalid proposer coinbase address")) + .unwrap_or(DEFAULT_L1_FEE_VAULT_ADDRESS) +} + async fn wait_for_l2_deposit_receipt( rpc_receipt: &RpcReceipt, l1_client: &EthClient, @@ -2378,3 +2499,107 @@ async fn wait_for_verified_proof( proof } + +// ====================================================================== +// Auxiliary functions to calculate account diff size for different tx +// ====================================================================== + +fn get_account_diff_size_for_deploy( + bytecode: &Bytes, + storage_after_deploy: BTreeMap, +) -> u64 { + let mut account_diffs = HashMap::new(); + // tx sender + account_diffs.insert(Address::random(), sender_account_diff()); + // Deployed contract account + account_diffs.insert( + Address::random(), + AccountStateDiff { + nonce_diff: 1, + bytecode: Some(bytecode.clone()), + storage: storage_after_deploy, + ..Default::default() + }, + ); + get_accounts_diff_size(&account_diffs).unwrap() +} + +fn get_account_diff_size_for_withdraw() -> u64 { + let mut account_diffs = HashMap::new(); + // tx sender + account_diffs.insert(Address::random(), sender_account_diff()); + // L2_TO_L1_MESSENGER + account_diffs.insert( + Address::random(), + AccountStateDiff { + storage: dummy_modified_storage_slots(1), + ..Default::default() + }, + ); + // zero address + account_diffs.insert( + Address::zero(), + AccountStateDiff { + new_balance: Some(U256::zero()), + ..Default::default() + }, + ); + get_accounts_diff_size(&account_diffs).unwrap() +} + +fn get_account_diff_size_for_erc20withdraw() -> u64 { + let mut account_diffs = HashMap::new(); + // tx sender + account_diffs.insert(Address::random(), sender_account_diff()); + // L2_TO_L1_MESSENGER + account_diffs.insert( + Address::random(), + AccountStateDiff { + storage: dummy_modified_storage_slots(1), + ..Default::default() + }, + ); + // ERC20 contract + account_diffs.insert( + Address::random(), + AccountStateDiff { + storage: dummy_modified_storage_slots(2), + ..Default::default() + }, + ); + get_accounts_diff_size(&account_diffs).unwrap() +} + +fn get_account_diff_size_for_erc20approve() -> u64 { + let mut account_diffs = HashMap::new(); + // tx sender + account_diffs.insert(Address::random(), sender_account_diff()); + + // ERC20 contract + account_diffs.insert( + Address::random(), + AccountStateDiff { + storage: dummy_modified_storage_slots(1), + ..Default::default() + }, + ); + + get_accounts_diff_size(&account_diffs).unwrap() +} + +// Account diff for the sender of the transaction +fn sender_account_diff() -> AccountStateDiff { + AccountStateDiff { + nonce_diff: 1, + new_balance: Some(U256::zero()), + ..Default::default() + } +} + +fn dummy_modified_storage_slots(modified_storage_slots: u64) -> BTreeMap { + let mut storage = BTreeMap::new(); + for _ in 0..modified_storage_slots { + storage.insert(H256::random(), U256::zero()); + } + storage +} diff --git a/crates/networking/rpc/clients/eth/errors.rs b/crates/networking/rpc/clients/eth/errors.rs index b7e35199293..998642d0ba9 100644 --- a/crates/networking/rpc/clients/eth/errors.rs +++ b/crates/networking/rpc/clients/eth/errors.rs @@ -63,6 +63,8 @@ pub enum EthClientError { FailedToGetTxPool(#[from] TxPoolContentError), #[error("ethrex_getBatchByNumber request error: {0}")] GetBatchByNumberError(#[from] GetBatchByNumberError), + #[error("ethrex_getBlobBaseFee request error: {0}")] + GetBlobBaseFeeError(#[from] GetBlobBaseFeeRequestError), #[error("All RPC calls failed")] FailedAllRPC, #[error("Generic transaction error: {0}")] @@ -75,6 +77,8 @@ pub enum EthClientError { GetOperatorFeeVaultAddressError(#[from] GetOperatorFeeVaultAddressError), #[error("ethrex_getOperatorFee request error: {0}")] GetOperatorFeeError(#[from] GetOperatorFeeError), + #[error("ethrex_getL1BlobBaseFee request error: {0}")] + GetL1BlobBaseFeeError(#[from] GetL1BlobBaseFeeRequestError), } #[derive(Debug, thiserror::Error)] @@ -300,6 +304,16 @@ pub enum TxPoolContentError { RPCError(String), } +#[derive(Debug, thiserror::Error)] +pub enum GetBlobBaseFeeRequestError { + #[error("{0}")] + SerdeJSONError(#[from] serde_json::Error), + #[error("{0}")] + RPCError(String), + #[error("{0}")] + ParseIntError(#[from] std::num::ParseIntError), +} + // TODO: move to L2 #[derive(Debug, thiserror::Error)] pub enum GetBatchByNumberError { @@ -330,3 +344,13 @@ pub enum GetOperatorFeeError { #[error("{0}")] RPCError(String), } + +#[derive(Debug, thiserror::Error)] +pub enum GetL1BlobBaseFeeRequestError { + #[error("{0}")] + SerdeJSONError(#[from] serde_json::Error), + #[error("{0}")] + RPCError(String), + #[error("{0}")] + ParseIntError(#[from] std::num::ParseIntError), +} diff --git a/crates/networking/rpc/clients/eth/mod.rs b/crates/networking/rpc/clients/eth/mod.rs index 08996a540aa..923d6a4e294 100644 --- a/crates/networking/rpc/clients/eth/mod.rs +++ b/crates/networking/rpc/clients/eth/mod.rs @@ -1,7 +1,10 @@ use std::collections::BTreeMap; use crate::{ - clients::eth::errors::{CallError, GetPeerCountError, GetWitnessError, TxPoolContentError}, + clients::eth::errors::{ + CallError, GetBlobBaseFeeRequestError, GetPeerCountError, GetWitnessError, + TxPoolContentError, + }, debug::execution_witness::RpcExecutionWitness, mempool::MempoolContent, types::{ @@ -664,6 +667,24 @@ impl EthClient { } } + pub async fn get_blob_base_fee(&self, block: BlockIdentifier) -> Result { + let params = Some(vec![block.into()]); + let request = RpcRequest::new("eth_blobBaseFee", params); + + match self.send_request(request).await? { + RpcResponse::Success(result) => Ok(u64::from_str_radix( + serde_json::from_value::(result.result) + .map_err(GetBlobBaseFeeRequestError::SerdeJSONError)? + .trim_start_matches("0x"), + 16, + ) + .map_err(GetBlobBaseFeeRequestError::ParseIntError)?), + RpcResponse::Error(error_response) => { + Err(GetBlobBaseFeeRequestError::RPCError(error_response.error.message).into()) + } + } + } + /// Smoke test the all the urls by calling eth_blockNumber pub async fn test_urls(&self) -> BTreeMap { let mut map = BTreeMap::new(); diff --git a/crates/networking/rpc/eth/gas_price.rs b/crates/networking/rpc/eth/gas_price.rs index fa6637c07c6..a3b85cffd74 100644 --- a/crates/networking/rpc/eth/gas_price.rs +++ b/crates/networking/rpc/eth/gas_price.rs @@ -39,10 +39,11 @@ impl RpcHandler for GasPrice { let mut gas_price = base_fee + estimated_gas_tip; // Add the operator fee to the gas price if configured - if let BlockchainType::L2(fee_config) = &context.blockchain.options.r#type - && let Some(operator_fee_config) = &fee_config.operator_fee_config - { - gas_price += operator_fee_config.operator_fee_per_gas; + if let BlockchainType::L2(l2_config) = &context.blockchain.options.r#type { + let fee_config = *l2_config.fee_config.read().await; + if let Some(operator_fee_config) = &fee_config.operator_fee_config { + gas_price += operator_fee_config.operator_fee_per_gas; + } } let gas_as_hex = format!("0x{gas_price:x}"); diff --git a/crates/networking/rpc/eth/transaction.rs b/crates/networking/rpc/eth/transaction.rs index cbc9cbc072e..9ce440bb6e2 100644 --- a/crates/networking/rpc/eth/transaction.rs +++ b/crates/networking/rpc/eth/transaction.rs @@ -112,7 +112,8 @@ impl RpcHandler for CallRequest { &header, context.storage, context.blockchain, - )?; + ) + .await?; serde_json::to_value(format!("0x{:#x}", result.output())) .map_err(|error| RpcErr::Internal(error.to_string())) } @@ -347,7 +348,7 @@ impl RpcHandler for CreateAccessListRequest { }; let vm_db = StoreVmDatabase::new(context.storage.clone(), header.hash()); - let mut vm = context.blockchain.new_evm(vm_db)?; + let mut vm = context.blockchain.new_evm(vm_db).await?; // Run transaction and obtain access list let (gas_used, access_list, error) = vm.create_access_list(&self.transaction, &header)?; @@ -471,7 +472,8 @@ impl RpcHandler for EstimateGasRequest { &block_header, storage.clone(), blockchain.clone(), - ); + ) + .await; if let Ok(ExecutionResult::Success { .. }) = result { return serde_json::to_value(format!("{TRANSACTION_GAS:#x}")) .map_err(|error| RpcErr::Internal(error.to_string())); @@ -503,7 +505,8 @@ impl RpcHandler for EstimateGasRequest { &block_header, storage.clone(), blockchain.clone(), - )?; + ) + .await?; let gas_used = result.gas_used(); let gas_refunded = result.gas_refunded(); @@ -531,7 +534,8 @@ impl RpcHandler for EstimateGasRequest { &block_header, storage.clone(), blockchain.clone(), - ); + ) + .await; if let Ok(ExecutionResult::Success { .. }) = result { highest_gas_limit = middle_gas_limit; } else { @@ -561,14 +565,14 @@ async fn recap_with_account_balances( Ok(highest_gas_limit.min(account_gas.as_u64())) } -fn simulate_tx( +async fn simulate_tx( transaction: &GenericTransaction, block_header: &BlockHeader, storage: Store, blockchain: Arc, ) -> Result { let vm_db = StoreVmDatabase::new(storage.clone(), block_header.hash()); - let mut vm = blockchain.new_evm(vm_db)?; + let mut vm = blockchain.new_evm(vm_db).await?; match vm.simulate_tx_from_generic(transaction, block_header)? { ExecutionResult::Revert { diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index b0d5d5503c8..aeab583a138 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -523,14 +523,15 @@ fn adjust_disabled_base_fee(env: &mut Environment) { } } -/// When operator fee is disabled (ie. env.gas_price = 0), set operator fee to None to avoid breaking EVM invariants. -fn adjust_disabled_operator_fee(env: &Environment, vm_type: VMType) -> VMType { +/// When l2 fees are disabled (ie. env.gas_price = 0), set fee configs to None to avoid breaking failing fee deductions +fn adjust_disabled_l2_fees(env: &Environment, vm_type: VMType) -> VMType { if env.gas_price == U256::zero() && let VMType::L2(fee_config) = vm_type { - // Don't deduct operator fee if no gas price is set + // Don't deduct fees if no gas price is set return VMType::L2(FeeConfig { operator_fee_config: None, + l1_fee_config: None, ..fee_config }); } @@ -609,6 +610,6 @@ fn vm_from_generic<'a>( ..Default::default() }), }; - let vm_type = adjust_disabled_operator_fee(&env, vm_type); + let vm_type = adjust_disabled_l2_fees(&env, vm_type); VM::new(env, db, &tx, LevmCallTracer::disabled(), vm_type) } diff --git a/crates/vm/levm/src/hooks/hook.rs b/crates/vm/levm/src/hooks/hook.rs index d79c4fb5a79..1116c97cd33 100644 --- a/crates/vm/levm/src/hooks/hook.rs +++ b/crates/vm/levm/src/hooks/hook.rs @@ -29,7 +29,10 @@ pub fn l1_hooks() -> Vec>> { pub fn l2_hooks(fee_config: FeeConfig) -> Vec>> { vec![ - Rc::new(RefCell::new(L2Hook { fee_config })), + Rc::new(RefCell::new(L2Hook { + fee_config, + pre_execution_backup: Default::default(), + })), Rc::new(RefCell::new(BackupHook::default())), ] } diff --git a/crates/vm/levm/src/hooks/l2_hook.rs b/crates/vm/levm/src/hooks/l2_hook.rs index 6d58f4ee9a4..e21af7a0604 100644 --- a/crates/vm/levm/src/hooks/l2_hook.rs +++ b/crates/vm/levm/src/hooks/l2_hook.rs @@ -1,4 +1,5 @@ use crate::{ + call_frame::CallFrameBackup, errors::{ContextResult, InternalError, TxValidationError}, hooks::{ DefaultHook, @@ -9,14 +10,21 @@ use crate::{ hook::Hook, }, opcodes::Opcode, + utils::get_account_diffs_in_tx, vm::VM, }; +use bytes::Bytes; use ethrex_common::{ Address, H160, H256, U256, + constants::GAS_PER_BLOB, types::{ Code, - fee_config::{FeeConfig, OperatorFeeConfig}, + { + SAFE_BYTES_PER_BLOB, + account_diff::get_accounts_diff_size, + fee_config::{FeeConfig, L1FeeConfig, OperatorFeeConfig}, + }, }, }; @@ -27,6 +35,7 @@ pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([ pub struct L2Hook { pub fee_config: FeeConfig, + pub pre_execution_backup: CallFrameBackup, } impl Hook for L2Hook { @@ -39,6 +48,8 @@ impl Hook for L2Hook { // Max fee per gas must be sufficient to cover base fee + operator fee validate_sufficient_max_fee_per_gas_l2(vm, &self.fee_config.operator_fee_config)?; + // Backup the callframe to calculate the tx state diff later + self.pre_execution_backup = vm.current_call_frame.call_frame_backup.clone(); return Ok(()); } @@ -138,27 +149,60 @@ impl Hook for L2Hook { let gas_refunded: u64 = compute_gas_refunded(vm, ctx_result)?; let actual_gas_used = compute_actual_gas_used(vm, gas_refunded, ctx_result.gas_used)?; - refund_sender(vm, ctx_result, gas_refunded, actual_gas_used)?; - - delete_self_destruct_accounts(vm)?; // Different from L1: - pay_coinbase_l2( + let mut l1_gas = calculate_l1_fee_gas( vm, - ctx_result.gas_used, - &self.fee_config.operator_fee_config, + std::mem::take(&mut self.pre_execution_backup), + &self.fee_config.l1_fee_config, )?; + let mut total_gas = actual_gas_used + .checked_add(l1_gas) + .ok_or(InternalError::Overflow)?; + + // Check that sender has enough gas to pay the l1 fee + if total_gas > vm.current_call_frame.gas_limit { + // Not enough gas to pay l1 fee, force revert + + // Restore VM state to before execution + vm.substate.revert_backup(); + vm.restore_cache_state()?; + + undo_value_transfer(vm)?; + + ctx_result.result = crate::errors::TxResult::Revert( + TxValidationError::InsufficientMaxFeePerGas.into(), + ); + ctx_result.gas_used = vm.current_call_frame.gas_limit; + ctx_result.output = Bytes::new(); + // Set l1_gas to use all remaining gas + l1_gas = vm + .current_call_frame + .gas_limit + .saturating_sub(actual_gas_used); + + total_gas = vm.current_call_frame.gas_limit + } + + delete_self_destruct_accounts(vm)?; + + // L1 fee is paid to the L1 fee vault + pay_to_l1_fee_vault(vm, l1_gas, self.fee_config.l1_fee_config)?; + + refund_sender(vm, ctx_result, gas_refunded, total_gas)?; + + // We pay to coinbase after the l1_fee to avoid charging the diff to every transaction + pay_coinbase_l2(vm, actual_gas_used, &self.fee_config.operator_fee_config)?; + // Base fee is not burned - pay_base_fee_vault(vm, ctx_result.gas_used, self.fee_config.base_fee_vault)?; + pay_base_fee_vault(vm, actual_gas_used, self.fee_config.base_fee_vault)?; // Operator fee is paid to the chain operator - pay_operator_fee( - vm, - ctx_result.gas_used, - &self.fee_config.operator_fee_config, - )?; + pay_operator_fee(vm, actual_gas_used, &self.fee_config.operator_fee_config)?; + + ctx_result.gas_used = total_gas; return Ok(()); } @@ -258,3 +302,73 @@ fn pay_operator_fee( vm.increase_account_balance(fee_config.operator_fee_vault, operator_fee)?; Ok(()) } + +fn calculate_l1_fee( + fee_config: &L1FeeConfig, + account_diffs_size: u64, +) -> Result { + let l1_fee_per_blob: U256 = fee_config + .l1_fee_per_blob_gas + .checked_mul(GAS_PER_BLOB.into()) + .ok_or(InternalError::Overflow)? + .into(); + + let l1_fee_per_blob_byte = l1_fee_per_blob + .checked_div(U256::from(SAFE_BYTES_PER_BLOB)) + .ok_or(InternalError::DivisionByZero)?; + + let l1_fee = l1_fee_per_blob_byte + .checked_mul(U256::from(account_diffs_size)) + .ok_or(InternalError::Overflow)?; + + Ok(l1_fee) +} + +fn calculate_l1_fee_gas( + vm: &mut VM<'_>, + pre_execution_backup: CallFrameBackup, + l1_fee_config: &Option, +) -> Result { + let Some(fee_config) = l1_fee_config else { + // No l1 fee configured, l1 fee gas is zero + return Ok(0); + }; + + let mut execution_backup = vm.current_call_frame.call_frame_backup.clone(); + execution_backup.extend(pre_execution_backup); + let account_diffs_in_tx = get_account_diffs_in_tx(vm.db, execution_backup)?; + let account_diffs_size = get_accounts_diff_size(&account_diffs_in_tx) + .map_err(|e| InternalError::Custom(format!("Failed to get account diffs size: {}", e)))?; + + let l1_fee = calculate_l1_fee(fee_config, account_diffs_size)?; + let mut l1_fee_gas = l1_fee + .checked_div(vm.env.gas_price) + .ok_or(InternalError::DivisionByZero)?; + + // Ensure at least 1 gas is charged if there is a non-zero l1 fee + if l1_fee_gas == U256::zero() && l1_fee > U256::zero() { + l1_fee_gas = U256::one(); + } + + Ok(l1_fee_gas.try_into().map_err(|_| InternalError::Overflow)?) +} + +fn pay_to_l1_fee_vault( + vm: &mut VM<'_>, + gas_to_pay: u64, + l1_fee_config: Option, +) -> Result<(), crate::errors::VMError> { + let Some(fee_config) = l1_fee_config else { + // No l1 fee configured, l1 fee is not paid + return Ok(()); + }; + + let l1_fee = U256::from(gas_to_pay) + .checked_mul(vm.env.gas_price) + .ok_or(InternalError::Overflow)?; + + vm.increase_account_balance(fee_config.l1_fee_vault, l1_fee) + .map_err(|_| TxValidationError::InsufficientAccountFunds)?; + + Ok(()) +} diff --git a/crates/vm/levm/src/utils.rs b/crates/vm/levm/src/utils.rs index 3f9d455a715..c6709341e2c 100644 --- a/crates/vm/levm/src/utils.rs +++ b/crates/vm/levm/src/utils.rs @@ -4,7 +4,7 @@ use crate::{ call_frame::CallFrameBackup, constants::*, db::gen_db::GeneralizedDatabase, - errors::{ExceptionalHalt, InternalError, TxValidationError, VMError}, + errors::{DatabaseError, ExceptionalHalt, InternalError, TxValidationError, VMError}, gas_cost::{ self, ACCESS_LIST_ADDRESS_COST, ACCESS_LIST_STORAGE_KEY_COST, BLOB_GAS_PER_BLOB, COLD_ADDRESS_ACCESS_COST, CREATE_BASE_COST, STANDARD_TOKEN_COST, @@ -19,7 +19,7 @@ use bytes::Bytes; use ethrex_common::{ Address, H256, U256, evm::calculate_create_address, - types::{Account, Code, Fork, Transaction, tx_fields::*}, + types::{Account, Code, Fork, Transaction, account_diff::AccountStateDiff, tx_fields::*}, utils::{keccak, u256_to_big_endian}, }; use ethrex_common::{types::TxKind, utils::u256_from_big_endian_const}; @@ -30,7 +30,7 @@ use secp256k1::{ ecdsa::{RecoverableSignature, RecoveryId}, }; use sha3::{Digest, Keccak256}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; pub type Storage = HashMap; // ================== Address related functions ====================== @@ -164,6 +164,99 @@ pub fn restore_cache_state( Ok(()) } +/// 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). +pub fn get_account_diffs_in_tx( + db: &GeneralizedDatabase, + transaction_backup: CallFrameBackup, +) -> Result, VMError> { + let mut modified_accounts = HashMap::new(); + + // First we add the account info + for (address, original_account) in transaction_backup.original_accounts_info.iter() { + let new_account = db + .current_accounts_state + .get(address) + .ok_or(DatabaseError::Custom("DB Cache".to_owned()))?; + + let nonce_diff: u16 = new_account + .info + .nonce + .checked_sub(original_account.info.nonce) + .ok_or(InternalError::TypeConversion)? + .try_into() + .map_err(|_| InternalError::TypeConversion)?; + + let new_balance = if new_account.info.balance != original_account.info.balance { + Some(new_account.info.balance) + } else { + None + }; + + let bytecode = if new_account.info.code_hash != original_account.info.code_hash { + // After execution the code should be in db.codes + let code = db + .codes + .get(&new_account.info.code_hash) + .ok_or_else(|| DatabaseError::Custom("Code DB Cache".to_owned()))?; + Some(code.clone()) + } else { + None + }; + + let account_state_diff = AccountStateDiff { + new_balance, + nonce_diff, + storage: BTreeMap::new(), // We add the storage later + bytecode: bytecode.map(|c| c.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 + transaction_backup.original_account_storage_slots.iter() + { + let account_info = db + .current_accounts_state + .get(address) + .ok_or(DatabaseError::Custom("DB Cache".to_owned()))?; + + let mut added_storage = BTreeMap::new(); + for key in original_storage_slots.keys() { + added_storage.insert( + *key, + *account_info + .storage + .get(key) + .ok_or(DatabaseError::Custom("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, + }; + + // If account state diff is NOT empty + if account_state_diff != AccountStateDiff::default() { + modified_accounts.insert(*address, account_state_diff); + } + } + } + + Ok(modified_accounts) +} + // ================= Blob hash related functions ===================== pub fn get_base_fee_per_blob_gas( block_excess_blob_gas: Option, diff --git a/docs/l2/architecture/sequencer.md b/docs/l2/architecture/sequencer.md index c53e6d65b02..b1d7b9cfa50 100644 --- a/docs/l2/architecture/sequencer.md +++ b/docs/l2/architecture/sequencer.md @@ -10,7 +10,7 @@ Creates Blocks with a connection to the `auth.rpc` port. ### L1 Watcher -This component monitors the L1 for new deposits made by users. For that, it queries the CommonBridge contract on L1 at regular intervals (defined by the config file) for new DepositInitiated() events. Once a new deposit event is detected, it creates the corresponding deposit transaction on the L2. +This component monitors the L1 for new deposits made by users. For that, it queries the CommonBridge contract on L1 at regular intervals (defined by the config file) for new DepositInitiated() events. Once a new deposit event is detected, it creates the corresponding deposit transaction on the L2. It also periodically fetches the `BlobBaseFee` from L1 (at a configured interval), which is used to compute the [L1 fees](../fundamentals/transaction_fees.md#l1-fees). ### L1 Transaction Sender (a.k.a. L1 Committer) diff --git a/docs/l2/fundamentals/transaction_fees.md b/docs/l2/fundamentals/transaction_fees.md index 92a52328579..8a8f2d9706b 100644 --- a/docs/l2/fundamentals/transaction_fees.md +++ b/docs/l2/fundamentals/transaction_fees.md @@ -17,7 +17,7 @@ The base fee follows the same rules as the Ethereum L1 base fee. It adjusts dyna By default, base fees are burned. However, a sequencer can configure a `base fee vault` address to receive the collected base fees instead of burning them. ```sh -ethrex l2 --block-producer.base-fee-vault-address +ethrex l2 --block-producer.base-fee-vault-address ``` > [!CAUTION] @@ -80,6 +80,56 @@ This behavior ensures that transaction senders **never pay more than `max_fee_pe > The `eth_gasPrice` RPC endpoint has been **modified** to include the `operator_fee_per_gas` value when the operator fee mechanism is active. > This means that the value returned by `eth_gasPrice` corresponds to `base_fee_per_gas + operator_fee_per_gas + estimated_gas_tip`. +## L1 Fees + +L1 fees represent the cost of posting data from the L2 to the L1. +Each transaction is charged based on the amount of **L1 Blob space** it occupies (the size of the transaction’s **stateDiff**). + +After each transaction is executed, the sequencer calculates the `stateDiff` generated by that transaction. +The L1 fee for that transaction is computed as: + +``` +l1_fee = blob_base_fee_per_byte * tx_state_diff_size +``` + +An additional amount of gas (`l1_gas`) is added to the transaction execution so that: + +``` +l1_gas * gas_price = l1_fee +``` + +This guarantees that the total amount charged to the user never exceeds `gas_limit * gas_price`, while transparently accounting for the L1 posting cost. +Importantly, this process happens automatically — users do **not** need to perform any additional steps. +Calls to `eth_estimateGas` already inherit this behavior and will include the extra gas required for the L1 fee. + +The computed L1 fee is deducted from the sender’s balance and transferred to the `L1 Fee Vault` address. + +The **blob base fee per byte** is derived from the L1 `BlobBaseFee`. +The `L1Watcher` periodically fetches the `BlobBaseFee` from L1 (at a configured interval) and uses it to compute: + +``` +blob_base_fee_per_byte = (l1_fee_per_blob_gas * GAS_PER_BLOB) / SAFE_BYTES_PER_BLOB +``` + +See [State Diffs](./state_diffs.md) for more information about how `stateDiffs` works. + + +L1 fee is deactivated by default. To activate it, configure the **L1 fee vault address**: + +```sh +ethrex l2 --block-producer.l1-fee-vault-address +``` + +To configure the **interval** at which the `BlobBaseFee` is fetched from L1: + +```sh +ethrex l2 --block-producer.blob-base-fee-update-interval +``` + +> [!CAUTION] +> If the L1 fee vault and coinbase addresses are the same, its balance will change in a way that differs from the standard L1 behavior, which may break assumptions about EVM compatibility. + + ## Useful RPC Methods The following custom RPC methods are available to query fee-related parameters directly from the L2 node. @@ -90,6 +140,4 @@ Each method accepts a single argument: the **`block_number`** to query historica | `ethrex_getBaseFeeVaultAddress` | Returns the address configured to receive the **base fees** collected in the specified block. | ```ethrex_getBaseFeeVaultAddress {"block_number": 12345}``` | | `ethrex_getOperatorFeeVaultAddress` | Returns the address configured as the **operator fee vault** in the specified block. | ```ethrex_getOperatorFeeVaultAddress {"block_number": 12345}``` | | `ethrex_getOperatorFee` | Returns the **operator fee per gas** value active at the specified block. | ```ethrex_getOperatorFee {"block_number": 12345}``` | - - -## L1 Fees +| `ethrex_getL1BlobBaseFee` | Returns the **L1 blob base fee per gas** fetched from L1 and used for L1 fee computation at the specified block. | ```ethrex_getL1BlobBaseFee {"block_number": 12345}``` | diff --git a/tooling/migrations/src/cli.rs b/tooling/migrations/src/cli.rs index bdb84f3ba80..ffd6e4b90c2 100644 --- a/tooling/migrations/src/cli.rs +++ b/tooling/migrations/src/cli.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; use clap::{Parser as ClapParser, Subcommand as ClapSubcommand}; -use ethrex_blockchain::{Blockchain, BlockchainOptions, BlockchainType}; -use ethrex_common::types::{Block, fee_config::FeeConfig}; +use ethrex_blockchain::{Blockchain, BlockchainOptions, BlockchainType, L2Config}; +use ethrex_common::types::Block; use crate::utils::{migrate_block_body, migrate_block_header}; @@ -93,7 +93,7 @@ async fn migrate_libmdbx_to_rocksdb( let blockchain_opts = BlockchainOptions { // TODO: we may want to migrate using a specified fee config - r#type: BlockchainType::L2(FeeConfig::default()), + r#type: BlockchainType::L2(L2Config::default()), ..Default::default() }; let blockchain = Blockchain::new(new_store.clone(), blockchain_opts);