diff --git a/.github/workflows/main_prover.yaml b/.github/workflows/main_prover.yaml index 27a77cf74d1..86a99062eb3 100644 --- a/.github/workflows/main_prover.yaml +++ b/.github/workflows/main_prover.yaml @@ -79,6 +79,7 @@ jobs: ETHREX_PROPOSER_BLOCK_TIME=12000 \ ETHREX_COMMITTER_COMMIT_TIME=30000 \ ETHREX_WATCHER_WATCH_INTERVAL=1000 \ + ETHREX_BLOCK_PRODUCER_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ 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 07e961aba3e..589220e5855 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -96,22 +96,32 @@ jobs: validium: true web3signer: false use_native_token: false + based: false compose_targets: [docker-compose.yaml] - name: "Vanilla" validium: false web3signer: false use_native_token: false + based: false compose_targets: [docker-compose.yaml] - name: "Vanilla with Web3signer" validium: false web3signer: true use_native_token: false + based: false compose_targets: [docker-compose.yaml, docker-compose-l2-web3signer.yaml] - name: "ERC20 as Native Token" validium: false web3signer: false use_native_token: true + based: false + compose_targets: [docker-compose.yaml] + - name: "Based" + validium: false + web3signer: false + use_native_token: false + based: true compose_targets: [docker-compose.yaml] steps: - name: Checkout sources @@ -158,14 +168,14 @@ jobs: docker compose up --detach ethrex_l1 - name: Install rex - if: matrix.use_native_token + if: ${{ matrix.use_native_token || matrix.based }} run: | cd .. git clone https://github.com/lambdaclass/rex.git cd rex git checkout 18466ec1c3dbcbbf22a68fc1f850d660cb2fbf1f make cli - echo "rex install successfully at $(which rex)" + echo "rex install successfully at $(which rex)" - name: Deploy native token if: matrix.use_native_token @@ -181,7 +191,10 @@ jobs: else export ETHREX_NATIVE_TOKEN_L1_ADDRESS=0x0000000000000000000000000000000000000000 fi - + if [ "${{ matrix.based }}" = true ]; then + export ETHREX_DEPLOYER_DEPLOY_BASED_CONTRACTS=true + export COMPILE_CONTRACTS=true + fi DOCKER_ETHREX_WORKDIR=/usr/local/bin \ ETHREX_DEPLOYER_DEPLOY_RICH=true \ ETHREX_DEPLOYER_PICO_CONTRACT_ADDRESS=0x00000000000000000000000000000000000000aa \ @@ -196,86 +209,8 @@ jobs: docker cp contract_deployer:/env/.env cmd/.env cat cmd/.env - - name: Start Sequencer - run: | - cd crates/l2 - DOCKER_ETHREX_WORKDIR=/usr/local/bin \ - ETHREX_L2_VALIDIUM=${{ matrix.validium }} \ - ETHREX_WATCHER_BLOCK_DELAY=0 \ - ETHREX_COMMITTER_COMMIT_TIME=15000 \ - ETHREX_WATCHER_WATCH_INTERVAL=1000 \ - docker compose -f ${{ join(matrix.compose_targets, ' -f ') }} up --detach --no-deps ethrex_l2 - - - name: Run test - run: | - sudo chmod -R a+rw crates/l2 - cd crates/l2 - RUST_LOG=info,ethrex_prover_lib=debug make init-prover & - docker logs --follow ethrex_l2 & - PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test l2 --release -- --nocapture --test-threads=1 - killall ethrex -s SIGINT - - integration-test-based: - name: Integration Test - Based - runs-on: ubuntu-latest - needs: build-docker - steps: - - name: Checkout sources - uses: actions/checkout@v4 - - name: Setup Rust Environment - uses: ./.github/actions/setup-rust - - - name: Install solc - uses: lambdaclass/get-solc@master - with: - version: v0.8.29 - token: ${{ secrets.GITHUB_TOKEN || '' }} - - - name: Download ethrex image artifact - uses: actions/download-artifact@v4 - with: - name: ethrex_image - path: /tmp - - - name: Load ethrex image - run: | - docker load --input /tmp/ethrex_image.tar - - # also creates empty verification keys (as workflow runs with exec backend) - - name: Build prover - run: | - cd crates/l2 - make build-prover - mkdir -p prover/src/guest_program/src/sp1/out && touch prover/src/guest_program/src/sp1/out/riscv32im-succinct-zkvm-vk - - - name: Start L1 & Deploy contracts - run: | - touch cmd/.env - cd crates/l2 - DOCKER_ETHREX_WORKDIR=/usr/local/bin \ - ETHREX_DEPLOYER_DEPLOY_RICH=true \ - ETHREX_DEPLOYER_PICO_CONTRACT_ADDRESS=0x00000000000000000000000000000000000000aa \ - ETHREX_DEPLOYER_SP1_CONTRACT_ADDRESS=0x00000000000000000000000000000000000000aa \ - ETHREX_DEPLOYER_RISC0_CONTRACT_ADDRESS=0x00000000000000000000000000000000000000aa \ - ETHREX_DEPLOYER_DEPLOY_BASED_CONTRACTS=true \ - COMPILE_CONTRACTS=true \ - docker compose up contract_deployer - - - name: Copy env to host - run: | - docker cp contract_deployer:/env/.env cmd/.env - cat cmd/.env - - - name: Install rex - run: | - cd .. - git clone https://github.com/lambdaclass/rex.git - cd rex - git checkout 18466ec1c3dbcbbf22a68fc1f850d660cb2fbf1f - make cli - echo "rex install successfully at $(which rex)" - - name: Register sequencer + if: matrix.based run: | cd cmd SEQUENCER_REGISTRY=$(grep ETHREX_DEPLOYER_SEQUENCER_REGISTRY .env | cut -d= -f2) @@ -290,22 +225,28 @@ jobs: - name: Start Sequencer run: | - cd cmd - SEQUENCER_REGISTRY=$(grep ETHREX_DEPLOYER_SEQUENCER_REGISTRY .env | cut -d= -f2) - export SEQUENCER_REGISTRY - cd ../crates/l2 - + if [ "${{ matrix.based }}" = true ]; then + cd cmd + ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=$(grep ETHREX_DEPLOYER_SEQUENCER_REGISTRY .env | cut -d= -f2) + export ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY + export ETHREX_BASED=true + cd .. + else + export ETHREX_BLOCK_PRODUCER_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 + fi + cd crates/l2 DOCKER_ETHREX_WORKDIR=/usr/local/bin \ - ETHREX_COMMITTER_VALIDIUM=false \ - ETHREX_COMMITTER_COMMIT_TIME=15000 \ + ETHREX_L2_VALIDIUM=${{ matrix.validium }} \ ETHREX_WATCHER_BLOCK_DELAY=0 \ - ETHREX_BASED=true \ - ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY="$SEQUENCER_REGISTRY" \ + ETHREX_COMMITTER_COMMIT_TIME=15000 \ ETHREX_WATCHER_WATCH_INTERVAL=1000 \ - docker compose up --detach --no-deps ethrex_l2 + docker compose -f ${{ join(matrix.compose_targets, ' -f ') }} up --detach --no-deps ethrex_l2 - name: Run test run: | + if [ "${{ matrix.based }}" = true ]; then + export INTEGRATION_TEST_SKIP_FEE_VAULT_CHECK=true + fi sudo chmod -R a+rw crates/l2 cd crates/l2 RUST_LOG=info,ethrex_prover_lib=debug make init-prover & @@ -387,6 +328,7 @@ jobs: ETHREX_WATCHER_WATCH_INTERVAL=1000 \ DOCKER_ETHREX_WORKDIR=/usr/local/bin \ ETHREX_COMMITTER_COMMIT_TIME=15000 \ + ETHREX_BLOCK_PRODUCER_FEE_VAULT_ADDRESS=0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ 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 @@ -494,11 +436,13 @@ jobs: - name: Init prover run: | target/release/ethrex l2 prover --proof-coordinators http://localhost:3900 & - + - name: Run test run: | cd crates/l2 - PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d cargo test l2 --release -- --nocapture --test-threads=1 + INTEGRATION_TEST_SKIP_FEE_VAULT_CHECK=true \ + PROPOSER_COINBASE_ADDRESS=0x0007a881CD95B1484fca47615B64803dad620C8d \ + cargo test l2 --release -- --nocapture --test-threads=1 # The purpose of this job is to add it as a required check in GitHub so that we don't have to add every individual job as a required check all-tests: @@ -509,12 +453,11 @@ jobs: [ integration-test, state-diff-test, - integration-test-based, integration-test-tdx, integration-test-l2-dev, ] # Make sure this job runs even if the previous jobs failed or were skipped - if: ${{ always() && needs.integration-test.result != 'skipped' && needs.state-diff-test.result != 'skipped' && needs.integration-test-based.result != 'skipped' && needs.integration-test-tdx.result != 'skipped' && needs.integration-test-l2-dev.result != 'skipped' }} + if: ${{ always() && needs.integration-test.result != 'skipped' && needs.state-diff-test.result != 'skipped' && needs.integration-test-tdx.result != 'skipped' && needs.integration-test-l2-dev.result != 'skipped' }} steps: - name: Check if any job failed run: | @@ -528,11 +471,6 @@ jobs: exit 1 fi - if [ "${{ needs.integration-test-based.result }}" != "success" ]; then - echo "Job Integration Tests Based failed" - exit 1 - fi - if [ "${{ needs.integration-test-l2-dev.result }}" != "success" ]; then echo "Job Integration Tests L2 Dev failed" exit 1 diff --git a/cmd/ethrex/cli.rs b/cmd/ethrex/cli.rs index 443d5b5a6b7..18ad18e821f 100644 --- a/cmd/ethrex/cli.rs +++ b/cmd/ethrex/cli.rs @@ -7,7 +7,7 @@ use std::{ use clap::{ArgAction, Parser as ClapParser, Subcommand as ClapSubcommand}; use ethrex_blockchain::{BlockchainOptions, BlockchainType, error::ChainError}; -use ethrex_common::types::{Block, Genesis}; +use ethrex_common::types::{Block, Genesis, fee_config::FeeConfig}; use ethrex_p2p::{sync::SyncMode, tx_broadcaster::BROADCAST_INTERVAL_MS, types::Node}; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::error::StoreError; @@ -346,7 +346,7 @@ impl Subcommand { let network = get_network(opts); let genesis = network.get_genesis()?; let blockchain_type = if l2 { - BlockchainType::L2 + BlockchainType::L2(FeeConfig::default()) } else { BlockchainType::L1 }; diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index 9e39147b141..b55dbb359a2 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -8,6 +8,7 @@ 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; use ethrex_common::{Address, types::DEFAULT_BUILDER_GAS_CEIL}; use ethrex_l2::SequencerConfig; use ethrex_p2p::{ @@ -155,9 +156,14 @@ pub async fn init_l2( let store = init_store(&datadir, genesis).await; let rollup_store = init_rollup_store(&rollup_store_dir).await; + let fee_config = FeeConfig { + fee_vault: opts.sequencer_opts.block_producer_opts.fee_vault_address, + ..Default::default() + }; + let blockchain_opts = ethrex_blockchain::BlockchainOptions { max_mempool_size: opts.node_opts.mempool_max_size, - r#type: BlockchainType::L2, + r#type: BlockchainType::L2(fee_config), perf_logs_enabled: true, }; diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 518fb42268c..6dd9ffcfa57 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -160,6 +160,7 @@ impl TryFrom for SequencerConfig { .block_producer_opts .coinbase_address .ok_or(SequencerOptionsError::NoCoinbaseAddress)?, + fee_vault_address: opts.block_producer_opts.fee_vault_address, elasticity_multiplier: opts.block_producer_opts.elasticity_multiplier, block_gas_limit: opts.block_producer_opts.block_gas_limit, }, @@ -387,6 +388,13 @@ pub struct BlockProducerOptions { required_unless_present = "dev" )] pub coinbase_address: Option
, + #[arg( + long = "block-producer.fee-vault-address", + value_name = "ADDRESS", + env = "ETHREX_BLOCK_PRODUCER_FEE_VAULT_ADDRESS", + help_heading = "Block producer options" + )] + pub fee_vault_address: Option
, #[arg( long, default_value = "2", @@ -415,6 +423,7 @@ impl Default for BlockProducerOptions { .parse() .unwrap(), ), + 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 2e953098461..8801344ecdb 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -13,6 +13,7 @@ use error::MempoolError; use error::{ChainError, InvalidBlockError}; use ethrex_common::constants::{GAS_PER_BLOB, MAX_RLP_BLOCK_SIZE, MIN_BASE_FEE_PER_BLOB_GAS}; use ethrex_common::types::block_execution_witness::ExecutionWitness; +use ethrex_common::types::fee_config::FeeConfig; use ethrex_common::types::requests::{EncodedRequests, Requests, compute_requests_hash}; use ethrex_common::types::{ AccountUpdate, Block, BlockHash, BlockHeader, BlockNumber, ChainConfig, EIP4844Transaction, @@ -57,7 +58,7 @@ const MAX_MEMPOOL_SIZE_DEFAULT: usize = 10_000; pub enum BlockchainType { #[default] L1, - L2, + L2(FeeConfig), } #[derive(Debug)] @@ -221,7 +222,9 @@ impl Blockchain { let logger = Arc::new(DatabaseLogger::new(Arc::new(Mutex::new(Box::new(vm_db))))); let mut vm = match self.options.r#type { BlockchainType::L1 => Evm::new_from_db_for_l1(logger.clone()), - BlockchainType::L2 => Evm::new_from_db_for_l2(logger.clone()), + BlockchainType::L2(fee_config) => { + Evm::new_from_db_for_l2(logger.clone(), fee_config) + } }; // Re-execute block with logger @@ -918,7 +921,7 @@ impl Blockchain { pub 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 => Evm::new_for_l2(vm_db)?, + BlockchainType::L2(fee_config) => Evm::new_for_l2(vm_db, fee_config)?, }; Ok(evm) } diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index bf9ae54ae5e..280854e90c9 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -236,7 +236,7 @@ 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 => Evm::new_for_l2(vm_db)?, + BlockchainType::L2(fee_config) => Evm::new_for_l2(vm_db, fee_config)?, }; Ok(PayloadBuildContext { diff --git a/crates/common/rkyv_utils.rs b/crates/common/rkyv_utils.rs index 447a7b3f5ba..e873663b181 100644 --- a/crates/common/rkyv_utils.rs +++ b/crates/common/rkyv_utils.rs @@ -63,6 +63,23 @@ impl Hash for ArchivedH160Wrapper { } } +#[derive(Archive, Serialize, Deserialize)] +#[rkyv(remote = Option)] +pub enum OptionH160Wrapper { + Some(#[rkyv(with = H160Wrapper)] H160), + None, +} + +impl From for Option { + fn from(value: OptionH160Wrapper) -> Self { + if let OptionH160Wrapper::Some(x) = value { + Some(x) + } else { + None + } + } +} + #[derive( Archive, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, )] diff --git a/crates/common/types/l2.rs b/crates/common/types/l2.rs new file mode 100644 index 00000000000..d1764247099 --- /dev/null +++ b/crates/common/types/l2.rs @@ -0,0 +1,2 @@ +pub mod batch; +pub mod fee_config; diff --git a/crates/common/types/batch.rs b/crates/common/types/l2/batch.rs similarity index 90% rename from crates/common/types/batch.rs rename to crates/common/types/l2/batch.rs index 901cdabd03b..379d6ce814e 100644 --- a/crates/common/types/batch.rs +++ b/crates/common/types/l2/batch.rs @@ -1,9 +1,6 @@ +use crate::{H256, types::BlobsBundle}; use serde::{Deserialize, Serialize}; -use crate::H256; - -use super::BlobsBundle; - #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct Batch { pub number: u64, diff --git a/crates/common/types/l2/fee_config.rs b/crates/common/types/l2/fee_config.rs new file mode 100644 index 00000000000..9a37cf5e52f --- /dev/null +++ b/crates/common/types/l2/fee_config.rs @@ -0,0 +1,26 @@ +use ethereum_types::Address; +use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize}; +use serde::{Deserialize, Serialize}; + +use crate::rkyv_utils::{H160Wrapper, OptionH160Wrapper}; + +#[derive( + Serialize, Deserialize, RDeserialize, RSerialize, Archive, Clone, Copy, Debug, Default, +)] +pub struct FeeConfig { + /// If set, the base fee is sent to this address instead of being burned. + #[rkyv(with=OptionH160Wrapper)] + pub fee_vault: Option
, + pub operator_fee_config: Option, +} + +/// Configuration for operator fees on L2 +/// The operator fee is an additional fee on top of the base fee +/// that is sent to the operator fee vault. +/// This is used to pay for the cost of running the L2 network. +#[derive(Serialize, Deserialize, RDeserialize, RSerialize, Archive, Clone, Copy, Debug)] +pub struct OperatorFeeConfig { + #[rkyv(with=H160Wrapper)] + pub operator_fee_vault: Address, + pub operator_fee: u64, +} diff --git a/crates/common/types/mod.rs b/crates/common/types/mod.rs index 2d9134bf13e..96487001e11 100644 --- a/crates/common/types/mod.rs +++ b/crates/common/types/mod.rs @@ -1,12 +1,12 @@ mod account; mod account_update; -pub mod batch; pub mod blobs_bundle; mod block; pub mod block_execution_witness; mod constants; mod fork_id; mod genesis; +pub mod l2; pub mod payload; mod receipt; pub mod requests; @@ -20,6 +20,7 @@ pub use block::*; pub use constants::*; pub use fork_id::*; pub use genesis::*; +pub use l2::*; pub use receipt::*; pub use transaction::*; pub use tx_fields::*; diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 4c848c7dc38..8eb355dbe3f 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -132,6 +132,7 @@ init-l2: ## 🚀 Initializes an L2 Lambda ethrex Client --l1.on-chain-proposer-address ${DEFAULT_ON_CHAIN_PROPOSER_ADDRESS} \ --eth.rpc-url ${L1_RPC_URL} \ --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ + --block-producer.fee-vault-address 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 \ --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ --proof-coordinator.addr ${PROOF_COORDINATOR_ADDRESS} diff --git a/crates/l2/docker-compose-l2-store.overrides.yaml b/crates/l2/docker-compose-l2-store.overrides.yaml index 38e02b99f51..ec1bcbc1162 100644 --- a/crates/l2/docker-compose-l2-store.overrides.yaml +++ b/crates/l2/docker-compose-l2-store.overrides.yaml @@ -11,6 +11,7 @@ services: --datadir /store --proof-coordinator.addr 0.0.0.0 --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d + --block-producer.fee-vault-address 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d --no-monitor diff --git a/crates/l2/docker-compose-l2-web3signer.yaml b/crates/l2/docker-compose-l2-web3signer.yaml index 2e6f76b5b62..76e2efae7ba 100644 --- a/crates/l2/docker-compose-l2-web3signer.yaml +++ b/crates/l2/docker-compose-l2-web3signer.yaml @@ -18,6 +18,7 @@ services: --authrpc.port 8552 --proof-coordinator.addr 0.0.0.0 --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d + --block-producer.fee-vault-address 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 --committer.remote-signer-url http://web3signer:9000 --committer.remote-signer-public-key 02eadbea0cdb17fda8d56fc9c51df8a6158c2ab157aabf2ca57c3a32cd69f98bbc --proof-coordinator.remote-signer-url http://web3signer:9000 diff --git a/crates/l2/docker-compose.yaml b/crates/l2/docker-compose.yaml index b34fbd68916..c68ca854315 100644 --- a/crates/l2/docker-compose.yaml +++ b/crates/l2/docker-compose.yaml @@ -79,6 +79,7 @@ services: - ETHREX_DEPLOYER_PICO_DEPLOY_VERIFIER=${ETHREX_DEPLOYER_PICO_DEPLOY_VERIFIER:-false} - ETHREX_WATCHER_BLOCK_DELAY=${ETHREX_WATCHER_BLOCK_DELAY:-0} - ETHREX_BASED=${ETHREX_BASED:-false} + - ETHREX_BLOCK_PRODUCER_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 73999f13bba..3be8a8635b8 100644 --- a/crates/l2/networking/rpc/clients.rs +++ b/crates/l2/networking/rpc/clients.rs @@ -1,4 +1,5 @@ use crate::l2::batch::RpcBatch; +use ethrex_common::Address; use ethrex_common::H256; use ethrex_l2_common::l1_messages::L1MessageProof; use ethrex_rpc::{ @@ -7,7 +8,7 @@ use ethrex_rpc::{ EthClientError, eth::{ RpcResponse, - errors::{GetBatchByNumberError, GetMessageProofError}, + errors::{GetBatchByNumberError, GetFeeVaultAddressError, GetMessageProofError}, }, }, utils::RpcRequest, @@ -47,3 +48,16 @@ pub async fn get_batch_by_number( } } } + +pub async fn get_fee_vault_address(client: &EthClient) -> Result, EthClientError> { + let request = RpcRequest::new("ethrex_getFeeVaultAddress", None); + + match client.send_request(request).await? { + RpcResponse::Success(result) => serde_json::from_value(result.result) + .map_err(GetFeeVaultAddressError::SerdeJSONError) + .map_err(EthClientError::from), + RpcResponse::Error(error_response) => { + Err(GetFeeVaultAddressError::RPCError(error_response.error.message).into()) + } + } +} diff --git a/crates/l2/networking/rpc/l2/fee_vault.rs b/crates/l2/networking/rpc/l2/fee_vault.rs new file mode 100644 index 00000000000..59d1132f366 --- /dev/null +++ b/crates/l2/networking/rpc/l2/fee_vault.rs @@ -0,0 +1,32 @@ +use serde_json::Value; + +use crate::{ + rpc::{RpcApiContext, RpcHandler}, + utils::RpcErr, +}; + +pub struct GetFeeVaultAddress; + +impl RpcHandler for GetFeeVaultAddress { + fn parse(_params: &Option>) -> Result { + Ok(GetFeeVaultAddress) + } + + async fn handle(&self, context: RpcApiContext) -> Result { + let fee_vault_address = match context.l1_ctx.blockchain.options.r#type { + ethrex_blockchain::BlockchainType::L1 => None, + ethrex_blockchain::BlockchainType::L2(fee_config) => fee_config.fee_vault, + }; + + Ok( + serde_json::to_value(fee_vault_address.map(|addr| format!("{:#x}", addr))).map_err( + |e| { + ethrex_rpc::RpcErr::Internal(format!( + "Failed to serialize fee vault address: {}", + e + )) + }, + )?, + ) + } +} diff --git a/crates/l2/networking/rpc/l2/mod.rs b/crates/l2/networking/rpc/l2/mod.rs index 2e128754499..7b3394e5d57 100644 --- a/crates/l2/networking/rpc/l2/mod.rs +++ b/crates/l2/networking/rpc/l2/mod.rs @@ -1,3 +1,4 @@ pub mod batch; +pub mod fee_vault; pub mod l1_message; pub mod transaction; diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index 9c1508bfc39..c4b6f65744a 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -1,4 +1,5 @@ use crate::l2::batch::{BatchNumberRequest, GetBatchByBatchNumberRequest}; +use crate::l2::fee_vault::GetFeeVaultAddress; use crate::l2::l1_message::GetL1MessageProof; use crate::utils::{RpcErr, RpcNamespace, resolve_namespace}; use axum::extract::State; @@ -216,6 +217,7 @@ pub async fn map_l2_requests(req: &RpcRequest, context: RpcApiContext) -> Result "ethrex_getMessageProof" => GetL1MessageProof::call(req, context).await, "ethrex_batchNumber" => BatchNumberRequest::call(req, context).await, "ethrex_getBatchByNumber" => GetBatchByBatchNumberRequest::call(req, context).await, + "ethrex_getFeeVaultAddress" => GetFeeVaultAddress::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/prover/src/guest_program/src/execution.rs b/crates/l2/prover/src/guest_program/src/execution.rs index a9e9aa83191..34846b9c1c3 100644 --- a/crates/l2/prover/src/guest_program/src/execution.rs +++ b/crates/l2/prover/src/guest_program/src/execution.rs @@ -7,6 +7,7 @@ use ethrex_blockchain::{ }; use ethrex_common::types::AccountUpdate; use ethrex_common::types::block_execution_witness::ExecutionWitness; +use ethrex_common::types::fee_config::FeeConfig; use ethrex_common::types::{ block_execution_witness::GuestProgramState, block_execution_witness::GuestProgramStateError, }; @@ -64,6 +65,9 @@ pub enum StatelessExecutionError { #[cfg(feature = "l2")] #[error("Invalid state diff")] InvalidStateDiff, + #[cfg(feature = "l2")] + #[error("FeeConfig not provided for L2 execution")] + FeeConfigNotFound, #[error("Batch has no blocks")] EmptyBatchError, #[error("Invalid database")] @@ -97,6 +101,7 @@ pub fn execution_program(input: ProgramInput) -> Result Result, blob_commitment: Commitment, blob_proof: Proof, chain_id: u64, @@ -171,7 +178,7 @@ pub fn stateless_validation_l2( nodes_hashed, codes_hashed, parent_block_header, - } = execute_stateless(blocks, execution_witness, elasticity_multiplier)?; + } = execute_stateless(blocks, execution_witness, elasticity_multiplier, fee_config)?; let (l1messages, privileged_transactions) = get_batch_l1messages_and_privileged_transactions(blocks, &receipts)?; @@ -255,6 +262,7 @@ fn execute_stateless( blocks: &[Block], execution_witness: ExecutionWitness, elasticity_multiplier: u64, + fee_config: Option, ) -> Result { let guest_program_state: GuestProgramState = execution_witness .try_into() @@ -320,7 +328,10 @@ fn execute_stateless( // Execute block #[cfg(feature = "l2")] - let mut vm = Evm::new_for_l2(wrapped_db.clone())?; + let mut vm = Evm::new_for_l2( + wrapped_db.clone(), + fee_config.ok_or_else(|| StatelessExecutionError::FeeConfigNotFound)?, + )?; #[cfg(not(feature = "l2"))] let mut vm = Evm::new_for_l1(wrapped_db.clone()); let result = vm diff --git a/crates/l2/prover/src/guest_program/src/input.rs b/crates/l2/prover/src/guest_program/src/input.rs index 79f5b485b06..31bb3989843 100644 --- a/crates/l2/prover/src/guest_program/src/input.rs +++ b/crates/l2/prover/src/guest_program/src/input.rs @@ -1,4 +1,6 @@ -use ethrex_common::types::{Block, block_execution_witness::ExecutionWitness}; +use ethrex_common::types::{ + Block, block_execution_witness::ExecutionWitness, fee_config::FeeConfig, +}; use rkyv::{Archive, Deserialize as RDeserialize, Serialize as RSerialize}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -16,6 +18,8 @@ pub struct ProgramInput { pub execution_witness: ExecutionWitness, /// value used to calculate base fee pub elasticity_multiplier: u64, + /// Configuration for L2 fees + pub fee_config: Option, #[cfg(feature = "l2")] /// KZG commitment to the blob data #[serde_as(as = "[_; 48]")] @@ -32,6 +36,7 @@ impl Default for ProgramInput { blocks: Default::default(), execution_witness: ExecutionWitness::default(), elasticity_multiplier: Default::default(), + fee_config: None, #[cfg(feature = "l2")] blob_commitment: [0; 48], #[cfg(feature = "l2")] diff --git a/crates/l2/prover/src/prover.rs b/crates/l2/prover/src/prover.rs index 65c19cae99a..8894f3dcc98 100644 --- a/crates/l2/prover/src/prover.rs +++ b/crates/l2/prover/src/prover.rs @@ -130,6 +130,7 @@ impl Prover { blob_commitment: input.blob_commitment, #[cfg(feature = "l2")] blob_proof: input.blob_proof, + fee_config: Some(input.fee_config), }, })) } diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index 2a7be56c72a..9a58965d280 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -22,7 +22,7 @@ use serde::Serialize; use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, send_after, }; -use tracing::{debug, error, info}; +use tracing::{debug, error, info, warn}; use crate::{ BlockProducerConfig, SequencerConfig, @@ -83,9 +83,17 @@ impl BlockProducer { let BlockProducerConfig { block_time_ms, coinbase_address, + fee_vault_address, elasticity_multiplier, block_gas_limit, } = config; + + if fee_vault_address.is_some_and(|fee_vault| fee_vault == *coinbase_address) { + warn!( + "The coinbase address and fee vault address are the same. Coinbase balance behavior will be affected.", + ); + } + Self { store, blockchain, diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index a16890eaf04..2a09d982846 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -23,6 +23,7 @@ pub struct SequencerConfig { pub struct BlockProducerConfig { pub block_time_ms: u64, pub coinbase_address: Address, + pub fee_vault_address: Option
, pub elasticity_multiplier: u64, pub block_gas_limit: u64, } diff --git a/crates/l2/sequencer/proof_coordinator.rs b/crates/l2/sequencer/proof_coordinator.rs index 8eb0073106c..c360e6eb3e6 100644 --- a/crates/l2/sequencer/proof_coordinator.rs +++ b/crates/l2/sequencer/proof_coordinator.rs @@ -5,9 +5,10 @@ use crate::{ BlockProducerConfig, CommitterConfig, EthConfig, ProofCoordinatorConfig, SequencerConfig, }; use bytes::Bytes; -use ethrex_blockchain::Blockchain; +use ethrex_blockchain::{Blockchain, BlockchainType}; use ethrex_common::types::BlobsBundle; use ethrex_common::types::block_execution_witness::ExecutionWitness; +use ethrex_common::types::fee_config::FeeConfig; use ethrex_common::{ Address, types::{Block, blobs_bundle}, @@ -49,6 +50,7 @@ pub struct ProverInputData { #[cfg(feature = "l2")] #[serde_as(as = "[_; 48]")] pub blob_proof: blobs_bundle::Proof, + pub fee_config: FeeConfig, } /// Enum for the ProverServer <--> ProverClient Communication Protocol. @@ -501,6 +503,12 @@ impl ProofCoordinator { debug!("Created prover input for batch {batch_number}"); + let BlockchainType::L2(fee_config) = self.blockchain.options.r#type else { + return Err(ProofCoordinatorError::InternalError( + "Invalid blockchain type, expected L2".to_string(), + )); + }; + Ok(ProverInputData { execution_witness: witness, blocks, @@ -509,6 +517,7 @@ impl ProofCoordinator { blob_commitment, #[cfg(feature = "l2")] blob_proof, + fee_config, }) } diff --git a/crates/l2/tee/quote-gen/src/sender.rs b/crates/l2/tee/quote-gen/src/sender.rs index c67f503664e..a26c83cb14e 100644 --- a/crates/l2/tee/quote-gen/src/sender.rs +++ b/crates/l2/tee/quote-gen/src/sender.rs @@ -33,6 +33,7 @@ pub async fn get_batch(commit_hash: String) -> Result<(u64, ProgramInput), Strin blob_commitment: input.blob_commitment, #[cfg(feature = "l2")] blob_proof: input.blob_proof, + fee_config: Some(input.fee_config), }, )), _ => Err("No blocks to prove.".to_owned()), diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index a77f73d77a5..597409f0cd5 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -30,6 +30,7 @@ use ethrex_rpc::{ }; use hex::FromHexError; use secp256k1::SecretKey; +use std::ops::{Add, AddAssign}; use std::{ fs::{File, read_to_string}, io::{BufRead, BufReader}, @@ -55,6 +56,7 @@ use tokio::task::JoinSet; /// Contract addresses: /// ETHREX_WATCHER_BRIDGE_ADDRESS: The address of the l1 bridge contract /// INTEGRATION_TEST_PROPOSER_COINBASE_ADDRESS: The address of the l2 coinbase +/// INTEGRATION_TEST_PROPOSER_FEE_VAULT_ADDRESS: The address of the l2 fee_vault /// /// Test parameters: /// @@ -84,6 +86,13 @@ const DEFAULT_ON_CHAIN_PROPOSER_ADDRESS: Address = H160([ 0xc2, 0xdb, 0xc7, 0xe2, ]); +// 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 +// pk: 0xe9ea73e0ca433882aa9d4e2311ecc4e17286121e6bd8e600e5d25d4243b2baa3 +const DEFAULT_PROPOSER_FEE_VAULT_ADDRESS: Address = H160([ + 0x00, 0x0c, 0x0d, 0x6b, 0x7c, 0x45, 0x16, 0xa5, 0xb2, 0x74, 0xc5, 0x1e, 0xa3, 0x31, 0xa9, 0x41, + 0x0f, 0xe6, 0x91, 0x27, +]); + const L2_GAS_COST_MAX_DELTA: U256 = U256([100_000_000_000_000, 0, 0, 0]); const DEFAULT_RICH_KEYS_FILE_PATH: &str = "../../fixtures/keys/private_keys_l1.txt"; @@ -104,7 +113,7 @@ async fn l2_integration_test() -> Result<(), Box> { let native_token_l1_address = std::env::var("ETHREX_NATIVE_TOKEN_L1_ADDRESS") .map(|address| address.parse().expect("Invalid native token L1 address")) .unwrap_or(Address::zero()); - // Not thread-safe (fee vault and bridge balance checks). + // Not thread-safe (coinbase and bridge balance checks). test_deposit( &l1_client, &l2_client, @@ -113,8 +122,11 @@ async fn l2_integration_test() -> Result<(), Box> { ) .await?; + let coinbase_balance_before_tests = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) + .await?; let fee_vault_balance_before_tests = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .get_balance(fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; let mut set = JoinSet::new(); @@ -190,25 +202,39 @@ async fn l2_integration_test() -> Result<(), Box> { private_keys.pop().unwrap(), )); - let mut recoverable_fees = U256::zero(); + let mut acc_priority_fees = U256::zero(); + let mut acc_base_fees = U256::zero(); while let Some(res) = set.join_next().await { - let fee = res??; - recoverable_fees += fee; + let fees_details = res??; + acc_priority_fees += fees_details.priority_fees; + acc_base_fees += fees_details.total_fees - fees_details.priority_fees; } + let coinbase_balance_after_tests = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) + .await?; + let fee_vault_balance_after_tests = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .get_balance(fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; - println!("Checking fee vault balance"); + println!("Checking coinbase and fee vault balances"); assert_eq!( - fee_vault_balance_after_tests, - fee_vault_balance_before_tests + recoverable_fees, - "Fee vault is not correct after tests" + coinbase_balance_after_tests, + coinbase_balance_before_tests + acc_priority_fees, + "Coinbase is not correct after tests" ); - // Not thread-safe (fee vault and bridge balance checks) + if std::env::var("INTEGRATION_TEST_SKIP_FEE_VAULT_CHECK").is_err() { + assert_eq!( + fee_vault_balance_after_tests, + fee_vault_balance_before_tests + acc_base_fees, + "Fee vault is not correct after tests" + ); + } + + // Not thread-safe (coinbase and bridge balance checks) test_n_withdraws( &l1_client, &l2_client, @@ -233,7 +259,7 @@ async fn l2_integration_test() -> Result<(), Box> { /// 2. Deploys the new implementation on L2 /// 3. Calls the upgrade function on the L1 bridge contract /// 4. Checks that the implementation address has changed -async fn test_upgrade(l1_client: EthClient, l2_client: EthClient) -> Result { +async fn test_upgrade(l1_client: EthClient, l2_client: EthClient) -> Result { println!("Testing upgrade"); let bridge_owner_private_key = bridge_owner_private_key(); @@ -259,7 +285,7 @@ async fn test_upgrade(l1_client: EthClient, l2_client: EthClient) -> Result Result {final_impl:#x}"); assert_ne!(initial_impl, final_impl); - Ok(deploy_recoverable_fees) + Ok(fees_details) } /// In this test we deploy a contract on L2 and call it from L1 using the CommonBridge contract. @@ -317,7 +343,7 @@ async fn test_privileged_tx_with_contract_call( l1_client: EthClient, l2_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { // pragma solidity ^0.8.27; // contract Test { // event NumberSet(uint256 indexed number); @@ -331,7 +357,7 @@ async fn test_privileged_tx_with_contract_call( println!("ptx_with_contract_call: Deploying contract on L2"); - let (deployed_contract_address, recoverable_fees) = test_deploy( + let (deployed_contract_address, fees_details) = test_deploy( &l2_client, &init_code, &rich_wallet_private_key, @@ -406,7 +432,7 @@ async fn test_privileged_tx_with_contract_call( "ptx_with_contract_call: Event emitted with wrong value. Expected 424242, got {number_emitted}" ); - Ok(recoverable_fees) + Ok(fees_details) } /// Test the deployment of a contract on L2 and call it from L1 using the CommonBridge contract. @@ -415,7 +441,7 @@ async fn test_privileged_tx_with_contract_call_revert( l1_client: EthClient, l2_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { // pragma solidity ^0.8.27; // contract RevertTest { // function revert_call() public { @@ -428,7 +454,7 @@ async fn test_privileged_tx_with_contract_call_revert( println!("ptx_with_contract_call_revert: Deploying contract on L2"); - let (deployed_contract_address, recoverable_fees) = test_deploy( + let (deployed_contract_address, fees_details) = test_deploy( &l2_client, &init_code, &rich_wallet_private_key, @@ -450,7 +476,7 @@ async fn test_privileged_tx_with_contract_call_revert( ) .await?; - Ok(recoverable_fees) + Ok(fees_details) } async fn find_withdrawal_with_widget( @@ -480,7 +506,7 @@ async fn test_erc20_roundtrip( l1_client: EthClient, l2_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { let token_amount: U256 = U256::from(100); let rich_wallet_signer: Signer = LocalSigner::new(rich_wallet_private_key).into(); @@ -674,7 +700,7 @@ async fn test_erc20_roundtrip( let l2_final_balance = test_balance_of(&l2_client, token_l2, rich_address).await; assert_eq!(initial_balance, l1_final_balance); assert!(l2_final_balance.is_zero()); - Ok(deploy_fees + approve_fees.recoverable_fees + withdraw_fees.recoverable_fees) + Ok(deploy_fees + approve_fees + withdraw_fees) } /// Tests that the aliasing is done correctly when calling from L1 to L2 @@ -685,7 +711,7 @@ async fn test_aliasing( l1_client: EthClient, l2_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { println!("Testing aliasing"); let init_code_l1 = hex::decode(std::fs::read("../../fixtures/contracts/caller/Caller.bin")?)?; let caller_l1 = test_deploy_l1(&l1_client, &init_code_l1, &rich_wallet_private_key).await?; @@ -724,7 +750,7 @@ async fn test_aliasing( receipt_l2.tx_info.from ); assert_eq!(receipt_l2.tx_info.from, get_address_alias(caller_l1)); - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Tests that a failed deposit can be withdrawn back to L1 @@ -735,7 +761,7 @@ async fn test_erc20_failed_deposit( l1_client: EthClient, l2_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { let token_amount: U256 = U256::from(100); let rich_wallet_signer: Signer = LocalSigner::new(rich_wallet_private_key).into(); @@ -815,7 +841,7 @@ async fn test_erc20_failed_deposit( wait_for_transaction_receipt(withdraw_claim_tx, &l1_client, 5).await?; let l1_final_balance = test_balance_of(&l1_client, token_l1, rich_address).await; assert_eq!(initial_balance, l1_final_balance); - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Tests that a withdrawal can be triggered by a privileged transaction @@ -825,7 +851,7 @@ async fn test_forced_withdrawal( l2_client: EthClient, rich_wallet_private_key: SecretKey, native_token_l1_address: Address, -) -> Result { +) -> Result { let native_token_is_eth = native_token_l1_address == Address::zero(); println!("forced_withdrawal: Testing forced withdrawal"); let rich_address = @@ -963,7 +989,7 @@ async fn test_forced_withdrawal( assert_eq!(initial_l1_eth_balance - l1_gas_costs, l1_final_eth_balance); } assert_eq!(l2_initial_balance - transfer_value, l2_final_balance); - Ok(U256::zero()) + Ok(FeesDetails::default()) } async fn test_balance_of(client: &EthClient, token: Address, user: Address) -> U256 { @@ -1007,7 +1033,7 @@ async fn test_send( } /// Test depositing ETH from L1 to L2 -/// 1. Fetch initial balances of depositor on L1, recipient on L2, bridge on L1 and fee vault on L2. +/// 1. Fetch initial balances of depositor on L1, recipient on L2, bridge on L1 and coinbase on L2. /// 2. Perform deposit from L1 to L2 /// 3. Check final balances. async fn test_deposit( @@ -1056,8 +1082,8 @@ async fn test_deposit( test_balance_of(l1_client, native_token_l1_address, bridge_address()?).await }; - let fee_vault_balance_before_deposit = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + let coinbase_balance_before_deposit = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; println!("test_deposit: Depositing funds from L1 to L2"); @@ -1175,13 +1201,13 @@ async fn test_deposit( "Deposit recipient L2 balance didn't increase as expected after deposit" ); - let fee_vault_balance_after_deposit = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + let coinbase_balance_after_deposit = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; assert_eq!( - fee_vault_balance_after_deposit, fee_vault_balance_before_deposit, - "Fee vault balance should not change after deposit" + coinbase_balance_after_deposit, coinbase_balance_before_deposit, + "Coinbase balance should not change after deposit" ); Ok(()) @@ -1190,7 +1216,7 @@ async fn test_deposit( async fn test_privileged_spammer( l1_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { let init_code_l1 = hex::decode(std::fs::read( "../../fixtures/contracts/deposit_spammer/DepositSpammer.bin", )?)?; @@ -1206,7 +1232,7 @@ async fn test_privileged_spammer( ) .await?; } - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Test transferring ETH on L2 @@ -1218,7 +1244,7 @@ async fn test_transfer( l2_client: EthClient, transferer_private_key: SecretKey, returnerer_private_key: SecretKey, -) -> Result { +) -> Result { println!("test_transfer: Transferring funds on L2"); let transferer_address = get_address_from_secret_key(&transferer_private_key).unwrap(); let returner_address = get_address_from_secret_key(&returnerer_private_key).unwrap(); @@ -1227,7 +1253,7 @@ async fn test_transfer( "test_transfer: Performing transfer from {transferer_address:#x} to {returner_address:#x}" ); - let mut recoverable_fees = perform_transfer( + let mut fees_details = perform_transfer( &l2_client, &transferer_private_key, returner_address, @@ -1241,7 +1267,7 @@ async fn test_transfer( "test_transfer: Performing return transfer from {returner_address:#x} to {transferer_address:#x} with amount {return_amount}" ); - recoverable_fees += perform_transfer( + fees_details += perform_transfer( &l2_client, &returnerer_private_key, transferer_address, @@ -1249,7 +1275,7 @@ async fn test_transfer( ) .await?; - Ok(recoverable_fees) + Ok(fees_details) } /// Test transferring ETH on L2 through a privileged transaction (deposit from L1) @@ -1261,7 +1287,7 @@ async fn test_transfer_with_privileged_tx( l2_client: EthClient, transferer_private_key: SecretKey, receiver_private_key: SecretKey, -) -> Result { +) -> Result { println!("transfer_with_ptx: Transferring funds on L2 through a deposit"); let transferer_address = get_address_from_secret_key(&transferer_private_key).unwrap(); let receiver_address = get_address_from_secret_key(&receiver_private_key).unwrap(); @@ -1309,7 +1335,7 @@ async fn test_transfer_with_privileged_tx( receiver_balance_after, receiver_balance_before + transfer_value() ); - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Test that gas is burned from the L1 account when making a deposit with a specified L2 gas limit. @@ -1318,7 +1344,7 @@ async fn test_transfer_with_privileged_tx( async fn test_gas_burning( l1_client: EthClient, rich_wallet_private_key: SecretKey, -) -> Result { +) -> Result { println!("test_gas_burning: Transferring funds on L2 through a deposit"); let rich_address = get_address_from_secret_key(&rich_wallet_private_key).unwrap(); let l2_gas_limit = 2_000_000; @@ -1342,7 +1368,7 @@ async fn test_gas_burning( assert!(l1_to_l2_tx_receipt.receipt.status); assert!(l1_to_l2_tx_receipt.tx_info.gas_used > l2_gas_limit); assert!(l1_to_l2_tx_receipt.tx_info.gas_used < l2_gas_limit + l1_extra_gas_limit); - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Test transferring ETH on L2 through a privileged transaction (deposit from L1) with insufficient balance @@ -1354,7 +1380,7 @@ async fn test_privileged_tx_not_enough_balance( l2_client: EthClient, rich_wallet_private_key: SecretKey, receiver_private_key: SecretKey, -) -> Result { +) -> Result { println!( "ptx_not_enough_balance: Starting test for privileged transaction with insufficient balance" ); @@ -1406,7 +1432,7 @@ async fn test_privileged_tx_not_enough_balance( .get_balance(receiver_address, BlockIdentifier::Tag(BlockTag::Latest)) .await?; assert_eq!(balance_after, balance_before); - Ok(U256::zero()) + Ok(FeesDetails::default()) } /// Test helper @@ -1418,7 +1444,7 @@ async fn perform_transfer( transferer_private_key: &SecretKey, transfer_recipient_address: Address, transfer_value: U256, -) -> Result { +) -> Result { let transferer_address = get_address_from_secret_key(transferer_private_key).unwrap(); let transferer_initial_l2_balance = l2_client @@ -1484,7 +1510,7 @@ async fn perform_transfer( let transfer_fees = get_fees_details_l2(&transfer_tx_receipt, l2_client).await; - Ok(transfer_fees.recoverable_fees) + Ok(transfer_fees) } async fn test_n_withdraws( @@ -1533,8 +1559,8 @@ async fn test_n_withdraws( test_balance_of(l1_client, native_token_l1_address, withdrawer_address).await }; - let fee_vault_balance_before_withdrawal = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + let coinbase_balance_before_withdrawal = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; println!("test_n_withdraws: Withdrawing funds from L2 to L1"); @@ -1606,21 +1632,19 @@ async fn test_n_withdraws( "Withdrawer L1 balance should not change after withdrawal" ); - let fee_vault_balance_after_withdrawal = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + let coinbase_balance_after_withdrawal = l2_client + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; - let mut recoverable_fees = U256::zero(); + let mut priority_fees = U256::zero(); for receipt in receipts { - recoverable_fees += get_fees_details_l2(&receipt, l2_client) - .await - .recoverable_fees; + priority_fees += get_fees_details_l2(&receipt, l2_client).await.priority_fees; } assert_eq!( - fee_vault_balance_after_withdrawal, - fee_vault_balance_before_withdrawal + recoverable_fees, - "Fee vault balance didn't increase as expected after withdrawal" + coinbase_balance_after_withdrawal, + coinbase_balance_before_withdrawal + priority_fees, + "Coinbase balance didn't increase as expected after withdrawal" ); // We need to wait for all the txs to be included in some batch @@ -1733,17 +1757,22 @@ async fn test_total_balance_l2( .expect("Failed to get rich accounts balance"); let coinbase_balance = l2_client - .get_balance(fees_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .get_balance(coinbase(), BlockIdentifier::Tag(BlockTag::Latest)) .await?; println!("Coinbase balance: {coinbase_balance}"); - let total_balance_on_l2 = rich_accounts_balance + coinbase_balance; + let fee_vault_balance = l2_client + .get_balance(fee_vault(), BlockIdentifier::Tag(BlockTag::Latest)) + .await?; + + println!("Fee vault balance: {fee_vault_balance}"); + + let total_balance_on_l2 = rich_accounts_balance + coinbase_balance + fee_vault_balance; println!( - "Total balance on L2: {rich_accounts_balance} + {coinbase_balance} = {total_balance_on_l2}" + "Total balance on L2: {rich_accounts_balance} + {coinbase_balance} + {fee_vault_balance} = {total_balance_on_l2}" ); - println!("Checking native tokens locked on CommonBridge"); let bridge_address = bridge_address()?; @@ -1757,10 +1786,17 @@ async fn test_total_balance_l2( println!("Bridge has locked: {bridge_native_locked}"); - assert!( - total_balance_on_l2 <= bridge_native_locked, - "Total balance on L2 ({total_balance_on_l2}) is greater than the assets locked by the bridge ({bridge_native_locked})" - ); + if std::env::var("INTEGRATION_TEST_SKIP_FEE_VAULT_CHECK").is_err() { + assert!( + total_balance_on_l2 == bridge_native_locked, + "Total balance on L2 ({total_balance_on_l2}) differs from bridge native locked ({bridge_native_locked})" + ); + } else { + assert!( + total_balance_on_l2 < bridge_native_locked, + "Total balance on L2 ({total_balance_on_l2}) is greater than the assets locked by the bridge ({bridge_native_locked})" + ); + } Ok(()) } @@ -1774,7 +1810,7 @@ async fn test_deploy( init_code: &[u8], deployer_private_key: &SecretKey, test_name: &str, -) -> Result<(Address, U256)> { +) -> Result<(Address, FeesDetails)> { println!("{test_name}: Deploying contract on L2"); let deployer: Signer = LocalSigner::new(*deployer_private_key).into(); @@ -1820,7 +1856,7 @@ async fn test_deploy( "{test_name}: Deployed contract balance should be zero after deploy" ); - Ok((contract_address, deploy_fees.recoverable_fees)) + Ok((contract_address, deploy_fees)) } async fn test_deploy_l1( @@ -1843,7 +1879,7 @@ async fn test_deploy_l1( Ok(contract_address) } -/// Fee vault must be 0 +/// Coinbase must be 0 async fn test_call_to_contract_with_deposit( l1_client: &EthClient, l2_client: &EthClient, @@ -1932,10 +1968,28 @@ fn bridge_owner_private_key() -> SecretKey { SecretKey::from_slice(l1_rich_wallet_pk.as_bytes()).unwrap() } -#[derive(Debug)] +#[derive(Debug, Default)] struct FeesDetails { total_fees: U256, - recoverable_fees: U256, + priority_fees: U256, +} + +impl Add for FeesDetails { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + total_fees: self.total_fees + other.total_fees, + priority_fees: self.priority_fees + other.priority_fees, + } + } +} + +impl AddAssign for FeesDetails { + fn add_assign(&mut self, other: Self) { + self.total_fees += other.total_fees; + self.priority_fees += other.priority_fees; + } } async fn get_fees_details_l2(tx_receipt: &RpcReceipt, l2_client: &EthClient) -> FeesDetails { @@ -1956,11 +2010,11 @@ async fn get_fees_details_l2(tx_receipt: &RpcReceipt, l2_client: &EthClient) -> let max_priority_fee_per_gas_transfer: U256 = (effective_gas_price - base_fee_per_gas).into(); - let recoverable_fees = max_priority_fee_per_gas_transfer.mul(tx_receipt.tx_info.gas_used); + let priority_fees = max_priority_fee_per_gas_transfer.mul(tx_receipt.tx_info.gas_used); FeesDetails { total_fees, - recoverable_fees, + priority_fees, } } @@ -1974,12 +2028,18 @@ fn l2_client() -> EthClient { .unwrap() } -fn fees_vault() -> Address { +fn coinbase() -> Address { std::env::var("INTEGRATION_TEST_PROPOSER_COINBASE_ADDRESS") .map(|address| address.parse().expect("Invalid proposer coinbase address")) .unwrap_or(DEFAULT_PROPOSER_COINBASE_ADDRESS) } +fn fee_vault() -> Address { + std::env::var("INTEGRATION_TEST_PROPOSER_FEE_VAULT_ADDRESS") + .map(|address| address.parse().expect("Invalid proposer coinbase address")) + .unwrap_or(DEFAULT_PROPOSER_FEE_VAULT_ADDRESS) +} + async fn wait_for_l2_deposit_receipt( rpc_receipt: &RpcReceipt, l1_client: &EthClient, diff --git a/crates/networking/rpc/clients/eth/errors.rs b/crates/networking/rpc/clients/eth/errors.rs index f51ea3eb0b5..52d92b8c33f 100644 --- a/crates/networking/rpc/clients/eth/errors.rs +++ b/crates/networking/rpc/clients/eth/errors.rs @@ -69,6 +69,8 @@ pub enum EthClientError { GenericTransactionError(#[from] GenericTransactionError), #[error("Failed to parse hex string: {0}")] FromStrRadixError(#[from] FromStrRadixErr), + #[error("ethrex_getFeeVaultAddress request error: {0}")] + GetFeeVaultAddressError(#[from] GetFeeVaultAddressError), } #[derive(Debug, thiserror::Error)] @@ -302,3 +304,11 @@ pub enum GetBatchByNumberError { #[error("{0}")] RPCError(String), } + +#[derive(Debug, thiserror::Error)] +pub enum GetFeeVaultAddressError { + #[error("{0}")] + SerdeJSONError(#[from] serde_json::Error), + #[error("{0}")] + RPCError(String), +} diff --git a/crates/vm/backends/levm/mod.rs b/crates/vm/backends/levm/mod.rs index 2ecec009d21..926469a8425 100644 --- a/crates/vm/backends/levm/mod.rs +++ b/crates/vm/backends/levm/mod.rs @@ -73,7 +73,7 @@ impl LEVM { // in L2 execution, but its implementation behaves differently based on this. let requests = match vm_type { VMType::L1 => extract_all_requests_levm(&receipts, db, &block.header, vm_type)?, - VMType::L2 => Default::default(), + VMType::L2(_) => Default::default(), }; Ok(BlockExecutionResult { receipts, requests }) @@ -193,7 +193,7 @@ impl LEVM { db: &mut GeneralizedDatabase, vm_type: VMType, ) -> Result<(), EvmError> { - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Err(EvmError::InvalidEVM( "beacon_root_contract_call should not be called for L2 VM".to_string(), )); @@ -219,7 +219,7 @@ impl LEVM { db: &mut GeneralizedDatabase, vm_type: VMType, ) -> Result<(), EvmError> { - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Err(EvmError::InvalidEVM( "process_block_hash_history should not be called for L2 VM".to_string(), )); @@ -240,7 +240,7 @@ impl LEVM { db: &mut GeneralizedDatabase, vm_type: VMType, ) -> Result { - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Err(EvmError::InvalidEVM( "read_withdrawal_requests should not be called for L2 VM".to_string(), )); @@ -269,7 +269,7 @@ impl LEVM { db: &mut GeneralizedDatabase, vm_type: VMType, ) -> Result { - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Err(EvmError::InvalidEVM( "dequeue_consolidation_requests should not be called for L2 VM".to_string(), )); @@ -332,7 +332,7 @@ impl LEVM { let fork = chain_config.fork(block_header.timestamp); // TODO: I don't like deciding the behavior based on the VMType here. - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Ok(()); } @@ -434,7 +434,7 @@ pub fn extract_all_requests_levm( header: &BlockHeader, vm_type: VMType, ) -> Result, EvmError> { - if let VMType::L2 = vm_type { + if let VMType::L2(_) = vm_type { return Err(EvmError::InvalidEVM( "extract_all_requests_levm should not be called for L2 VM".to_string(), )); diff --git a/crates/vm/backends/mod.rs b/crates/vm/backends/mod.rs index edc3b450e57..e954d0664d3 100644 --- a/crates/vm/backends/mod.rs +++ b/crates/vm/backends/mod.rs @@ -4,12 +4,12 @@ use levm::LEVM; use crate::db::{DynVmDatabase, VmDatabase}; use crate::errors::EvmError; use crate::execution_result::ExecutionResult; -use ethrex_common::Address; use ethrex_common::types::requests::Requests; use ethrex_common::types::{ AccessList, AccountUpdate, Block, BlockHeader, Fork, GenericTransaction, Receipt, Transaction, Withdrawal, }; +use ethrex_common::{Address, types::fee_config::FeeConfig}; pub use ethrex_levm::call_frame::CallFrameBackup; use ethrex_levm::db::Database as LevmDatabase; use ethrex_levm::db::gen_db::GeneralizedDatabase; @@ -39,12 +39,15 @@ impl Evm { } } - pub fn new_for_l2(_db: impl VmDatabase + 'static) -> Result { - let wrapped_db: DynVmDatabase = Box::new(_db); + pub fn new_for_l2( + db: impl VmDatabase + 'static, + fee_config: FeeConfig, + ) -> Result { + let wrapped_db: DynVmDatabase = Box::new(db); let evm = Evm { db: GeneralizedDatabase::new(Arc::new(wrapped_db)), - vm_type: VMType::L2, + vm_type: VMType::L2(fee_config), }; Ok(evm) @@ -54,8 +57,11 @@ impl Evm { Self::_new_from_db(store, VMType::L1) } - pub fn new_from_db_for_l2(store: Arc) -> Self { - Self::_new_from_db(store, VMType::L2) + pub fn new_from_db_for_l2( + store: Arc, + fee_config: FeeConfig, + ) -> Self { + Self::_new_from_db(store, VMType::L2(fee_config)) } fn _new_from_db(store: Arc, vm_type: VMType) -> Self { diff --git a/crates/vm/levm/src/environment.rs b/crates/vm/levm/src/environment.rs index b1bc1818fab..0a7d1c81cb2 100644 --- a/crates/vm/levm/src/environment.rs +++ b/crates/vm/levm/src/environment.rs @@ -21,7 +21,7 @@ pub struct Environment { pub gas_limit: u64, pub config: EVMConfig, pub block_number: U256, - /// Coinbase is the block's beneficiary - the address that receives the block rewards and fees. + /// Coinbase is the block's beneficiary - the address that receives the block rewards (priority fees). pub coinbase: Address, pub timestamp: U256, pub prev_randao: Option, diff --git a/crates/vm/levm/src/hooks/hook.rs b/crates/vm/levm/src/hooks/hook.rs index 023a2a5ada4..d79c4fb5a79 100644 --- a/crates/vm/levm/src/hooks/hook.rs +++ b/crates/vm/levm/src/hooks/hook.rs @@ -1,10 +1,10 @@ -use std::{cell::RefCell, rc::Rc}; - use crate::{ errors::{ContextResult, VMError}, hooks::{L2Hook, backup_hook::BackupHook, default_hook::DefaultHook}, vm::{VM, VMType}, }; +use ethrex_common::types::fee_config::FeeConfig; +use std::{cell::RefCell, rc::Rc}; pub trait Hook { fn prepare_execution(&mut self, vm: &mut VM<'_>) -> Result<(), VMError>; @@ -19,7 +19,7 @@ pub trait Hook { pub fn get_hooks(vm_type: &VMType) -> Vec>> { match vm_type { VMType::L1 => l1_hooks(), - VMType::L2 => l2_hooks(), + VMType::L2(fee_config) => l2_hooks(*fee_config), } } @@ -27,9 +27,9 @@ pub fn l1_hooks() -> Vec>> { vec![Rc::new(RefCell::new(DefaultHook))] } -pub fn l2_hooks() -> Vec>> { +pub fn l2_hooks(fee_config: FeeConfig) -> Vec>> { vec![ - Rc::new(RefCell::new(L2Hook {})), + Rc::new(RefCell::new(L2Hook { fee_config })), 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 3da32ee6882..46f52660a45 100644 --- a/crates/vm/levm/src/hooks/l2_hook.rs +++ b/crates/vm/levm/src/hooks/l2_hook.rs @@ -5,14 +5,16 @@ use crate::{ vm::VM, }; -use ethrex_common::{Address, H160, U256}; +use ethrex_common::{Address, H160, U256, types::fee_config::FeeConfig}; pub const COMMON_BRIDGE_L2_ADDRESS: Address = H160([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, ]); -pub struct L2Hook {} +pub struct L2Hook { + pub fee_config: FeeConfig, +} impl Hook for L2Hook { fn prepare_execution(&mut self, vm: &mut VM<'_>) -> Result<(), crate::errors::VMError> { @@ -107,7 +109,9 @@ impl Hook for L2Hook { ctx_result: &mut ContextResult, ) -> Result<(), crate::errors::VMError> { if !vm.env.is_privileged { - return DefaultHook.finalize_execution(vm, ctx_result); + DefaultHook.finalize_execution(vm, ctx_result)?; + // Different from L1, the base fee is not burned + return pay_to_fee_vault(vm, ctx_result.gas_used, self.fee_config.fee_vault); } if !ctx_result.is_success() && vm.env.origin != COMMON_BRIDGE_L2_ADDRESS { @@ -121,3 +125,21 @@ impl Hook for L2Hook { Ok(()) } } + +fn pay_to_fee_vault( + vm: &mut VM<'_>, + gas_to_pay: u64, + fee_vault: Option
, +) -> Result<(), crate::errors::VMError> { + let Some(fee_vault) = fee_vault else { + // No fee vault configured, base fee is effectively burned + return Ok(()); + }; + + let base_fee = U256::from(gas_to_pay) + .checked_mul(vm.env.base_fee_per_gas) + .ok_or(InternalError::Overflow)?; + + vm.increase_account_balance(fee_vault, base_fee)?; + Ok(()) +} diff --git a/crates/vm/levm/src/precompiles.rs b/crates/vm/levm/src/precompiles.rs index 8ace41bd1d2..e4ad1c0f80b 100644 --- a/crates/vm/levm/src/precompiles.rs +++ b/crates/vm/levm/src/precompiles.rs @@ -302,7 +302,7 @@ pub fn precompiles_for_fork(fork: Fork) -> impl Iterator { } pub fn is_precompile(address: &Address, fork: Fork, vm_type: VMType) -> bool { - (matches!(vm_type, VMType::L2) && *address == P256_VERIFICATION.address) + (matches!(vm_type, VMType::L2(_)) && *address == P256_VERIFICATION.address) || precompiles_for_fork(fork).any(|precompile| precompile.address == *address) } diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index 1679117e74f..cc1e877e958 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -20,7 +20,7 @@ use bytes::Bytes; use ethrex_common::{ Address, H160, H256, U256, tracing::CallType, - types::{AccessListEntry, Fork, Log, Transaction}, + types::{AccessListEntry, Fork, Log, Transaction, fee_config::FeeConfig}, }; use std::{ cell::RefCell, @@ -35,7 +35,7 @@ pub type Storage = HashMap; pub enum VMType { #[default] L1, - L2, + L2(FeeConfig), } /// Information that changes during transaction execution. diff --git a/docs/CLI.md b/docs/CLI.md index 4c091d4f4d5..f3735d60790 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -138,17 +138,19 @@ Commands: blobs-saver Launch a server that listens for Blobs submissions and saves them offline. reconstruct Reconstructs the L2 state from L1 blobs. revert-batch Reverts unverified batches. + pause Pause L1 contracts + unpause Unpause L1 contracts deploy Deploy in L1 all contracts needed by an L2. help Print this message or the help of the given subcommand(s) Options: -t, --tick-rate time in ms between two ticks - + [default: 1000] --batch-widget-height - + -h, --help Print help (see a summary with '-h') @@ -156,14 +158,14 @@ Options: Node options: --network Alternatively, the name of a known network can be provided instead to use its preset genesis file and include its preset bootnodes. The networks currently supported include holesky, sepolia, hoodi and mainnet. If not specified, defaults to mainnet. - + [env: ETHREX_NETWORK=] --datadir If the datadir is the word `memory`, ethrex will use the `InMemory Engine`. - + [env: ETHREX_DATADIR=] - [default: /home/runner/.local/share/ethrex] + [default: "/home/runner/.local/share/ethrex"] --force Delete the database without confirmation. @@ -183,7 +185,7 @@ Node options: --log.level Possible values: info, debug, trace, warn, error - + [default: INFO] P2P options: @@ -192,53 +194,54 @@ P2P options: --syncmode Can be either "full" or "snap" with "full" as default value. - + [default: full] --p2p.enabled + --p2p.port - TCP port for P2P. - + TCP port for P2P protocol. + [default: 30303] --discovery.port UDP port for P2P discovery. - + [default: 30303] RPC options: --http.addr
Listening address for the http rpc server. - + [env: ETHREX_HTTP_ADDR=] - [default: localhost] + [default: 0.0.0.0] --http.port Listening port for the http rpc server. - + [env: ETHREX_HTTP_PORT=] [default: 8545] --authrpc.addr
Listening address for the authenticated rpc server. - - [default: localhost] + + [default: 127.0.0.1] --authrpc.port Listening port for the authenticated rpc server. - + [default: 8551] --authrpc.jwtsecret Receives the jwt secret used for authenticated rpc requests. - + [default: jwt.hex] Eth options: --eth.rpc-url ... List of rpc urls to use. - + [env: ETHREX_ETH_RPC_URL=] --eth.maximum-allowed-max-fee-per-gas @@ -271,9 +274,9 @@ L1 Watcher options: --watcher.watch-interval How often the L1 watcher checks for new blocks in milliseconds. - + [env: ETHREX_WATCHER_WATCH_INTERVAL=] - [default: 1000] + [default: 12000] --watcher.max-block-step [env: ETHREX_WATCHER_MAX_BLOCK_STEP=] @@ -281,20 +284,29 @@ L1 Watcher options: --watcher.block-delay Number of blocks the L1 watcher waits before trusting an L1 block. - + [env: ETHREX_WATCHER_BLOCK_DELAY=] [default: 10] Block producer options: --block-producer.block-time How often does the sequencer produce new blocks to the L1 in milliseconds. - + [env: ETHREX_BLOCK_PRODUCER_BLOCK_TIME=] [default: 5000] --block-producer.coinbase-address
[env: ETHREX_BLOCK_PRODUCER_COINBASE_ADDRESS=] + --block-producer.fee-vault-address
+ [env: ETHREX_BLOCK_PRODUCER_FEE_VAULT_ADDRESS=] + + --block-producer.block-gas-limit + Maximum gas limit for the L2 blocks. + + [env: ETHREX_BLOCK_PRODUCER_BLOCK_GAS_LIMIT=] + [default: 30000000] + Proposer options: --elasticity-multiplier [env: ETHREX_PROPOSER_ELASTICITY_MULTIPLIER=] @@ -303,17 +315,17 @@ Proposer options: L1 Committer options: --committer.l1-private-key Private key of a funded account that the sequencer will use to send commit txs to the L1. - + [env: ETHREX_COMMITTER_L1_PRIVATE_KEY=] --committer.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_URL=] --committer.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_PUBLIC_KEY=] --l1.on-chain-proposer-address
@@ -321,10 +333,20 @@ L1 Committer options: --committer.commit-time How often does the sequencer commit new blocks to the L1 in milliseconds. - + [env: ETHREX_COMMITTER_COMMIT_TIME=] [default: 60000] + --committer.batch-gas-limit + Maximum gas limit for the batch + + [env: ETHREX_COMMITTER_BATCH_GAS_LIMIT=] + + --committer.first-wake-up-time + Time to wait before the sequencer seals a batch when started. After committing the first batch, `committer.commit-time` will be used. + + [env: ETHREX_COMMITTER_FIRST_WAKE_UP_TIME=] + --committer.arbitrary-base-blob-gas-price [env: ETHREX_COMMITTER_ARBITRARY_BASE_BLOB_GAS_PRICE=] [default: 1000000000] @@ -332,27 +354,27 @@ L1 Committer options: Proof coordinator options: --proof-coordinator.l1-private-key Private key of of a funded account that the sequencer will use to send verify txs to the L1. Has to be a different account than --committer-l1-private-key. - + [env: ETHREX_PROOF_COORDINATOR_L1_PRIVATE_KEY=] --proof-coordinator.tdx-private-key Private key of of a funded account that the TDX tool that will use to send the tdx attestation to L1. - + [env: ETHREX_PROOF_COORDINATOR_TDX_PRIVATE_KEY=] --proof-coordinator.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_URL=] --proof-coordinator.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_PUBLIC_KEY=] --proof-coordinator.addr Set it to 0.0.0.0 to allow connections from other machines. - + [env: ETHREX_PROOF_COORDINATOR_LISTEN_ADDRESS=] [default: 127.0.0.1] @@ -362,13 +384,10 @@ Proof coordinator options: --proof-coordinator.send-interval How often does the proof coordinator send proofs to the L1 in milliseconds. - + [env: ETHREX_PROOF_COORDINATOR_SEND_INTERVAL=] [default: 5000] - --proof-coordinator.dev-mode - [env: ETHREX_PROOF_COORDINATOR_DEV_MODE=] - Based options: --state-updater.sequencer-registry
[env: ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=] @@ -398,30 +417,39 @@ Aligned options: --aligned.beacon-url ... List of beacon urls to use. - + [env: ETHREX_ALIGNED_BEACON_URL=] --aligned-network L1 network name for Aligned sdk - + [env: ETHREX_ALIGNED_NETWORK=] [default: devnet] --aligned.fee-estimate Fee estimate for Aligned sdk - + [env: ETHREX_ALIGNED_FEE_ESTIMATE=] [default: instant] --aligned-sp1-elf-path Path to the SP1 elf. This is used for proof verification. - + [env: ETHREX_ALIGNED_SP1_ELF_PATH=] +Admin server options: + --admin-server.addr + [env: ETHREX_ADMIN_SERVER_LISTEN_ADDRESS=] + [default: 127.0.0.1] + + --admin-server.port + [env: ETHREX_ADMIN_SERVER_LISTEN_PORT=] + [default: 5555] + L2 options: --validium If true, L2 will run on validium mode as opposed to the default rollup mode, meaning it will not publish state diffs to the L1. - + [env: ETHREX_L2_VALIDIUM=] --sponsorable-addresses @@ -429,13 +457,14 @@ L2 options: --sponsor-private-key The private key of ethrex L2 transactions sponsor. - + [env: SPONSOR_PRIVATE_KEY=] [default: 0xffd790338a2798b648806fc8635ac7bf14af15425fed0c8f25bcc5febaa9b192] Monitor options: --no-monitor [env: ETHREX_NO_MONITOR=] + ``` ## ethrex l2 prover diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 68f7e082f32..fb9b31b0c4c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -52,6 +52,7 @@ - [CommonBridge]() - [L1MessageSender]() - [Based sequencing](./l2/fundamentals/based.md) + - [Transaction fees](./l2/fundamentals/transaction_fees.md) # Ethrex for developers diff --git a/docs/l2/architecture/aligned_mode.md b/docs/l2/architecture/aligned_mode.md index a1f921e95ac..6690ed88965 100644 --- a/docs/l2/architecture/aligned_mode.md +++ b/docs/l2/architecture/aligned_mode.md @@ -221,7 +221,7 @@ cargo run deposit-to-batcher \ ``` cd ethrex/crates/l2 -cargo run --release --manifest-path ../../Cargo.toml --bin ethrex --features "l2" -- l2 --watcher.block-delay 0 --network ../../fixtures/genesis/l2.json --http.port 1729 --http.addr 0.0.0.0 --datadir dev_ethrex_l2 --l1.bridge-address --l1.on-chain-proposer-address --eth.rpc-url http://localhost:8545 --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d --proof-coordinator.addr 127.0.0.1 --aligned --aligned.beacon-url http://127.0.0.1:58801 --aligned-network devnet --aligned-sp1-elf-path prover/src/guest_program/src/sp1/out/riscv32im-succinct-zkvm-elf +cargo run --release --manifest-path ../../Cargo.toml --bin ethrex --features "l2" -- l2 --watcher.block-delay 0 --network ../../fixtures/genesis/l2.json --http.port 1729 --http.addr 0.0.0.0 --datadir dev_ethrex_l2 --l1.bridge-address --l1.on-chain-proposer-address --eth.rpc-url http://localhost:8545 --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d --block-producer.fee-vault-address 0x000c0d6b7c4516a5b274c51ea331a9410fe69127 --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d --proof-coordinator.addr 127.0.0.1 --aligned --aligned.beacon-url http://127.0.0.1:58801 --aligned-network devnet --aligned-sp1-elf-path prover/src/guest_program/src/sp1/out/riscv32im-succinct-zkvm-elf ``` > [!IMPORTANT] diff --git a/docs/l2/fundamentals/transaction_fees.md b/docs/l2/fundamentals/transaction_fees.md new file mode 100644 index 00000000000..d50a9e3a169 --- /dev/null +++ b/docs/l2/fundamentals/transaction_fees.md @@ -0,0 +1,31 @@ +# Transaction Fees + +This page describes the different types of transaction fees that the Ethrex L2 rollup can charge and how they can be configured. + +> [!NOTE] +> Privileged transactions are exempt from all fees. + +## Execution Fees + +Execution fees are divided into two components: **base fee** and **priority fee**. + +### Base Fee + +The base fee follows the same rules as the Ethereum L1 base fee. It adjusts dynamically depending on network congestion to ensure stable transaction pricing. +By default, base fees are burned. However a sequencer can configure a `fee vault` address to receive the collected base fees instead of burning them. + +```sh +ethrex l2 --block-producer.fee-vault-address +``` + +> [!CAUTION] +> If the 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. + +### Priority Fee + +The priority fee works exactly the same way as on Ethereum L1. +It is an additional tip paid by the transaction sender to incentivize the sequencer to prioritize the inclusion of their transaction. The priority fee is always forwarded directly to the sequencer’s coinbase address. + +## Operator Fees + +## L1 Fees diff --git a/fixtures/keys/private_keys.txt b/fixtures/keys/private_keys.txt index 890bd315a9d..ac8e9738ec7 100644 --- a/fixtures/keys/private_keys.txt +++ b/fixtures/keys/private_keys.txt @@ -12,7 +12,6 @@ 0x556a4c63c314bf2a1734f34bcc2c45941b41afe3e4827d205092f3cc8d3ab11b 0x658edfde371c42f7dbdafdb73b9d28b87fff929753f74eb7c9888c2f25b02ea6 0xabea088cc1a18dda221851da62d00bdc3724368e47ae0a2516727871554859bd -0xe9ea73e0ca433882aa9d4e2311ecc4e17286121e6bd8e600e5d25d4243b2baa3 0x0ae566c7c3db3611107b876eff2dd4a27e6513642bcd4fab418a7595a14240e4 0x31950b39d586c6d3e37bc37233880ab3266505165be34c628fe5ee6746a3afec 0x547ba217be53dfe01f7d6a4af1452f8e0ba7755ca43ba8bad731d3da77fe1de7 diff --git a/fixtures/keys/private_keys_l1.txt b/fixtures/keys/private_keys_l1.txt index 92353befb95..e183ff2e202 100644 --- a/fixtures/keys/private_keys_l1.txt +++ b/fixtures/keys/private_keys_l1.txt @@ -179,7 +179,6 @@ 0xe556d12479cde2e345ba952bdae2a47f6179f4f03a491ecda414970bab5c8005 0xe76b962fc2d87e124bba333e6ff90a258760260f24f8a3c4fbceaeac68157f6e 0xe88d9382e5e4914469a23dcc27271e05aa7715ddeaee66b857a455c1fd51104a -0xe9ea73e0ca433882aa9d4e2311ecc4e17286121e6bd8e600e5d25d4243b2baa3 0xea016edc3a0dda43660a83cd7dddddfed87f34fe2849ab5c4339e9b8660e9553 0xeaa700570abf8ad30abc2c4d922f4a76475e0bf37bdc9b434faff602bfe3847b 0xeac93dc1957553f0816bc3a26d62f5ec475ecf2f02ee2a92bf9ff0214fc1fdd9 diff --git a/tooling/migrations/src/cli.rs b/tooling/migrations/src/cli.rs index d2e39112eaa..bdb84f3ba80 100644 --- a/tooling/migrations/src/cli.rs +++ b/tooling/migrations/src/cli.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use clap::{Parser as ClapParser, Subcommand as ClapSubcommand}; use ethrex_blockchain::{Blockchain, BlockchainOptions, BlockchainType}; -use ethrex_common::types::Block; +use ethrex_common::types::{Block, fee_config::FeeConfig}; use crate::utils::{migrate_block_body, migrate_block_header}; @@ -92,7 +92,8 @@ async fn migrate_libmdbx_to_rocksdb( println!("Migrating from block {last_known_block} to {last_block_number}"); let blockchain_opts = BlockchainOptions { - r#type: BlockchainType::L2, + // TODO: we may want to migrate using a specified fee config + r#type: BlockchainType::L2(FeeConfig::default()), ..Default::default() }; let blockchain = Blockchain::new(new_store.clone(), blockchain_opts);