diff --git a/.github/workflows/ci_l1.yaml b/.github/workflows/ci_l1.yaml index b24b627d5..d629fabb4 100644 --- a/.github/workflows/ci_l1.yaml +++ b/.github/workflows/ci_l1.yaml @@ -197,6 +197,7 @@ jobs: run: | docker load --input /tmp/ethrex_image.tar + # By default ethrex uses revm as evm backend. - name: Run Hive Simulation run: chmod +x hive && ./hive --client ethrex --sim ${{ matrix.simulation }} --sim.limit "${{ matrix.test_pattern }}" --sim.parallelism 4 diff --git a/.github/workflows/ci_levm.yaml b/.github/workflows/ci_levm.yaml index 0c64d8ac7..0030cd020 100644 --- a/.github/workflows/ci_levm.yaml +++ b/.github/workflows/ci_levm.yaml @@ -87,7 +87,10 @@ jobs: name: results_levm_trigger.md - name: Rename result (1) - run: cp results.md results_levm.md + run: | + cp results.md results_levm.md + echo "RESULTS:" + cat results_levm.md - name: Download results (main) uses: actions/download-artifact@v4 @@ -97,12 +100,17 @@ jobs: continue-on-error: true - name: Rename result (2) - run: cp results.md results_levm_main.md + run: | + cp results.md results_levm_main.md + echo "RESULTS:" + cat results_levm_main.md - name: Create diff message run: | bash .github/scripts/hive_levm_revm_diff.sh results_levm_main.md results_levm.md >> diff.md cat diff.md >> $GITHUB_STEP_SUMMARY + echo "SUMMARY:" + cat diff.md - name: Check Regression run: | diff --git a/.github/workflows/common_hive_reports.yaml b/.github/workflows/common_hive_reports.yaml index acac49de8..e446d636f 100644 --- a/.github/workflows/common_hive_reports.yaml +++ b/.github/workflows/common_hive_reports.yaml @@ -38,7 +38,6 @@ jobs: steps: - name: Pull image - if: ${{ inputs.evm == 'revm' }} run: | docker pull ghcr.io/lambdaclass/ethrex:latest docker tag ghcr.io/lambdaclass/ethrex:latest ethrex:latest @@ -53,10 +52,6 @@ jobs: with: ref: main - - name: Build Image with LEVM - if: ${{ inputs.evm == 'levm' }} - run: cd crates/vm/levm && make build-image-levm - - name: Setup Go uses: actions/setup-go@v5 @@ -64,7 +59,7 @@ jobs: run: make setup-hive - name: Run Hive Simulation - run: cd hive && ./hive --client ethrex --sim ${{ matrix.test.simulation }} --sim.parallelism 16 + run: cd hive && ./hive --client ethrex --sim ${{ matrix.test.simulation }} --ethrex.flags "--evm ${{ inputs.evm }}" --sim.parallelism 16 continue-on-error: true - name: Upload results diff --git a/.github/workflows/flamegraph_reporter.yaml b/.github/workflows/flamegraph_reporter.yaml index ea4e894d4..b2dad338e 100644 --- a/.github/workflows/flamegraph_reporter.yaml +++ b/.github/workflows/flamegraph_reporter.yaml @@ -252,6 +252,7 @@ jobs: ethrex_l2 config create default --default ethrex_l2 config set default + # By default ethrex uses revm as evm backend. - id: generate-flamegraph-ethrex name: Generate Flamegraph data for Ethrex shell: bash diff --git a/Makefile b/Makefile index cba14309e..329e59809 100644 --- a/Makefile +++ b/Makefile @@ -66,12 +66,13 @@ stop-localnet-silent: @kurtosis enclave stop $(ENCLAVE) >/dev/null 2>&1 || true @kurtosis enclave rm $(ENCLAVE) --force >/dev/null 2>&1 || true -HIVE_REVISION := b0b0f98bd24676239722e3aa7885e29ef856d804 +HIVE_REVISION := feb4333db7fe9f6dc161326ebb11957d4306d2f9 # Shallow clones can't specify a single revision, but at least we avoid working # the whole history by making it shallow since a given date (one day before our # target revision). HIVE_SHALLOW_SINCE := 2024-09-02 QUIET ?= false + hive: if [ "$(QUIET)" = "true" ]; then \ git clone --quiet --single-branch --branch master --shallow-since=$(HIVE_SHALLOW_SINCE) https://github.com/lambdaclass/hive && \ @@ -106,6 +107,9 @@ SIM_LOG_LEVEL ?= 4 run-hive: build-image setup-hive ## ๐Ÿงช Run Hive testing suite cd hive && ./hive --client ethrex --sim $(SIMULATION) --sim.limit "$(TEST_PATTERN)" +run-hive-levm: build-image setup-hive ## ๐Ÿงช Run Hive testing suite with LEVM + cd hive && ./hive --client ethrex --ethrex.flags "--evm levm" --sim $(SIMULATION) --sim.limit "$(TEST_PATTERN)" + run-hive-all: build-image setup-hive ## ๐Ÿงช Run all Hive testing suites cd hive && ./hive --client ethrex --sim ".*" --sim.parallelism 4 @@ -115,6 +119,16 @@ run-hive-debug: build-image setup-hive ## ๐Ÿž Run Hive testing suite in debug m clean-hive-logs: ## ๐Ÿงน Clean Hive logs rm -rf ./hive/workspace/logs +SIM_PARALLELISM := 48 +EVM_BACKEND := revm +# `make run-hive-report SIM_PARALLELISM=24 EVM_BACKEND="levm"` +run-hive-report: build-image setup-hive clean-hive-logs ## ๐Ÿ Run Hive and Build report + cd hive && ./hive --ethrex.flags "--evm $(EVM_BACKEND)" --sim ethereum/rpc-compat --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd hive && ./hive --ethrex.flags "--evm $(EVM_BACKEND)" --sim devp2p --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd hive && ./hive --ethrex.flags "--evm $(EVM_BACKEND)" --sim ethereum/engine --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd hive && ./hive --ethrex.flags "--evm $(EVM_BACKEND)" --sim ethereum/sync --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cargo run --release -p hive_report + loc: cargo run -p loc @@ -152,17 +166,18 @@ install-cli: ## ๐Ÿ› ๏ธ Installs the ethrex-l2 cli start-node-with-flamegraph: rm-test-db ## ๐Ÿš€๐Ÿ”ฅ Starts an ethrex client used for testing @if [ -z "$$L" ]; then \ - LEVM=""; \ + LEVM="revm"; \ echo "Running the test-node without the LEVM feature"; \ echo "If you want to use levm, run the target with an L at the end: make L=1"; \ else \ - LEVM=",levm"; \ + LEVM="levm"; \ echo "Running the test-node with the LEVM feature"; \ fi; \ - sudo CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph \ + sudo -E CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph \ --bin ethrex \ - --features "dev$$LEVM" \ + --features "dev" \ -- \ + --evm $$LEVM \ --network test_data/genesis-l2.json \ --http.port 1729 \ --datadir test_ethrex @@ -176,14 +191,10 @@ load-node: install-cli ## ๐Ÿšง Runs a load-test. Run make start-node-with-flameg CONTRACT_INTERACTION="-c"; \ echo "Running the load-test with contract interaction"; \ fi; \ - ethrex_l2 test load --path test_data/private_keys.txt -i 100 -v --value 1 $$CONTRACT_INTERACTION + ethrex_l2 test load --path test_data/private_keys.txt -i 1000 -v --value 100000 $$CONTRACT_INTERACTION rm-test-db: ## ๐Ÿ›‘ Removes the DB used by the ethrex client used for testing sudo cargo run --release --bin ethrex -- removedb --datadir test_ethrex -flamegraph: - sudo -E CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --bin ethrex --features dev -- --network test_data/genesis-l2.json --http.port 1729 >/dev/null & +flamegraph: ## ๐Ÿšง Runs a load-test. Run make start-node-with-flamegraph and in a new terminal make flamegraph bash scripts/flamegraph.sh - -test-load: - ethrex_l2 test load --path ./test_data/private_keys.txt -i 1000 -v --value 10000000 --to 0xFCbaC0713ACf16708aB6BC977227041FA1BC618D diff --git a/README.md b/README.md index 6b74c8bc5..a2aadc9b8 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,7 @@ ethrex supports the following command line arguments: - `--bootnodes `: Comma separated enode URLs for P2P discovery bootstrap. - `--log.level `: The verbosity level used for logs. Default value: info. possible values: info, debug, trace, warn, error - `--syncmode `: The way in which the node will sync its state. Can be either "full" or "snap" with "snap" as default value. +- `--evm `: Has to be `levm` or `revm`. Default value: `revm`. # ethrex L2 diff --git a/cmd/ef_tests/levm/Cargo.toml b/cmd/ef_tests/levm/Cargo.toml index cc4fa55d4..6dcbbfb4a 100644 --- a/cmd/ef_tests/levm/Cargo.toml +++ b/cmd/ef_tests/levm/Cargo.toml @@ -4,11 +4,11 @@ version.workspace = true edition.workspace = true [dependencies] -ethrex-blockchain = { workspace = true, features = ["levm"] } +ethrex-blockchain = { workspace = true } ethrex-core.workspace = true ethrex-storage.workspace = true ethrex-rlp.workspace = true -ethrex-vm = { workspace = true, features = ["levm"] } +ethrex-vm = { workspace = true } ethrex-levm = { path = "../../../crates/vm/levm" } serde.workspace = true serde_json.workspace = true diff --git a/cmd/ef_tests/levm/runner/levm_runner.rs b/cmd/ef_tests/levm/runner/levm_runner.rs index 7e006a757..065c8656b 100644 --- a/cmd/ef_tests/levm/runner/levm_runner.rs +++ b/cmd/ef_tests/levm/runner/levm_runner.rs @@ -15,7 +15,7 @@ use ethrex_levm::{ Environment, }; use ethrex_storage::AccountUpdate; -use ethrex_vm::{db::StoreWrapper, EvmState}; +use ethrex_vm::db::{EvmState, StoreWrapper}; use keccak_hash::keccak; use std::{collections::HashMap, sync::Arc}; diff --git a/cmd/ef_tests/levm/runner/revm_runner.rs b/cmd/ef_tests/levm/runner/revm_runner.rs index 2d529bc10..6ca9bb9d0 100644 --- a/cmd/ef_tests/levm/runner/revm_runner.rs +++ b/cmd/ef_tests/levm/runner/revm_runner.rs @@ -17,7 +17,10 @@ use ethrex_levm::{ Account, StorageSlot, }; use ethrex_storage::{error::StoreError, AccountUpdate}; -use ethrex_vm::{db::StoreWrapper, fork_to_spec_id, EvmState, RevmAddress, RevmU256}; +use ethrex_vm::{ + db::{EvmState, StoreWrapper}, + fork_to_spec_id, RevmAddress, RevmU256, +}; use revm::{ db::State, inspectors::TracerEip3155 as RevmTracerEip3155, diff --git a/cmd/ef_tests/levm/utils.rs b/cmd/ef_tests/levm/utils.rs index 7eaf2eb2d..d0a6906b5 100644 --- a/cmd/ef_tests/levm/utils.rs +++ b/cmd/ef_tests/levm/utils.rs @@ -4,7 +4,7 @@ use crate::{ }; use ethrex_core::{types::Genesis, H256, U256}; use ethrex_storage::{EngineType, Store}; -use ethrex_vm::{evm_state, EvmState}; +use ethrex_vm::db::{evm_state, EvmState}; use spinoff::Spinner; pub fn load_initial_state(test: &EFTest) -> (EvmState, H256) { diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index 3aa1dc081..f19857a40 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -48,4 +48,3 @@ metrics = ["ethrex-blockchain/metrics", "ethrex-l2/metrics"] libmdbx = ["dep:libmdbx", "ethrex-storage/libmdbx"] redb = ["dep:redb", "ethrex-storage/redb"] l2 = ["dep:ethrex-l2", "ethrex-vm/l2"] -levm = ["default", "ethrex-vm/levm", "ethrex-blockchain/levm"] diff --git a/cmd/ethrex/cli.rs b/cmd/ethrex/cli.rs index fa1e5ee78..8bd876887 100644 --- a/cmd/ethrex/cli.rs +++ b/cmd/ethrex/cli.rs @@ -1,5 +1,6 @@ use clap::{Arg, ArgAction, Command}; use ethrex_p2p::types::Node; +use ethrex_vm::backends::EVM; use tracing::Level; pub fn cli() -> Command { @@ -128,6 +129,7 @@ pub fn cli() -> Command { .required(false) .default_value("revm") .value_name("EVM_BACKEND") + .value_parser(clap::value_parser!(EVM)) .help("Has to be `levm` or `revm`"), ) .subcommand( diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index b7c29c25a..6bc97f5a2 100644 --- a/cmd/ethrex/ethrex.rs +++ b/cmd/ethrex/ethrex.rs @@ -10,6 +10,7 @@ use ethrex_p2p::{ }; use ethrex_rlp::decode::RLPDecode; use ethrex_storage::{EngineType, Store}; +use ethrex_vm::{backends::EVM, EVM_BACKEND}; use k256::ecdsa::SigningKey; use local_ip_address::local_ip; use rand::rngs::OsRng; @@ -150,6 +151,10 @@ async fn main() { let sync_mode = sync_mode(&matches); + let evm = matches.get_one::("evm").unwrap_or(&EVM::REVM); + let evm = EVM_BACKEND.get_or_init(|| evm.clone()); + info!("EVM_BACKEND set to: {:?}", evm); + cfg_if::cfg_if! { if #[cfg(feature = "redb")] { let store = Store::new(&data_dir, EngineType::RedB).expect("Failed to create Store"); @@ -284,7 +289,14 @@ async fn main() { }; let max_tries = 3; let url = format!("http://{authrpc_socket_addr}"); - let block_producer_engine = ethrex_dev::block_producer::start_block_producer(url, authrpc_jwtsecret.into(), head_block_hash, max_tries, 1000, ethrex_core::Address::default()); + let block_producer_engine = ethrex_dev::block_producer::start_block_producer( + url, + authrpc_jwtsecret.into(), + head_block_hash, + max_tries, + 1000, + ethrex_core::Address::default(), + ); tracker.spawn(block_producer_engine); } else { ethrex_p2p::start_network( @@ -419,12 +431,13 @@ fn import_blocks(store: &Store, blocks: &Vec) { } if let Some(last_block) = blocks.last() { let hash = last_block.hash(); - cfg_if::cfg_if! { - if #[cfg(feature = "levm")] { + match EVM_BACKEND.get() { + Some(EVM::LEVM) => { // We are allowing this not to unwrap so that tests can run even if block execution results in the wrong root hash with LEVM. let _ = apply_fork_choice(store, hash, hash, hash); } - else { + // This means we are using REVM as default + Some(EVM::REVM) | None => { apply_fork_choice(store, hash, hash, hash).unwrap(); } } diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index da6c6d7f5..e9f0e0b72 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ethrex-levm = { path = "../vm/levm", optional = true } +ethrex-levm = { path = "../vm/levm" } thiserror.workspace = true sha3.workspace = true tracing.workspace = true @@ -31,7 +31,6 @@ path = "./blockchain.rs" [features] default = ["c-kzg"] -levm = ["default", "ethrex-vm/levm", "ethrex-levm"] libmdbx = ["ethrex-core/libmdbx", "ethrex-storage/default", "ethrex-vm/libmdbx"] c-kzg = ["ethrex-core/c-kzg"] metrics = ["ethrex-metrics/transactions"] diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 509f6dd76..3c2944653 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -16,7 +16,10 @@ use ethrex_core::H256; use ethrex_storage::error::StoreError; use ethrex_storage::{AccountUpdate, Store}; -use ethrex_vm::{evm_state, execute_block, EvmState}; +use ethrex_vm::db::{evm_state, EvmState}; + +use ethrex_vm::EVM_BACKEND; +use ethrex_vm::{backends, backends::EVM}; //TODO: Implement a struct Chain or BlockChain to encapsulate //functionality and canonical chain state and config @@ -40,16 +43,14 @@ pub fn add_block(block: &Block, storage: &Store) -> Result<(), ChainError> { // Validate the block pre-execution validate_block(block, &parent_header, &state)?; let (receipts, account_updates): (Vec, Vec) = { - // TODO: Consider refactoring both implementations so that they have the same signature - #[cfg(feature = "levm")] - { - execute_block(block, &mut state)? - } - #[cfg(not(feature = "levm"))] - { - let receipts = execute_block(block, &mut state)?; - let account_updates = ethrex_vm::get_state_transitions(&mut state); - (receipts, account_updates) + match EVM_BACKEND.get() { + Some(EVM::LEVM) => backends::levm::execute_block(block, &mut state)?, + // This means we are using REVM as default for tests + Some(EVM::REVM) | None => { + let receipts = backends::revm::execute_block(block, &mut state)?; + let account_updates = ethrex_vm::get_state_transitions(&mut state); + (receipts, account_updates) + } } }; diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index 49fea0051..18ebb4833 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -8,35 +8,25 @@ use ethrex_core::{ types::{ calculate_base_fee_per_blob_gas, calculate_base_fee_per_gas, compute_receipts_root, compute_transactions_root, compute_withdrawals_root, BlobsBundle, Block, BlockBody, - BlockHash, BlockHeader, BlockNumber, ChainConfig, Fork, MempoolTransaction, Receipt, - Transaction, Withdrawal, DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH, + BlockHash, BlockHeader, BlockNumber, ChainConfig, MempoolTransaction, Receipt, Transaction, + Withdrawal, DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH, }, Address, Bloom, Bytes, H256, U256, }; -#[cfg(not(feature = "levm"))] -use { - ethrex_core::types::Account, - ethrex_vm::{ - beacon_root_contract_call, execute_tx, get_state_transitions, process_withdrawals, spec_id, - SpecId, - }, -}; -#[cfg(feature = "levm")] -use { - ethrex_core::types::GWEI_TO_WEI, - ethrex_levm::{db::CacheDB, vm::EVMConfig, Account, AccountInfo}, - ethrex_vm::{ - beacon_root_contract_call_levm, db::StoreWrapper, execute_tx_levm, - get_state_transitions_levm, - }, - std::sync::Arc, + +use ethrex_core::types::{Fork, GWEI_TO_WEI}; +use ethrex_levm::{db::CacheDB, vm::EVMConfig, Account, AccountInfo}; +use ethrex_vm::{ + backends, + backends::EVM, + db::{evm_state, EvmState, StoreWrapper}, + get_state_transitions, spec_id, EvmError, SpecId, EVM_BACKEND, }; +use std::sync::Arc; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::{error::StoreError, Store}; -use ethrex_vm::{evm_state, EvmError, EvmState}; - use sha3::{Digest, Keccak256}; use ethrex_metrics::metrics; @@ -187,7 +177,6 @@ fn calc_excess_blob_gas( pub struct PayloadBuildContext<'a> { pub payload: &'a mut Block, pub evm_state: &'a mut EvmState, - #[cfg(feature = "levm")] pub block_cache: CacheDB, pub remaining_gas: u64, pub receipts: Vec, @@ -215,7 +204,6 @@ impl<'a> PayloadBuildContext<'a> { payload, evm_state, blobs_bundle: BlobsBundle::default(), - #[cfg(feature = "levm")] block_cache: CacheDB::new(), }) } @@ -259,106 +247,112 @@ pub fn build_payload( } pub fn apply_withdrawals(context: &mut PayloadBuildContext) -> Result<(), EvmError> { - #[cfg(feature = "levm")] - { - if let Some(withdrawals) = &context.payload.body.withdrawals { - // For every withdrawal we increment the target account's balance - for (address, increment) in withdrawals - .iter() - .filter(|withdrawal| withdrawal.amount > 0) - .map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI))) - { - // We check if it was in block_cache, if not, we get it from DB. - let mut account = context.block_cache.get(&address).cloned().unwrap_or({ - let acc_info = context - .store() - .ok_or(StoreError::MissingStore)? - .get_account_info_by_hash(context.parent_hash(), address)? - .unwrap_or_default(); - let acc_code = context - .store() - .ok_or(StoreError::MissingStore)? - .get_account_code(acc_info.code_hash)? - .unwrap_or_default(); - - Account { - info: AccountInfo { - balance: acc_info.balance, - bytecode: acc_code, - nonce: acc_info.nonce, - }, - // This is the added_storage for the withdrawal. - // If not involved in the TX, there won't be any updates in the storage - storage: HashMap::new(), - } - }); - - account.info.balance += increment.into(); - context.block_cache.insert(address, account); + match EVM_BACKEND.get() { + Some(EVM::LEVM) => { + if let Some(withdrawals) = &context.payload.body.withdrawals { + // For every withdrawal we increment the target account's balance + for (address, increment) in withdrawals + .iter() + .filter(|withdrawal| withdrawal.amount > 0) + .map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI))) + { + // We check if it was in block_cache, if not, we get it from DB. + let mut account = context.block_cache.get(&address).cloned().unwrap_or({ + let acc_info = context + .store() + .ok_or(StoreError::MissingStore)? + .get_account_info_by_hash(context.parent_hash(), address)? + .unwrap_or_default(); + let acc_code = context + .store() + .ok_or(StoreError::MissingStore)? + .get_account_code(acc_info.code_hash)? + .unwrap_or_default(); + + Account { + info: AccountInfo { + balance: acc_info.balance, + bytecode: acc_code, + nonce: acc_info.nonce, + }, + // This is the added_storage for the withdrawal. + // If not involved in the TX, there won't be any updates in the storage + storage: HashMap::new(), + } + }); + + account.info.balance += increment.into(); + context.block_cache.insert(address, account); + } } } - } - #[cfg(not(feature = "levm"))] - { - process_withdrawals( - context.evm_state, - context - .payload - .body - .withdrawals - .as_ref() - .unwrap_or(&Vec::new()), - )?; + Some(EVM::REVM) | None => { + backends::revm::process_withdrawals( + context.evm_state, + context + .payload + .body + .withdrawals + .as_ref() + .unwrap_or(&Vec::new()), + )?; + } } Ok(()) } pub fn make_beacon_root_call(context: &mut PayloadBuildContext) -> Result<(), EvmError> { - #[cfg(feature = "levm")] - { - let fork = context - .chain_config()? - .fork(context.payload.header.timestamp); - let blob_schedule = context - .chain_config()? - .get_fork_blob_schedule(context.payload.header.timestamp) - .unwrap_or(EVMConfig::canonical_values(fork)); - let config = EVMConfig::new(fork, blob_schedule); - - if context.payload.header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun { - let store_wrapper = Arc::new(StoreWrapper { - store: context.evm_state.database().unwrap().clone(), - block_hash: context.payload.header.parent_hash, - }); - let report = beacon_root_contract_call_levm( - store_wrapper.clone(), - &context.payload.header, - config, - )?; + match EVM_BACKEND.get() { + Some(EVM::LEVM) => { + let fork = context + .chain_config()? + .fork(context.payload.header.timestamp); + let blob_schedule = context + .chain_config()? + .get_fork_blob_schedule(context.payload.header.timestamp) + .unwrap_or(EVMConfig::canonical_values(fork)); + let config = EVMConfig::new(fork, blob_schedule); + + if context.payload.header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun { + let store_wrapper = Arc::new(StoreWrapper { + store: context.evm_state.database().unwrap().clone(), + block_hash: context.payload.header.parent_hash, + }); + let report = backends::levm::beacon_root_contract_call_levm( + store_wrapper.clone(), + &context.payload.header, + config, + )?; - let mut new_state = report.new_state.clone(); + let mut new_state = report.new_state.clone(); - // Now original_value is going to be the same as the current_value, for the next transaction. - // It should have only one value but it is convenient to keep on using our CacheDB structure - for account in new_state.values_mut() { - for storage_slot in account.storage.values_mut() { - storage_slot.original_value = storage_slot.current_value; + // Now original_value is going to be the same as the current_value, for the next transaction. + // It should have only one value but it is convenient to keep on using our CacheDB structure + for account in new_state.values_mut() { + for storage_slot in account.storage.values_mut() { + storage_slot.original_value = storage_slot.current_value; + } } - } - context.block_cache.extend(new_state); + context.block_cache.extend(new_state); + } } - Ok(()) - } - #[cfg(not(feature = "levm"))] - { - // Apply withdrawals & call beacon root contract, and obtain the new state root - let spec_id = spec_id(&context.chain_config()?, context.payload.header.timestamp); - if context.payload.header.parent_beacon_block_root.is_some() && spec_id >= SpecId::CANCUN { - beacon_root_contract_call(context.evm_state, &context.payload.header, spec_id)?; + // This means we are using REVM as default for tests + Some(EVM::REVM) | None => { + // Apply withdrawals & call beacon root contract, and obtain the new state root + let spec_id = spec_id(&context.chain_config()?, context.payload.header.timestamp); + if context.payload.header.parent_beacon_block_root.is_some() + && spec_id >= SpecId::CANCUN + { + backends::revm::beacon_root_contract_call( + context.evm_state, + &context.payload.header, + spec_id, + )?; + } } - Ok(()) } + Ok(()) } /// Fetches suitable transactions from the mempool @@ -561,112 +555,98 @@ fn apply_plain_transaction( head: &HeadTransaction, context: &mut PayloadBuildContext, ) -> Result { - #[cfg(feature = "levm")] - { - let store_wrapper = Arc::new(StoreWrapper { - store: context.evm_state.database().unwrap().clone(), - block_hash: context.payload.header.parent_hash, - }); + match EVM_BACKEND.get() { + Some(EVM::LEVM) => { + let store_wrapper = Arc::new(StoreWrapper { + store: context.evm_state.database().unwrap().clone(), + block_hash: context.payload.header.parent_hash, + }); - let fork = context - .chain_config()? - .fork(context.payload.header.timestamp); - let blob_schedule = context - .chain_config()? - .get_fork_blob_schedule(context.payload.header.timestamp) - .unwrap_or(EVMConfig::canonical_values(fork)); - let config = EVMConfig::new(fork, blob_schedule); - - let report = execute_tx_levm( - &head.tx, - &context.payload.header, - store_wrapper.clone(), - context.block_cache.clone(), - config, - ) - .map_err(|e| EvmError::Transaction(format!("Invalid Transaction: {e:?}")))?; - context.remaining_gas = context.remaining_gas.saturating_sub(report.gas_used); - context.block_value += U256::from(report.gas_used) * head.tip; - - let mut new_state = report.new_state.clone(); - - // Now original_value is going to be the same as the current_value, for the next transaction. - // It should have only one value but it is convenient to keep on using our CacheDB structure - for account in new_state.values_mut() { - for storage_slot in account.storage.values_mut() { - storage_slot.original_value = storage_slot.current_value; - } - } + let fork = context + .chain_config()? + .fork(context.payload.header.timestamp); + let blob_schedule = context + .chain_config()? + .get_fork_blob_schedule(context.payload.header.timestamp) + .unwrap_or(EVMConfig::canonical_values(fork)); + let config = EVMConfig::new(fork, blob_schedule); + + let report = backends::levm::execute_tx_levm( + &head.tx, + &context.payload.header, + store_wrapper.clone(), + context.block_cache.clone(), + config, + ) + .map_err(|e| EvmError::Transaction(format!("Invalid Transaction: {e:?}")))?; + context.remaining_gas = context.remaining_gas.saturating_sub(report.gas_used); + context.block_value += U256::from(report.gas_used) * head.tip; - context.block_cache.extend(new_state); + let mut new_state = report.new_state.clone(); - let receipt = Receipt::new( - head.tx.tx_type(), - report.is_success(), - context.payload.header.gas_limit - context.remaining_gas, - report.logs.clone(), - ); - Ok(receipt) - } + // Now original_value is going to be the same as the current_value, for the next transaction. + // It should have only one value but it is convenient to keep on using our CacheDB structure + for account in new_state.values_mut() { + for storage_slot in account.storage.values_mut() { + storage_slot.original_value = storage_slot.current_value; + } + } - // REVM Implementation - #[cfg(not(feature = "levm"))] - { - let report = execute_tx( - &head.tx, - &context.payload.header, - context.evm_state, - spec_id( - &context.chain_config().map_err(ChainError::from)?, - context.payload.header.timestamp, - ), - )?; - context.remaining_gas = context.remaining_gas.saturating_sub(report.gas_used()); - context.block_value += U256::from(report.gas_used()) * head.tip; - let receipt = Receipt::new( - head.tx.tx_type(), - report.is_success(), - context.payload.header.gas_limit - context.remaining_gas, - report.logs(), - ); - Ok(receipt) + context.block_cache.extend(new_state); + + let receipt = Receipt::new( + head.tx.tx_type(), + report.is_success(), + context.payload.header.gas_limit - context.remaining_gas, + report.logs.clone(), + ); + Ok(receipt) + } + // This means we are using REVM as default for tests + Some(EVM::REVM) | None => { + let report = backends::revm::execute_tx( + &head.tx, + &context.payload.header, + context.evm_state, + spec_id( + &context.chain_config().map_err(ChainError::from)?, + context.payload.header.timestamp, + ), + )?; + context.remaining_gas = context.remaining_gas.saturating_sub(report.gas_used()); + context.block_value += U256::from(report.gas_used()) * head.tip; + let receipt = Receipt::new( + head.tx.tx_type(), + report.is_success(), + context.payload.header.gas_limit - context.remaining_gas, + report.logs(), + ); + Ok(receipt) + } } } fn finalize_payload(context: &mut PayloadBuildContext) -> Result<(), StoreError> { - #[cfg(feature = "levm")] - { - let account_updates = get_state_transitions_levm( + let account_updates = match EVM_BACKEND.get() { + Some(EVM::LEVM) => backends::levm::get_state_transitions_levm( context.evm_state, context.parent_hash(), &context.block_cache.clone(), - ); + ), + // This means we are using REVM as default for tests + Some(EVM::REVM) | None => get_state_transitions(context.evm_state), + }; - context.payload.header.state_root = context - .store() - .ok_or(StoreError::MissingStore)? - .apply_account_updates(context.parent_hash(), &account_updates)? - .unwrap_or_default(); - context.payload.header.transactions_root = - compute_transactions_root(&context.payload.body.transactions); - context.payload.header.receipts_root = compute_receipts_root(&context.receipts); - context.payload.header.gas_used = context.payload.header.gas_limit - context.remaining_gas; - Ok(()) - } - #[cfg(not(feature = "levm"))] - { - let account_updates = get_state_transitions(context.evm_state); - context.payload.header.state_root = context - .store() - .ok_or(StoreError::MissingStore)? - .apply_account_updates(context.parent_hash(), &account_updates)? - .unwrap_or_default(); - context.payload.header.transactions_root = - compute_transactions_root(&context.payload.body.transactions); - context.payload.header.receipts_root = compute_receipts_root(&context.receipts); - context.payload.header.gas_used = context.payload.header.gas_limit - context.remaining_gas; - Ok(()) - } + context.payload.header.state_root = context + .store() + .ok_or(StoreError::MissingStore)? + .apply_account_updates(context.parent_hash(), &account_updates)? + .unwrap_or_default(); + context.payload.header.transactions_root = + compute_transactions_root(&context.payload.body.transactions); + context.payload.header.receipts_root = compute_receipts_root(&context.receipts); + context.payload.header.gas_used = context.payload.header.gas_limit - context.remaining_gas; + Ok(()) } /// A struct representing suitable mempool transactions waiting to be included in a block diff --git a/crates/l2/Makefile b/crates/l2/Makefile index dd3f5a723..dea20f49b 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -73,11 +73,12 @@ init-l1-levm: ## ๐Ÿš€ Initializes an L1 Lambda ethrex Client with LEVM cargo run --release \ --manifest-path ../../Cargo.toml \ --bin ethrex \ - --features dev,ethrex-blockchain/levm,ethrex-vm/levm -- \ + --features "dev" -- \ --network ${L1_GENESIS_FILE_PATH} \ --http.port ${L1_PORT} \ --http.addr 0.0.0.0 \ --authrpc.port ${L1_AUTH_PORT} \ + --evm levm \ --datadir ${ethrex_L1_DEV_LIBMDBX} down-local-l1: ## ๐Ÿ›‘ Shuts down the L1 Lambda ethrex Client diff --git a/crates/l2/proposer/l1_committer.rs b/crates/l2/proposer/l1_committer.rs index 72267350c..ae39e0a16 100644 --- a/crates/l2/proposer/l1_committer.rs +++ b/crates/l2/proposer/l1_committer.rs @@ -20,7 +20,7 @@ use ethrex_l2_sdk::{ eth_client::{eth_sender::Overrides, BlockByNumber, EthClient, WrappedTransaction}, }; use ethrex_storage::{error::StoreError, Store}; -use ethrex_vm::{evm_state, execute_block, get_state_transitions}; +use ethrex_vm::{backends::revm::execute_block, db::evm_state, get_state_transitions}; use keccak_hash::keccak; use secp256k1::SecretKey; use std::{collections::HashMap, time::Duration}; diff --git a/crates/networking/rpc/eth/mod.rs b/crates/networking/rpc/eth/mod.rs index 8d981e007..5fc298a79 100644 --- a/crates/networking/rpc/eth/mod.rs +++ b/crates/networking/rpc/eth/mod.rs @@ -16,7 +16,7 @@ pub mod test_utils { use ethrex_core::{ types::{ Block, BlockBody, BlockHeader, EIP1559Transaction, Genesis, LegacyTransaction, - Transaction, TxKind, EMPTY_KECCACK_HASH, + Transaction, TxKind, DEFAULT_REQUESTS_HASH, }, Address, Bloom, H256, U256, }; @@ -69,7 +69,7 @@ pub mod test_utils { blob_gas_used: Some(0x00), excess_blob_gas: Some(0x00), parent_beacon_block_root: Some(H256::zero()), - requests_hash: Some(*EMPTY_KECCACK_HASH), + requests_hash: Some(*DEFAULT_REQUESTS_HASH), } } diff --git a/crates/networking/rpc/eth/transaction.rs b/crates/networking/rpc/eth/transaction.rs index 92593b6cb..e098bce6f 100644 --- a/crates/networking/rpc/eth/transaction.rs +++ b/crates/networking/rpc/eth/transaction.rs @@ -16,7 +16,7 @@ use ethrex_blockchain::mempool; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::Store; -use ethrex_vm::{evm_state, ExecutionResult, SpecId}; +use ethrex_vm::{db::evm_state, ExecutionResult, SpecId}; use serde::Serialize; use serde_json::Value; diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 1ed8913cd..ad0d91901 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] ethrex-core = { path = "../common", default-features = false } ethrex-storage = { path = "../storage/store", default-features = false } -ethrex-levm = { path = "./levm", optional = true } +ethrex-levm = { path = "./levm" } ethrex-trie = { path = "../storage/trie", default-features = false } ethrex-rlp = { path = "../common/rlp", default-features = false } revm = { version = "18.0.0", features = [ @@ -39,7 +39,6 @@ path = "./vm.rs" [features] default = ["c-kzg", "blst"] -levm = ["default", "ethrex-levm"] l2 = [] c-kzg = ["revm/c-kzg"] blst = ["revm/blst"] diff --git a/crates/vm/backends/levm.rs b/crates/vm/backends/levm.rs new file mode 100644 index 000000000..593fd0442 --- /dev/null +++ b/crates/vm/backends/levm.rs @@ -0,0 +1,314 @@ +use crate::db::StoreWrapper; +use crate::EvmError; +use crate::EvmState; +#[cfg(not(feature = "l2"))] +use ethrex_core::types::Fork; +use ethrex_core::{ + types::{ + code_hash, AccountInfo, Block, BlockHeader, Receipt, Transaction, TxKind, GWEI_TO_WEI, + }, + Address, H256, U256, +}; + +use ethrex_levm::{ + db::{CacheDB, Database as LevmDatabase}, + errors::{ExecutionReport, TxResult, VMError}, + vm::{EVMConfig, VM}, + Account, Environment, +}; +use ethrex_storage::AccountUpdate; +use lazy_static::lazy_static; +use revm_primitives::Bytes; +use std::{collections::HashMap, sync::Arc}; + +/// Executes all transactions in a block and returns their receipts. +pub fn execute_block( + block: &Block, + state: &mut EvmState, +) -> Result<(Vec, Vec), EvmError> { + let store_wrapper = Arc::new(StoreWrapper { + store: state.database().unwrap().clone(), + block_hash: block.header.parent_hash, + }); + + let mut block_cache: CacheDB = HashMap::new(); + let block_header = &block.header; + let fork = state.chain_config()?.fork(block_header.timestamp); + // If there's no blob schedule in chain_config use the + // default/canonical values + let blob_schedule = state + .chain_config()? + .get_fork_blob_schedule(block_header.timestamp) + .unwrap_or(EVMConfig::canonical_values(fork)); + let config = EVMConfig::new(fork, blob_schedule); + cfg_if::cfg_if! { + if #[cfg(not(feature = "l2"))] { + if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun { + let report = beacon_root_contract_call_levm(store_wrapper.clone(), block_header, config)?; + block_cache.extend(report.new_state); + } + } + } + + // Account updates are initialized like this because of the beacon_root_contract_call, it is going to be empty if it wasn't called. + let mut account_updates = crate::get_state_transitions(state); + + let mut receipts = Vec::new(); + let mut cumulative_gas_used = 0; + + for tx in block.body.transactions.iter() { + let report = execute_tx_levm( + tx, + block_header, + store_wrapper.clone(), + block_cache.clone(), + config, + ) + .map_err(EvmError::from)?; + + let mut new_state = report.new_state.clone(); + + // Now original_value is going to be the same as the current_value, for the next transaction. + // It should have only one value but it is convenient to keep on using our CacheDB structure + for account in new_state.values_mut() { + for storage_slot in account.storage.values_mut() { + storage_slot.original_value = storage_slot.current_value; + } + } + + block_cache.extend(new_state); + + // Currently, in LEVM, we don't substract refunded gas to used gas, but that can change in the future. + let gas_used = report.gas_used - report.gas_refunded; + cumulative_gas_used += gas_used; + let receipt = Receipt::new( + tx.tx_type(), + matches!(report.result.clone(), TxResult::Success), + cumulative_gas_used, + report.logs.clone(), + ); + + receipts.push(receipt); + } + + // Here we update block_cache with balance increments caused by withdrawals. + if let Some(withdrawals) = &block.body.withdrawals { + // For every withdrawal we increment the target account's balance + for (address, increment) in withdrawals + .iter() + .filter(|withdrawal| withdrawal.amount > 0) + .map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI))) + { + // We check if it was in block_cache, if not, we get it from DB. + let mut account = block_cache.get(&address).cloned().unwrap_or({ + let acc_info = store_wrapper.get_account_info(address); + Account::from(acc_info) + }); + + account.info.balance += increment.into(); + + block_cache.insert(address, account); + } + } + + account_updates.extend(get_state_transitions_levm( + state, + block.header.parent_hash, + &block_cache, + )); + + Ok((receipts, account_updates)) +} + +pub fn execute_tx_levm( + tx: &Transaction, + block_header: &BlockHeader, + db: Arc, + block_cache: CacheDB, + config: EVMConfig, +) -> Result { + let gas_price: U256 = tx + .effective_gas_price(block_header.base_fee_per_gas) + .ok_or(VMError::InvalidTransaction)? + .into(); + + let env = Environment { + origin: tx.sender(), + refunded_gas: 0, + gas_limit: tx.gas_limit(), + config, + block_number: block_header.number.into(), + coinbase: block_header.coinbase, + timestamp: block_header.timestamp.into(), + prev_randao: Some(block_header.prev_randao), + chain_id: tx.chain_id().unwrap_or_default().into(), + base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(), + gas_price, + block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from), + block_blob_gas_used: block_header.blob_gas_used.map(U256::from), + tx_blob_hashes: tx.blob_versioned_hashes(), + tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from), + tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from), + tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas().map(U256::from), + tx_nonce: tx.nonce(), + block_gas_limit: block_header.gas_limit, + transient_storage: HashMap::new(), + }; + + let mut vm = VM::new( + tx.to(), + env, + tx.value(), + tx.data().clone(), + db, + block_cache, + tx.access_list(), + tx.authorization_list(), + )?; + + vm.execute() +} + +pub fn get_state_transitions_levm( + initial_state: &EvmState, + block_hash: H256, + new_state: &CacheDB, +) -> Vec { + let current_db = match initial_state { + EvmState::Store(state) => state.database.store.clone(), + EvmState::Execution(_cache_db) => unreachable!("Execution state should not be passed here"), + }; + let mut account_updates: Vec = vec![]; + for (new_state_account_address, new_state_account) in new_state { + let initial_account_state = current_db + .get_account_info_by_hash(block_hash, *new_state_account_address) + .expect("Error getting account info by address"); + + if initial_account_state.is_none() { + // New account, update everything + let new_account = AccountUpdate { + address: *new_state_account_address, + removed: new_state_account.is_empty(), + info: Some(AccountInfo { + code_hash: code_hash(&new_state_account.info.bytecode), + balance: new_state_account.info.balance, + nonce: new_state_account.info.nonce, + }), + code: Some(new_state_account.info.bytecode.clone()), + added_storage: new_state_account + .storage + .iter() + .map(|(key, storage_slot)| (*key, storage_slot.current_value)) + .collect(), + }; + + account_updates.push(new_account); + continue; + } + + // This unwrap is safe, just checked upside + let initial_account_state = initial_account_state.unwrap(); + let mut account_update = AccountUpdate::new(*new_state_account_address); + + // Account state after block execution. + let new_state_acc_info = AccountInfo { + code_hash: code_hash(&new_state_account.info.bytecode), + balance: new_state_account.info.balance, + nonce: new_state_account.info.nonce, + }; + + // Compare Account Info + if initial_account_state != new_state_acc_info { + account_update.info = Some(new_state_acc_info.clone()); + } + + // If code hash is different it means the code is different too. + if initial_account_state.code_hash != new_state_acc_info.code_hash { + account_update.code = Some(new_state_account.info.bytecode.clone()); + } + + let mut updated_storage = HashMap::new(); + for (key, storage_slot) in &new_state_account.storage { + // original_value in storage_slot is not the original_value on the DB, be careful. + let original_value = current_db + .get_storage_at_hash(block_hash, *new_state_account_address, *key) + .unwrap() + .unwrap_or_default(); // Option inside result, I guess I have to assume it is zero. + + if original_value != storage_slot.current_value { + updated_storage.insert(*key, storage_slot.current_value); + } + } + account_update.added_storage = updated_storage; + + account_update.removed = new_state_account.is_empty(); + + if account_update != AccountUpdate::new(*new_state_account_address) { + account_updates.push(account_update); + } + } + account_updates +} + +/// Calls the eip4788 beacon block root system call contract +/// More info on https://eips.ethereum.org/EIPS/eip-4788 +pub fn beacon_root_contract_call_levm( + store_wrapper: Arc, + block_header: &BlockHeader, + config: EVMConfig, +) -> Result { + lazy_static! { + static ref SYSTEM_ADDRESS: Address = + Address::from_slice(&hex::decode("fffffffffffffffffffffffffffffffffffffffe").unwrap()); + static ref CONTRACT_ADDRESS: Address = + Address::from_slice(&hex::decode("000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap(),); + }; + // This is OK + let beacon_root = match block_header.parent_beacon_block_root { + None => { + return Err(EvmError::Header( + "parent_beacon_block_root field is missing".to_string(), + )) + } + Some(beacon_root) => beacon_root, + }; + + let env = Environment { + origin: *SYSTEM_ADDRESS, + gas_limit: 30_000_000, + block_number: block_header.number.into(), + coinbase: block_header.coinbase, + timestamp: block_header.timestamp.into(), + prev_randao: Some(block_header.prev_randao), + base_fee_per_gas: U256::zero(), + gas_price: U256::zero(), + block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from), + block_blob_gas_used: block_header.blob_gas_used.map(U256::from), + block_gas_limit: 30_000_000, + transient_storage: HashMap::new(), + config, + ..Default::default() + }; + + let calldata = Bytes::copy_from_slice(beacon_root.as_bytes()).into(); + + // Here execute with LEVM but just return transaction report. And I will handle it in the calling place. + + let mut vm = VM::new( + TxKind::Call(*CONTRACT_ADDRESS), + env, + U256::zero(), + calldata, + store_wrapper, + CacheDB::new(), + vec![], + None, + ) + .map_err(EvmError::from)?; + + let mut report = vm.execute().map_err(EvmError::from)?; + + report.new_state.remove(&*SYSTEM_ADDRESS); + + Ok(report) +} diff --git a/crates/vm/backends/mod.rs b/crates/vm/backends/mod.rs new file mode 100644 index 000000000..e89fe5e38 --- /dev/null +++ b/crates/vm/backends/mod.rs @@ -0,0 +1,23 @@ +pub mod levm; +pub mod revm; + +use crate::errors::EvmError; +use std::str::FromStr; + +#[derive(Debug, Clone, Default)] +pub enum EVM { + #[default] + REVM, + LEVM, +} + +impl FromStr for EVM { + type Err = EvmError; + fn from_str(s: &str) -> Result { + match s { + "levm" => Ok(EVM::LEVM), + "revm" => Ok(EVM::REVM), + _ => Err(EvmError::InvalidEVM(s.to_string())), + } + } +} diff --git a/crates/vm/backends/revm.rs b/crates/vm/backends/revm.rs new file mode 100644 index 000000000..1f9088116 --- /dev/null +++ b/crates/vm/backends/revm.rs @@ -0,0 +1,498 @@ +use crate::spec_id; +use crate::EvmError; +use crate::EvmState; +use crate::ExecutionResult; +use ethrex_storage::error::StoreError; +use lazy_static::lazy_static; +use revm::{ + inspectors::TracerEip3155, + precompile::{PrecompileSpecId, Precompiles}, + primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256}, + Database, DatabaseCommit, Evm, +}; +use revm_inspectors::access_list::AccessListInspector; +// Rename imported types for clarity +use ethrex_core::{ + types::{ + Block, BlockHeader, GenericTransaction, PrivilegedTxType, Receipt, Transaction, TxKind, + Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, + }, + Address, +}; +use revm_primitives::{ + ruint::Uint, AccessList as RevmAccessList, AccessListItem, Address as RevmAddress, + Authorization as RevmAuthorization, Bytes, FixedBytes, SignedAuthorization, SpecId, + TxKind as RevmTxKind, U256 as RevmU256, +}; +use std::cmp::min; + +#[cfg(feature = "l2")] +use crate::mods; + +/// Executes all transactions in a block and returns their receipts. +pub fn execute_block(block: &Block, state: &mut EvmState) -> Result, EvmError> { + let block_header = &block.header; + let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); + //eip 4788: execute beacon_root_contract_call before block transactions + cfg_if::cfg_if! { + if #[cfg(not(feature = "l2"))] { + //eip 4788: execute beacon_root_contract_call before block transactions + if block_header.parent_beacon_block_root.is_some() && spec_id >= SpecId::CANCUN { + beacon_root_contract_call(state, block_header, spec_id)?; + } + } + } + let mut receipts = Vec::new(); + let mut cumulative_gas_used = 0; + + for transaction in block.body.transactions.iter() { + let result = execute_tx(transaction, block_header, state, spec_id)?; + cumulative_gas_used += result.gas_used(); + let receipt = Receipt::new( + transaction.tx_type(), + result.is_success(), + cumulative_gas_used, + result.logs(), + ); + receipts.push(receipt); + } + + if let Some(withdrawals) = &block.body.withdrawals { + process_withdrawals(state, withdrawals)?; + } + + Ok(receipts) +} +// Executes a single tx, doesn't perform state transitions +pub fn execute_tx( + tx: &Transaction, + header: &BlockHeader, + state: &mut EvmState, + spec_id: SpecId, +) -> Result { + let block_env = block_env(header); + let tx_env = tx_env(tx); + run_evm(tx_env, block_env, state, spec_id) +} + +/// Runs the transaction and returns the result, but does not commit it. +pub fn run_without_commit( + tx_env: TxEnv, + mut block_env: BlockEnv, + state: &mut EvmState, + spec_id: SpecId, +) -> Result { + adjust_disabled_base_fee( + &mut block_env, + tx_env.gas_price, + tx_env.max_fee_per_blob_gas, + ); + let chain_config = state.chain_config()?; + #[allow(unused_mut)] + let mut evm_builder = Evm::builder() + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .modify_cfg_env(|env| { + env.disable_base_fee = true; + env.disable_block_gas_limit = true; + env.chain_id = chain_config.chain_id; + }); + let tx_result = match state { + EvmState::Store(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact().map_err(EvmError::from)? + } + EvmState::Execution(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact().map_err(EvmError::from)? + } + }; + Ok(tx_result.result.into()) +} + +/// Runs EVM, doesn't perform state transitions, but stores them +fn run_evm( + tx_env: TxEnv, + block_env: BlockEnv, + state: &mut EvmState, + spec_id: SpecId, +) -> Result { + let tx_result = { + let chain_spec = state.chain_config()?; + #[allow(unused_mut)] + let mut evm_builder = Evm::builder() + .with_block_env(block_env) + .with_tx_env(tx_env) + .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) + .with_spec_id(spec_id) + .with_external_context( + TracerEip3155::new(Box::new(std::io::stderr())).without_summary(), + ); + cfg_if::cfg_if! { + if #[cfg(feature = "l2")] { + use revm::{Handler, primitives::{CancunSpec, HandlerCfg}}; + use std::sync::Arc; + + evm_builder = evm_builder.with_handler({ + let mut evm_handler = Handler::new(HandlerCfg::new(SpecId::LATEST)); + evm_handler.pre_execution.deduct_caller = Arc::new(mods::deduct_caller::); + evm_handler.validation.tx_against_state = Arc::new(mods::validate_tx_against_state::); + evm_handler.execution.last_frame_return = Arc::new(mods::last_frame_return::); + // TODO: Override `end` function. We should deposit even if we revert. + // evm_handler.pre_execution.end + evm_handler + }); + } + } + + match state { + EvmState::Store(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact_commit().map_err(EvmError::from)? + } + EvmState::Execution(db) => { + let mut evm = evm_builder.with_db(db).build(); + evm.transact_commit().map_err(EvmError::from)? + } + } + }; + Ok(tx_result.into()) +} + +/// Processes a block's withdrawals, updating the account balances in the state +pub fn process_withdrawals( + state: &mut EvmState, + withdrawals: &[Withdrawal], +) -> Result<(), StoreError> { + match state { + EvmState::Store(db) => { + //balance_increments is a vector of tuples (Address, increment as u128) + let balance_increments = withdrawals + .iter() + .filter(|withdrawal| withdrawal.amount > 0) + .map(|withdrawal| { + ( + RevmAddress::from_slice(withdrawal.address.as_bytes()), + (withdrawal.amount as u128 * GWEI_TO_WEI as u128), + ) + }) + .collect::>(); + + db.increment_balances(balance_increments)?; + } + EvmState::Execution(_) => { + // TODO: We should check withdrawals are valid + // (by checking that accounts exist if this is the only error) but there's no state to + // change. + } + } + Ok(()) +} + +pub fn block_env(header: &BlockHeader) -> BlockEnv { + BlockEnv { + number: RevmU256::from(header.number), + coinbase: RevmAddress(header.coinbase.0.into()), + timestamp: RevmU256::from(header.timestamp), + gas_limit: RevmU256::from(header.gas_limit), + basefee: RevmU256::from(header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE)), + difficulty: RevmU256::from_limbs(header.difficulty.0), + prevrandao: Some(header.prev_randao.as_fixed_bytes().into()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice::new( + header.excess_blob_gas.unwrap_or_default(), + )), + } +} + +// Used for the L2 +pub const WITHDRAWAL_MAGIC_DATA: &[u8] = b"burn"; +pub const DEPOSIT_MAGIC_DATA: &[u8] = b"mint"; +pub fn tx_env(tx: &Transaction) -> TxEnv { + let max_fee_per_blob_gas = tx + .max_fee_per_blob_gas() + .map(|x| RevmU256::from_be_bytes(x.to_big_endian())); + TxEnv { + caller: match tx { + Transaction::PrivilegedL2Transaction(tx) if tx.tx_type == PrivilegedTxType::Deposit => { + RevmAddress::ZERO + } + _ => RevmAddress(tx.sender().0.into()), + }, + gas_limit: tx.gas_limit(), + gas_price: RevmU256::from(tx.gas_price()), + transact_to: match tx { + Transaction::PrivilegedL2Transaction(tx) + if tx.tx_type == PrivilegedTxType::Withdrawal => + { + RevmTxKind::Call(RevmAddress::ZERO) + } + _ => match tx.to() { + TxKind::Call(address) => RevmTxKind::Call(address.0.into()), + TxKind::Create => RevmTxKind::Create, + }, + }, + value: RevmU256::from_limbs(tx.value().0), + data: match tx { + Transaction::PrivilegedL2Transaction(tx) => match tx.tx_type { + PrivilegedTxType::Deposit => DEPOSIT_MAGIC_DATA.into(), + PrivilegedTxType::Withdrawal => { + let to = match tx.to { + TxKind::Call(to) => to, + _ => Address::zero(), + }; + [Bytes::from(WITHDRAWAL_MAGIC_DATA), Bytes::from(to.0)] + .concat() + .into() + } + }, + _ => tx.data().clone().into(), + }, + nonce: Some(tx.nonce()), + chain_id: tx.chain_id(), + access_list: tx + .access_list() + .into_iter() + .map(|(addr, list)| { + let (address, storage_keys) = ( + RevmAddress(addr.0.into()), + list.into_iter() + .map(|a| FixedBytes::from_slice(a.as_bytes())) + .collect(), + ); + AccessListItem { + address, + storage_keys, + } + }) + .collect(), + gas_priority_fee: tx.max_priority_fee().map(RevmU256::from), + blob_hashes: tx + .blob_versioned_hashes() + .into_iter() + .map(|hash| B256::from(hash.0)) + .collect(), + max_fee_per_blob_gas, + // EIP7702 + // https://eips.ethereum.org/EIPS/eip-7702 + // The latest version of revm(19.3.0) is needed to run with the latest changes. + // NOTE: + // - rust 1.82.X is needed + // - rust-toolchain 1.82.X is needed (this can be found in ethrex/crates/vm/levm/rust-toolchain.toml) + authorization_list: tx.authorization_list().map(|list| { + list.into_iter() + .map(|auth_t| { + SignedAuthorization::new_unchecked( + RevmAuthorization { + chain_id: auth_t.chain_id.as_u64(), + address: RevmAddress(auth_t.address.0.into()), + nonce: auth_t.nonce, + }, + auth_t.y_parity.as_u32() as u8, + RevmU256::from_le_bytes(auth_t.r_signature.to_little_endian()), + RevmU256::from_le_bytes(auth_t.s_signature.to_little_endian()), + ) + }) + .collect::>() + .into() + }), + } +} + +// Used to estimate gas and create access lists +pub(crate) fn tx_env_from_generic(tx: &GenericTransaction, basefee: u64) -> TxEnv { + let gas_price = calculate_gas_price(tx, basefee); + TxEnv { + caller: RevmAddress(tx.from.0.into()), + gas_limit: tx.gas.unwrap_or(u64::MAX), // Ensure tx doesn't fail due to gas limit + gas_price, + transact_to: match tx.to { + TxKind::Call(address) => RevmTxKind::Call(address.0.into()), + TxKind::Create => RevmTxKind::Create, + }, + value: RevmU256::from_limbs(tx.value.0), + data: tx.input.clone().into(), + nonce: tx.nonce, + chain_id: tx.chain_id, + access_list: tx + .access_list + .iter() + .map(|list| { + let (address, storage_keys) = ( + RevmAddress::from_slice(list.address.as_bytes()), + list.storage_keys + .iter() + .map(|a| FixedBytes::from_slice(a.as_bytes())) + .collect(), + ); + AccessListItem { + address, + storage_keys, + } + }) + .collect(), + gas_priority_fee: tx.max_priority_fee_per_gas.map(RevmU256::from), + blob_hashes: tx + .blob_versioned_hashes + .iter() + .map(|hash| B256::from(hash.0)) + .collect(), + max_fee_per_blob_gas: tx.max_fee_per_blob_gas.map(|x| RevmU256::from_limbs(x.0)), + // EIP7702 + // https://eips.ethereum.org/EIPS/eip-7702 + // The latest version of revm(19.3.0) is needed to run with the latest changes. + // NOTE: + // - rust 1.82.X is needed + // - rust-toolchain 1.82.X is needed (this can be found in ethrex/crates/vm/levm/rust-toolchain.toml) + authorization_list: tx.authorization_list.clone().map(|list| { + list.into_iter() + .map(|auth_t| { + SignedAuthorization::new_unchecked( + RevmAuthorization { + //chain_id: RevmU256::from_le_bytes(auth_t.chain_id.to_little_endian()), + chain_id: auth_t.chain_id.as_u64(), + address: RevmAddress(auth_t.address.0.into()), + nonce: auth_t.nonce, + }, + auth_t.y_parity.as_u32() as u8, + RevmU256::from_le_bytes(auth_t.r.to_little_endian()), + RevmU256::from_le_bytes(auth_t.s.to_little_endian()), + ) + }) + .collect::>() + .into() + }), + } +} + +// Creates an AccessListInspector that will collect the accesses used by the evm execution +pub(crate) fn access_list_inspector( + tx_env: &TxEnv, + state: &mut EvmState, + spec_id: SpecId, +) -> Result { + // Access list provided by the transaction + let current_access_list = RevmAccessList(tx_env.access_list.clone()); + // Addresses accessed when using precompiles + let precompile_addresses = Precompiles::new(PrecompileSpecId::from_spec_id(spec_id)) + .addresses() + .cloned(); + // Address that is either called or created by the transaction + let to = match tx_env.transact_to { + RevmTxKind::Call(address) => address, + RevmTxKind::Create => { + let nonce = match state { + EvmState::Store(db) => db.basic(tx_env.caller)?, + EvmState::Execution(db) => db.basic(tx_env.caller)?, + } + .map(|info| info.nonce) + .unwrap_or_default(); + tx_env.caller.create(nonce) + } + }; + Ok(AccessListInspector::new( + current_access_list, + tx_env.caller, + to, + precompile_addresses, + )) +} + +/// Calculating gas_price according to EIP-1559 rules +/// See https://github.com/ethereum/go-ethereum/blob/7ee9a6e89f59cee21b5852f5f6ffa2bcfc05a25f/internal/ethapi/transaction_args.go#L430 +fn calculate_gas_price(tx: &GenericTransaction, basefee: u64) -> Uint<256, 4> { + if tx.gas_price != 0 { + // Legacy gas field was specified, use it + RevmU256::from(tx.gas_price) + } else { + // Backfill the legacy gas price for EVM execution, (zero if max_fee_per_gas is zero) + RevmU256::from(min( + tx.max_priority_fee_per_gas.unwrap_or(0) + basefee, + tx.max_fee_per_gas.unwrap_or(0), + )) + } +} + +/// When basefee tracking is disabled (ie. env.disable_base_fee = true; env.disable_block_gas_limit = true;) +/// and no gas prices were specified, lower the basefee to 0 to avoid breaking EVM invariants (basefee < feecap) +/// See https://github.com/ethereum/go-ethereum/blob/00294e9d28151122e955c7db4344f06724295ec5/core/vm/evm.go#L137 +fn adjust_disabled_base_fee( + block_env: &mut BlockEnv, + tx_gas_price: Uint<256, 4>, + tx_blob_gas_price: Option>, +) { + if tx_gas_price == RevmU256::from(0) { + block_env.basefee = RevmU256::from(0); + } + if tx_blob_gas_price.is_some_and(|v| v == RevmU256::from(0)) { + block_env.blob_excess_gas_and_price = None; + } +} + +/// Calls the eip4788 beacon block root system call contract +/// As of the Cancun hard-fork, parent_beacon_block_root needs to be present in the block header. +pub fn beacon_root_contract_call( + state: &mut EvmState, + header: &BlockHeader, + spec_id: SpecId, +) -> Result { + lazy_static! { + static ref SYSTEM_ADDRESS: RevmAddress = RevmAddress::from_slice( + &hex::decode("fffffffffffffffffffffffffffffffffffffffe").unwrap() + ); + static ref CONTRACT_ADDRESS: RevmAddress = RevmAddress::from_slice( + &hex::decode("000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap(), + ); + }; + let beacon_root = match header.parent_beacon_block_root { + None => { + return Err(EvmError::Header( + "parent_beacon_block_root field is missing".to_string(), + )) + } + Some(beacon_root) => beacon_root, + }; + + let tx_env = TxEnv { + caller: *SYSTEM_ADDRESS, + transact_to: RevmTxKind::Call(*CONTRACT_ADDRESS), + gas_limit: 30_000_000, + data: revm::primitives::Bytes::copy_from_slice(beacon_root.as_bytes()), + ..Default::default() + }; + let mut block_env = block_env(header); + block_env.basefee = RevmU256::ZERO; + block_env.gas_limit = RevmU256::from(30_000_000); + + match state { + EvmState::Store(db) => { + let mut evm = Evm::builder() + .with_db(db) + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .build(); + + let transaction_result = evm.transact()?; + let mut result_state = transaction_result.state; + result_state.remove(&*SYSTEM_ADDRESS); + result_state.remove(&evm.block().coinbase); + + evm.context.evm.db.commit(result_state); + + Ok(transaction_result.result.into()) + } + EvmState::Execution(db) => { + let mut evm = Evm::builder() + .with_db(db) + .with_block_env(block_env) + .with_tx_env(tx_env) + .with_spec_id(spec_id) + .build(); + + // Not necessary to commit to DB + let transaction_result = evm.transact()?; + Ok(transaction_result.result.into()) + } + } +} diff --git a/crates/vm/db.rs b/crates/vm/db.rs index 5528ee644..0110f9bf3 100644 --- a/crates/vm/db.rs +++ b/crates/vm/db.rs @@ -1,4 +1,8 @@ -use ethrex_core::{types::BlockHash, Address as CoreAddress, H256 as CoreH256}; +use crate::{execution_db::ExecutionDB, EvmError}; +use ethrex_core::{ + types::{BlockHash, ChainConfig}, + Address as CoreAddress, H256 as CoreH256, +}; use ethrex_storage::{error::StoreError, Store}; use revm::primitives::{ AccountInfo as RevmAccountInfo, Address as RevmAddress, Bytecode as RevmBytecode, @@ -10,57 +14,99 @@ pub struct StoreWrapper { pub block_hash: BlockHash, } -cfg_if::cfg_if! { - if #[cfg(feature = "levm")] { - use ethrex_core::{U256 as CoreU256}; - use ethrex_levm::db::Database as LevmDatabase; - - impl LevmDatabase for StoreWrapper { - fn get_account_info(&self, address: CoreAddress) -> ethrex_levm::account::AccountInfo { - let acc_info = self - .store - .get_account_info_by_hash(self.block_hash, address) - .unwrap() - .unwrap_or_default(); - - let acc_code = self - .store - .get_account_code(acc_info.code_hash) - .unwrap() - .unwrap_or_default(); - - ethrex_levm::account::AccountInfo { - balance: acc_info.balance, - nonce: acc_info.nonce, - bytecode: acc_code, - } - } - - fn account_exists(&self, address: CoreAddress) -> bool { - let acc_info = self - .store - .get_account_info_by_hash(self.block_hash, address) - .unwrap(); - - acc_info.is_some() - } - - fn get_storage_slot(&self, address: CoreAddress, key: CoreH256) -> CoreU256 { - self.store - .get_storage_at_hash(self.block_hash, address, key) - .unwrap() - .unwrap_or_default() - } - - fn get_block_hash(&self, block_number: u64) -> Option { - let a = self.store.get_block_header(block_number).unwrap(); - - a.map(|a| CoreH256::from(a.compute_block_hash().0)) - } +/// State used when running the EVM. The state can be represented with a [StoreWrapper] database, or +/// with a [ExecutionDB] in case we only want to store the necessary data for some particular +/// execution, for example when proving in L2 mode. +/// +/// Encapsulates state behaviour to be agnostic to the evm implementation for crate users. +pub enum EvmState { + Store(revm::db::State), + Execution(Box>), +} + +impl EvmState { + /// Get a reference to inner `Store` database + pub fn database(&self) -> Option<&Store> { + if let EvmState::Store(db) = self { + Some(&db.database.store) + } else { + None + } + } + + /// Gets the stored chain config + pub fn chain_config(&self) -> Result { + match self { + EvmState::Store(db) => db.database.store.get_chain_config().map_err(EvmError::from), + EvmState::Execution(db) => Ok(db.db.get_chain_config()), } } } +/// Builds EvmState from a Store +pub fn evm_state(store: Store, block_hash: BlockHash) -> EvmState { + EvmState::Store( + revm::db::State::builder() + .with_database(StoreWrapper { store, block_hash }) + .with_bundle_update() + .without_state_clear() + .build(), + ) +} + +impl From for EvmState { + fn from(value: ExecutionDB) -> Self { + EvmState::Execution(Box::new(revm::db::CacheDB::new(value))) + } +} + +use ethrex_core::U256 as CoreU256; +use ethrex_levm::db::Database as LevmDatabase; + +impl LevmDatabase for StoreWrapper { + fn get_account_info(&self, address: CoreAddress) -> ethrex_levm::account::AccountInfo { + let acc_info = self + .store + .get_account_info_by_hash(self.block_hash, address) + .unwrap() + .unwrap_or_default(); + + let acc_code = self + .store + .get_account_code(acc_info.code_hash) + .unwrap() + .unwrap_or_default(); + + ethrex_levm::account::AccountInfo { + balance: acc_info.balance, + nonce: acc_info.nonce, + bytecode: acc_code, + } + } + + fn account_exists(&self, address: CoreAddress) -> bool { + let acc_info = self + .store + .get_account_info_by_hash(self.block_hash, address) + .unwrap(); + + acc_info.is_some() + } + + fn get_storage_slot(&self, address: CoreAddress, key: CoreH256) -> CoreU256 { + self.store + .get_storage_at_hash(self.block_hash, address, key) + .unwrap() + .unwrap_or_default() + } + + fn get_block_hash(&self, block_number: u64) -> Option { + let a = self.store.get_block_header(block_number).unwrap(); + + a.map(|a| CoreH256::from(a.compute_block_hash().0)) + } +} + impl revm::Database for StoreWrapper { type Error = StoreError; diff --git a/crates/vm/errors.rs b/crates/vm/errors.rs index a80ae2a78..b440045fa 100644 --- a/crates/vm/errors.rs +++ b/crates/vm/errors.rs @@ -1,5 +1,6 @@ use ethereum_types::{H160, H256}; use ethrex_core::{types::BlockHash, Address}; +use ethrex_levm::errors::VMError; use ethrex_storage::error::StoreError; use ethrex_trie::TrieError; use revm::primitives::{ @@ -18,9 +19,11 @@ pub enum EvmError { #[error("Execution DB error: {0}")] ExecutionDB(#[from] ExecutionDBError), #[error("{0}")] - Custom(String), - #[error("{0}")] Precompile(String), + #[error("Invalid EVM or EVM not supported: {0}")] + InvalidEVM(String), + #[error("{0}")] + Custom(String), } #[derive(Debug, Error)] @@ -109,19 +112,14 @@ impl From> for EvmError { } } -cfg_if::cfg_if! { - if #[cfg(feature = "levm")] { - use ethrex_levm::errors::VMError; - impl From for EvmError { - fn from(value: VMError) -> Self { - if value.is_internal() { - // We don't categorize our internal errors yet, so we label them as "Custom" - EvmError::Custom(value.to_string()) - } else { - // If an error is not internal it means it is a transaction validation error. - EvmError::Transaction(value.to_string()) - } - } +impl From for EvmError { + fn from(value: VMError) -> Self { + if value.is_internal() { + // We don't categorize our internal errors yet, so we label them as "Custom" + EvmError::Custom(value.to_string()) + } else { + // If an error is not internal it means it is a transaction validation error. + EvmError::Transaction(value.to_string()) } } } diff --git a/crates/vm/execution_db.rs b/crates/vm/execution_db.rs index 2bf2b8caf..ead5f9a78 100644 --- a/crates/vm/execution_db.rs +++ b/crates/vm/execution_db.rs @@ -21,8 +21,10 @@ use revm_primitives::SpecId; use serde::{Deserialize, Serialize}; use crate::{ - block_env, db::StoreWrapper, errors::ExecutionDBError, evm_state, execute_block, - get_state_transitions, spec_id, tx_env, EvmError, + block_env, + db::{evm_state, StoreWrapper}, + errors::ExecutionDBError, + execute_block, get_state_transitions, spec_id, tx_env, EvmError, }; /// In-memory EVM database for single execution data. diff --git a/crates/vm/levm/Makefile b/crates/vm/levm/Makefile index 4d2d3048a..8739b3713 100644 --- a/crates/vm/levm/Makefile +++ b/crates/vm/levm/Makefile @@ -146,19 +146,24 @@ build-revm-comparison: ###### Build Client with LEVM ###### -FLAGS := "--features ethrex-blockchain/levm,ethrex-vm/levm,ethrex/levm" +STAMP_FILE := ../../../.docker_build_stamp +$(STAMP_FILE): + cd ../../../ + $(shell find crates cmd -type f -name '*.rs') Cargo.toml Dockerfile + docker build -t ethrex . + touch $(STAMP_FILE) -build-image-levm: ## ๐Ÿณ Build the Docker image with LEVM features - cd ../../../ && \ - docker build -t ethrex --build-arg BUILD_FLAGS=$(FLAGS) . +build-image-levm: $(STAMP_FILE) ## ๐Ÿณ Build the Docker image -run-hive-debug-levm: build-image-levm ## ๐Ÿ Run Hive with LEVM in debug mode +SIM_PARALLELISM := 48 +run-hive-levm: build-image-levm ## ๐Ÿ Run Hive with LEVM and Build report $(MAKE) -C ../../../ setup-hive - cd ../../../hive && ./hive --sim ethereum/rpc-compat --client ethrex --sim.limit "$(TEST_PATTERN)" --docker.output || exit 0 - cd ../../../hive && ./hive --sim devp2p --client ethrex --sim.limit "$(TEST_PATTERN)" --docker.output || exit 0 - cd ../../../hive && ./hive --sim ethereum/engine --client ethrex --sim.limit "$(TEST_PATTERN)" --docker.output || exit 0 - cd ../../../hive && ./hive --sim ethereum/sync --client ethrex --sim.limit "$(TEST_PATTERN)" --docker.output || exit 0 - cargo run --release -p hive_report + $(MAKE) -C ../../../ clean-hive-logs + cd ../../../hive && ./hive --ethrex.flags "--evm levm" --sim ethereum/rpc-compat --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd ../../../hive && ./hive --ethrex.flags "--evm levm" --sim devp2p --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd ../../../hive && ./hive --ethrex.flags "--evm levm" --sim ethereum/engine --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd ../../../hive && ./hive --ethrex.flags "--evm levm" --sim ethereum/sync --client ethrex --sim.limit "$(TEST_PATTERN)" --sim.parallelism $(SIM_PARALLELISM) || exit 0 + cd ../../../ && cargo run --release -p hive_report SUBDIRS := $(shell find $(VECTORS_DIR)/GeneralStateTests -maxdepth 1 -type d ! -path "$(VECTORS_DIR)/GeneralStateTests" -exec basename {} \;) diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 94fa62ab3..dafc616a0 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -1,3 +1,4 @@ +pub mod backends; pub mod db; pub mod errors; pub mod execution_db; @@ -5,408 +6,42 @@ mod execution_result; #[cfg(feature = "l2")] mod mods; -use db::StoreWrapper; -use execution_db::ExecutionDB; -use std::cmp::min; +use backends::EVM; +use db::EvmState; +use crate::backends::revm::*; use ethrex_core::{ types::{ - AccountInfo, Block, BlockHash, BlockHeader, ChainConfig, Fork, GenericTransaction, - PrivilegedTxType, Receipt, Transaction, TxKind, Withdrawal, GWEI_TO_WEI, INITIAL_BASE_FEE, + tx_fields::AccessList, AccountInfo, BlockHeader, ChainConfig, Fork, GenericTransaction, + INITIAL_BASE_FEE, }, Address, BigEndianHash, H256, U256, }; -use ethrex_storage::{error::StoreError, AccountUpdate, Store}; -use lazy_static::lazy_static; +use ethrex_storage::AccountUpdate; use revm::{ db::{states::bundle_state::BundleRetention, AccountState, AccountStatus}, inspector_handle_register, - inspectors::TracerEip3155, - precompile::{PrecompileSpecId, Precompiles}, - primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256}, - Database, DatabaseCommit, Evm, + primitives::{BlockEnv, TxEnv, B256}, + Evm, }; -use revm_inspectors::access_list::AccessListInspector; // Rename imported types for clarity -use revm_primitives::{ - ruint::Uint, AccessList as RevmAccessList, AccessListItem, Authorization as RevmAuthorization, - Bytes, FixedBytes, SignedAuthorization, TxKind as RevmTxKind, -}; +use revm_primitives::AccessList as RevmAccessList; // Export needed types pub use errors::EvmError; pub use execution_result::*; pub use revm::primitives::{Address as RevmAddress, SpecId, U256 as RevmU256}; -type AccessList = Vec<(Address, Vec)>; - -pub const WITHDRAWAL_MAGIC_DATA: &[u8] = b"burn"; -pub const DEPOSIT_MAGIC_DATA: &[u8] = b"mint"; - -/// State used when running the EVM. The state can be represented with a [StoreWrapper] database, or -/// with a [ExecutionDB] in case we only want to store the necessary data for some particular -/// execution, for example when proving in L2 mode. -/// -/// Encapsulates state behaviour to be agnostic to the evm implementation for crate users. -pub enum EvmState { - Store(revm::db::State), - Execution(Box>), -} - -impl EvmState { - /// Get a reference to inner `Store` database - pub fn database(&self) -> Option<&Store> { - if let EvmState::Store(db) = self { - Some(&db.database.store) - } else { - None - } - } - - /// Gets the stored chain config - pub fn chain_config(&self) -> Result { - match self { - EvmState::Store(db) => db.database.store.get_chain_config().map_err(EvmError::from), - EvmState::Execution(db) => Ok(db.db.get_chain_config()), - } - } -} - -impl From for EvmState { - fn from(value: ExecutionDB) -> Self { - EvmState::Execution(Box::new(revm::db::CacheDB::new(value))) - } -} - -cfg_if::cfg_if! { - if #[cfg(feature = "levm")] { - use ethrex_levm::{ - db::{CacheDB, Database as LevmDatabase}, - errors::{ExecutionReport, TxResult, VMError}, - vm::{VM, EVMConfig}, - Environment, - Account - }; - use std::{collections::HashMap, sync::Arc}; - use ethrex_core::types::code_hash; - - /// Calls the eip4788 beacon block root system call contract - /// More info on https://eips.ethereum.org/EIPS/eip-4788 - pub fn beacon_root_contract_call_levm( - store_wrapper: Arc, - block_header: &BlockHeader, - config: EVMConfig, - ) -> Result { - lazy_static! { - static ref SYSTEM_ADDRESS: Address = - Address::from_slice(&hex::decode("fffffffffffffffffffffffffffffffffffffffe").unwrap()); - static ref CONTRACT_ADDRESS: Address = - Address::from_slice(&hex::decode("000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap(),); - }; - // This is OK - let beacon_root = match block_header.parent_beacon_block_root { - None => { - return Err(EvmError::Header( - "parent_beacon_block_root field is missing".to_string(), - )) - } - Some(beacon_root) => beacon_root, - }; - - let env = Environment { - origin: *SYSTEM_ADDRESS, - gas_limit: 30_000_000, - block_number: block_header.number.into(), - coinbase: block_header.coinbase, - timestamp: block_header.timestamp.into(), - prev_randao: Some(block_header.prev_randao), - base_fee_per_gas: U256::zero(), - gas_price: U256::zero(), - block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from), - block_blob_gas_used: block_header.blob_gas_used.map(U256::from), - block_gas_limit: 30_000_000, - transient_storage: HashMap::new(), - config, - ..Default::default() - }; - - let calldata = Bytes::copy_from_slice(beacon_root.as_bytes()).into(); - - // Here execute with LEVM but just return transaction report. And I will handle it in the calling place. - - let mut vm = VM::new( - TxKind::Call(*CONTRACT_ADDRESS), - env, - U256::zero(), - calldata, - store_wrapper, - CacheDB::new(), - vec![], - None - ) - .map_err(EvmError::from)?; - - let mut report = vm.execute().map_err(EvmError::from)?; - - report.new_state.remove(&*SYSTEM_ADDRESS); - - Ok(report) +use std::sync::OnceLock; - } - - pub fn get_state_transitions_levm( - initial_state: &EvmState, - block_hash: H256, - new_state: &CacheDB, - ) -> Vec { - let current_db = match initial_state { - EvmState::Store(state) => state.database.store.clone(), - EvmState::Execution(_cache_db) => unreachable!("Execution state should not be passed here"), - }; - let mut account_updates: Vec = vec![]; - for (new_state_account_address, new_state_account) in new_state { - let initial_account_state = current_db - .get_account_info_by_hash(block_hash, *new_state_account_address) - .expect("Error getting account info by address"); - - if initial_account_state.is_none() { - // New account, update everything - let new_account = AccountUpdate{ - address: *new_state_account_address , - removed: new_state_account.is_empty(), - info: Some(AccountInfo { - code_hash: code_hash(&new_state_account.info.bytecode), - balance: new_state_account.info.balance, - nonce: new_state_account.info.nonce, - }), - code: Some(new_state_account.info.bytecode.clone()), - added_storage: new_state_account.storage.iter().map(|(key, storage_slot)| (*key, storage_slot.current_value)).collect(), - }; - - account_updates.push(new_account); - continue; - } - - // This unwrap is safe, just checked upside - let initial_account_state = initial_account_state.unwrap(); - let mut account_update = AccountUpdate::new(*new_state_account_address); - - // Account state after block execution. - let new_state_acc_info = AccountInfo { - code_hash: code_hash(&new_state_account.info.bytecode), - balance: new_state_account.info.balance, - nonce: new_state_account.info.nonce, - }; - - // Compare Account Info - if initial_account_state != new_state_acc_info { - account_update.info = Some(new_state_acc_info.clone()); - } - - // If code hash is different it means the code is different too. - if initial_account_state.code_hash != new_state_acc_info.code_hash { - account_update.code = Some(new_state_account.info.bytecode.clone()); - } +// This global variable can be initialized by the ethrex cli. +// EVM_BACKEND.get_or_init(|| evm); +// Then, we can retrieve the evm with: +// EVM_BACKEND.get(); -> returns Option +pub static EVM_BACKEND: OnceLock = OnceLock::new(); - let mut updated_storage = HashMap::new(); - for (key, storage_slot) in &new_state_account.storage { - // original_value in storage_slot is not the original_value on the DB, be careful. - let original_value = current_db.get_storage_at_hash(block_hash, *new_state_account_address, *key).unwrap().unwrap_or_default(); // Option inside result, I guess I have to assume it is zero. - - if original_value != storage_slot.current_value { - updated_storage.insert(*key, storage_slot.current_value); - } - } - account_update.added_storage = updated_storage; - - account_update.removed = new_state_account.is_empty(); - - if account_update != AccountUpdate::new(*new_state_account_address) { - account_updates.push(account_update); - } - } - account_updates - } - - /// Executes all transactions in a block and returns their receipts. - pub fn execute_block( - block: &Block, - state: &mut EvmState, - ) -> Result<(Vec, Vec), EvmError> { - let store_wrapper = Arc::new(StoreWrapper { - store: state.database().unwrap().clone(), - block_hash: block.header.parent_hash, - }); - - let mut block_cache: CacheDB = HashMap::new(); - let block_header = &block.header; - let fork = state.chain_config()?.fork(block_header.timestamp); - // If there's no blob schedule in chain_config use the - // default/canonical values - let blob_schedule = state.chain_config()?.get_fork_blob_schedule(block_header.timestamp) - .unwrap_or(EVMConfig::canonical_values(fork)); - let config = EVMConfig::new(fork , blob_schedule); - cfg_if::cfg_if! { - if #[cfg(not(feature = "l2"))] { - if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun { - let report = beacon_root_contract_call_levm(store_wrapper.clone(), block_header, config)?; - block_cache.extend(report.new_state); - } - } - } - - - // Account updates are initialized like this because of the beacon_root_contract_call, it is going to be empty if it wasn't called. - let mut account_updates = get_state_transitions(state); - - let mut receipts = Vec::new(); - let mut cumulative_gas_used = 0; - - - for tx in block.body.transactions.iter() { - let report = execute_tx_levm(tx, block_header, store_wrapper.clone(), block_cache.clone(), config).map_err(EvmError::from)?; - - let mut new_state = report.new_state.clone(); - - // Now original_value is going to be the same as the current_value, for the next transaction. - // It should have only one value but it is convenient to keep on using our CacheDB structure - for account in new_state.values_mut() { - for storage_slot in account.storage.values_mut() { - storage_slot.original_value = storage_slot.current_value; - } - } - - block_cache.extend(new_state); - - // Currently, in LEVM, we don't substract refunded gas to used gas, but that can change in the future. - let gas_used = report.gas_used - report.gas_refunded; - cumulative_gas_used += gas_used; - let receipt = Receipt::new( - tx.tx_type(), - matches!(report.result.clone(), TxResult::Success), - cumulative_gas_used, - report.logs.clone(), - ); - - receipts.push(receipt); - } - - // Here we update block_cache with balance increments caused by withdrawals. - if let Some(withdrawals) = &block.body.withdrawals { - // For every withdrawal we increment the target account's balance - for (address, increment) in withdrawals.iter().filter(|withdrawal| withdrawal.amount > 0).map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI))) { - // We check if it was in block_cache, if not, we get it from DB. - let mut account = block_cache.get(&address).cloned().unwrap_or({ - let acc_info = store_wrapper.get_account_info(address); - Account::from(acc_info) - }); - - account.info.balance += increment.into(); - - block_cache.insert(address, account); - } - } - - - account_updates.extend(get_state_transitions_levm(state, block.header.parent_hash, &block_cache)); - - Ok((receipts, account_updates)) - } - - pub fn execute_tx_levm( - tx: &Transaction, - block_header: &BlockHeader, - db: Arc, - block_cache: CacheDB, - config: EVMConfig, - ) -> Result { - let gas_price : U256 = tx.effective_gas_price(block_header.base_fee_per_gas).ok_or(VMError::InvalidTransaction)?.into(); - - let env = Environment { - origin: tx.sender(), - refunded_gas: 0, - gas_limit: tx.gas_limit(), - config, - block_number: block_header.number.into(), - coinbase: block_header.coinbase, - timestamp: block_header.timestamp.into(), - prev_randao: Some(block_header.prev_randao), - chain_id: tx.chain_id().unwrap_or_default().into(), - base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(), - gas_price, - block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from), - block_blob_gas_used: block_header.blob_gas_used.map(U256::from), - tx_blob_hashes: tx.blob_versioned_hashes(), - tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from), - tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from), - tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas().map(U256::from), - tx_nonce: tx.nonce(), - block_gas_limit: block_header.gas_limit, - transient_storage: HashMap::new(), - }; - - let mut vm = VM::new( - tx.to(), - env, - tx.value(), - tx.data().clone(), - db, - block_cache, - tx.access_list(), - tx.authorization_list(), - )?; - - vm.execute() - } - } else if #[cfg(not(feature = "levm"))] { - /// Executes all transactions in a block and returns their receipts. - pub fn execute_block(block: &Block, state: &mut EvmState) -> Result, EvmError> { - let block_header = &block.header; - let spec_id = spec_id(&state.chain_config()?, block_header.timestamp); - //eip 4788: execute beacon_root_contract_call before block transactions - cfg_if::cfg_if! { - if #[cfg(not(feature = "l2"))] { - //eip 4788: execute beacon_root_contract_call before block transactions - if block_header.parent_beacon_block_root.is_some() && spec_id >= SpecId::CANCUN { - beacon_root_contract_call(state, block_header, spec_id)?; - } - } - } - let mut receipts = Vec::new(); - let mut cumulative_gas_used = 0; - - for transaction in block.body.transactions.iter() { - let result = execute_tx(transaction, block_header, state, spec_id)?; - cumulative_gas_used += result.gas_used(); - let receipt = Receipt::new( - transaction.tx_type(), - result.is_success(), - cumulative_gas_used, - result.logs(), - ); - receipts.push(receipt); - } - - if let Some(withdrawals) = &block.body.withdrawals { - process_withdrawals(state, withdrawals)?; - } - - Ok(receipts) - } - } -} - -// Executes a single tx, doesn't perform state transitions -pub fn execute_tx( - tx: &Transaction, - header: &BlockHeader, - state: &mut EvmState, - spec_id: SpecId, -) -> Result { - let block_env = block_env(header); - let tx_env = tx_env(tx); - run_evm(tx_env, block_env, state, spec_id) -} +// ================== Commonly used functions ====================== +// TODO: IMPLEMENT FOR LEVM // Executes a single GenericTransaction, doesn't commit the result or perform state transitions pub fn simulate_tx_from_generic( tx: &GenericTransaction, @@ -419,71 +54,7 @@ pub fn simulate_tx_from_generic( run_without_commit(tx_env, block_env, state, spec_id) } -/// When basefee tracking is disabled (ie. env.disable_base_fee = true; env.disable_block_gas_limit = true;) -/// and no gas prices were specified, lower the basefee to 0 to avoid breaking EVM invariants (basefee < feecap) -/// See https://github.com/ethereum/go-ethereum/blob/00294e9d28151122e955c7db4344f06724295ec5/core/vm/evm.go#L137 -fn adjust_disabled_base_fee( - block_env: &mut BlockEnv, - tx_gas_price: Uint<256, 4>, - tx_blob_gas_price: Option>, -) { - if tx_gas_price == RevmU256::from(0) { - block_env.basefee = RevmU256::from(0); - } - if tx_blob_gas_price.is_some_and(|v| v == RevmU256::from(0)) { - block_env.blob_excess_gas_and_price = None; - } -} - -/// Runs EVM, doesn't perform state transitions, but stores them -fn run_evm( - tx_env: TxEnv, - block_env: BlockEnv, - state: &mut EvmState, - spec_id: SpecId, -) -> Result { - let tx_result = { - let chain_spec = state.chain_config()?; - #[allow(unused_mut)] - let mut evm_builder = Evm::builder() - .with_block_env(block_env) - .with_tx_env(tx_env) - .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) - .with_spec_id(spec_id) - .with_external_context( - TracerEip3155::new(Box::new(std::io::stderr())).without_summary(), - ); - cfg_if::cfg_if! { - if #[cfg(feature = "l2")] { - use revm::{Handler, primitives::{CancunSpec, HandlerCfg}}; - use std::sync::Arc; - - evm_builder = evm_builder.with_handler({ - let mut evm_handler = Handler::new(HandlerCfg::new(SpecId::LATEST)); - evm_handler.pre_execution.deduct_caller = Arc::new(mods::deduct_caller::); - evm_handler.validation.tx_against_state = Arc::new(mods::validate_tx_against_state::); - evm_handler.execution.last_frame_return = Arc::new(mods::last_frame_return::); - // TODO: Override `end` function. We should deposit even if we revert. - // evm_handler.pre_execution.end - evm_handler - }); - } - } - - match state { - EvmState::Store(db) => { - let mut evm = evm_builder.with_db(db).build(); - evm.transact_commit().map_err(EvmError::from)? - } - EvmState::Execution(db) => { - let mut evm = evm_builder.with_db(db).build(); - evm.transact_commit().map_err(EvmError::from)? - } - } - }; - Ok(tx_result.into()) -} - +// TODO: IMPLEMENT FOR LEVM /// Runs the transaction and returns the access list and estimated gas use (when running the tx with said access list) pub fn create_access_list( tx: &GenericTransaction, @@ -521,6 +92,7 @@ pub fn create_access_list( Ok((execution_result, access_list)) } +// TODO: IMPLEMENT FOR LEVM /// Runs the transaction and returns the access list for it fn create_access_list_inner( tx_env: TxEnv, @@ -563,42 +135,6 @@ fn create_access_list_inner( Ok((tx_result.result.into(), access_list)) } -/// Runs the transaction and returns the result, but does not commit it. -fn run_without_commit( - tx_env: TxEnv, - mut block_env: BlockEnv, - state: &mut EvmState, - spec_id: SpecId, -) -> Result { - adjust_disabled_base_fee( - &mut block_env, - tx_env.gas_price, - tx_env.max_fee_per_blob_gas, - ); - let chain_config = state.chain_config()?; - #[allow(unused_mut)] - let mut evm_builder = Evm::builder() - .with_block_env(block_env) - .with_tx_env(tx_env) - .with_spec_id(spec_id) - .modify_cfg_env(|env| { - env.disable_base_fee = true; - env.disable_block_gas_limit = true; - env.chain_id = chain_config.chain_id; - }); - let tx_result = match state { - EvmState::Store(db) => { - let mut evm = evm_builder.with_db(db).build(); - evm.transact().map_err(EvmError::from)? - } - EvmState::Execution(db) => { - let mut evm = evm_builder.with_db(db).build(); - evm.transact().map_err(EvmError::from)? - } - }; - Ok(tx_result.result.into()) -} - /// Merges transitions stored when executing transactions and returns the resulting account updates /// Doesn't update the DB pub fn get_state_transitions(state: &mut EvmState) -> Vec { @@ -727,320 +263,6 @@ pub fn get_state_transitions(state: &mut EvmState) -> Vec { } } -/// Processes a block's withdrawals, updating the account balances in the state -pub fn process_withdrawals( - state: &mut EvmState, - withdrawals: &[Withdrawal], -) -> Result<(), StoreError> { - match state { - EvmState::Store(db) => { - //balance_increments is a vector of tuples (Address, increment as u128) - let balance_increments = withdrawals - .iter() - .filter(|withdrawal| withdrawal.amount > 0) - .map(|withdrawal| { - ( - RevmAddress::from_slice(withdrawal.address.as_bytes()), - (withdrawal.amount as u128 * GWEI_TO_WEI as u128), - ) - }) - .collect::>(); - - db.increment_balances(balance_increments)?; - } - EvmState::Execution(_) => { - // TODO: We should check withdrawals are valid - // (by checking that accounts exist if this is the only error) but there's no state to - // change. - } - } - Ok(()) -} - -/// Builds EvmState from a Store -pub fn evm_state(store: Store, block_hash: BlockHash) -> EvmState { - EvmState::Store( - revm::db::State::builder() - .with_database(StoreWrapper { store, block_hash }) - .with_bundle_update() - .without_state_clear() - .build(), - ) -} - -/// Calls the eip4788 beacon block root system call contract -/// As of the Cancun hard-fork, parent_beacon_block_root needs to be present in the block header. -pub fn beacon_root_contract_call( - state: &mut EvmState, - header: &BlockHeader, - spec_id: SpecId, -) -> Result { - lazy_static! { - static ref SYSTEM_ADDRESS: RevmAddress = RevmAddress::from_slice( - &hex::decode("fffffffffffffffffffffffffffffffffffffffe").unwrap() - ); - static ref CONTRACT_ADDRESS: RevmAddress = RevmAddress::from_slice( - &hex::decode("000F3df6D732807Ef1319fB7B8bB8522d0Beac02").unwrap(), - ); - }; - let beacon_root = match header.parent_beacon_block_root { - None => { - return Err(EvmError::Header( - "parent_beacon_block_root field is missing".to_string(), - )) - } - Some(beacon_root) => beacon_root, - }; - - let tx_env = TxEnv { - caller: *SYSTEM_ADDRESS, - transact_to: RevmTxKind::Call(*CONTRACT_ADDRESS), - gas_limit: 30_000_000, - data: revm::primitives::Bytes::copy_from_slice(beacon_root.as_bytes()), - ..Default::default() - }; - let mut block_env = block_env(header); - block_env.basefee = RevmU256::ZERO; - block_env.gas_limit = RevmU256::from(30_000_000); - - match state { - EvmState::Store(db) => { - let mut evm = Evm::builder() - .with_db(db) - .with_block_env(block_env) - .with_tx_env(tx_env) - .with_spec_id(spec_id) - .build(); - - let transaction_result = evm.transact()?; - let mut result_state = transaction_result.state; - result_state.remove(&*SYSTEM_ADDRESS); - result_state.remove(&evm.block().coinbase); - - evm.context.evm.db.commit(result_state); - - Ok(transaction_result.result.into()) - } - EvmState::Execution(db) => { - let mut evm = Evm::builder() - .with_db(db) - .with_block_env(block_env) - .with_tx_env(tx_env) - .with_spec_id(spec_id) - .build(); - - // Not necessary to commit to DB - let transaction_result = evm.transact()?; - Ok(transaction_result.result.into()) - } - } -} - -pub fn block_env(header: &BlockHeader) -> BlockEnv { - BlockEnv { - number: RevmU256::from(header.number), - coinbase: RevmAddress(header.coinbase.0.into()), - timestamp: RevmU256::from(header.timestamp), - gas_limit: RevmU256::from(header.gas_limit), - basefee: RevmU256::from(header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE)), - difficulty: RevmU256::from_limbs(header.difficulty.0), - prevrandao: Some(header.prev_randao.as_fixed_bytes().into()), - blob_excess_gas_and_price: Some(BlobExcessGasAndPrice::new( - header.excess_blob_gas.unwrap_or_default(), - )), - } -} - -pub fn tx_env(tx: &Transaction) -> TxEnv { - let max_fee_per_blob_gas = tx - .max_fee_per_blob_gas() - .map(|x| RevmU256::from_be_bytes(x.to_big_endian())); - TxEnv { - caller: match tx { - Transaction::PrivilegedL2Transaction(tx) if tx.tx_type == PrivilegedTxType::Deposit => { - RevmAddress::ZERO - } - _ => RevmAddress(tx.sender().0.into()), - }, - gas_limit: tx.gas_limit(), - gas_price: RevmU256::from(tx.gas_price()), - transact_to: match tx { - Transaction::PrivilegedL2Transaction(tx) - if tx.tx_type == PrivilegedTxType::Withdrawal => - { - RevmTxKind::Call(RevmAddress::ZERO) - } - _ => match tx.to() { - TxKind::Call(address) => RevmTxKind::Call(address.0.into()), - TxKind::Create => RevmTxKind::Create, - }, - }, - value: RevmU256::from_limbs(tx.value().0), - data: match tx { - Transaction::PrivilegedL2Transaction(tx) => match tx.tx_type { - PrivilegedTxType::Deposit => DEPOSIT_MAGIC_DATA.into(), - PrivilegedTxType::Withdrawal => { - let to = match tx.to { - TxKind::Call(to) => to, - _ => Address::zero(), - }; - [Bytes::from(WITHDRAWAL_MAGIC_DATA), Bytes::from(to.0)] - .concat() - .into() - } - }, - _ => tx.data().clone().into(), - }, - nonce: Some(tx.nonce()), - chain_id: tx.chain_id(), - access_list: tx - .access_list() - .into_iter() - .map(|(addr, list)| { - let (address, storage_keys) = ( - RevmAddress(addr.0.into()), - list.into_iter() - .map(|a| FixedBytes::from_slice(a.as_bytes())) - .collect(), - ); - AccessListItem { - address, - storage_keys, - } - }) - .collect(), - gas_priority_fee: tx.max_priority_fee().map(RevmU256::from), - blob_hashes: tx - .blob_versioned_hashes() - .into_iter() - .map(|hash| B256::from(hash.0)) - .collect(), - max_fee_per_blob_gas, - // EIP7702 - // https://eips.ethereum.org/EIPS/eip-7702 - // The latest version of revm(19.3.0) is needed to run with the latest changes. - // NOTE: - // - rust 1.82.X is needed - // - rust-toolchain 1.82.X is needed (this can be found in ethrex/crates/vm/levm/rust-toolchain.toml) - authorization_list: tx.authorization_list().map(|list| { - list.into_iter() - .map(|auth_t| { - SignedAuthorization::new_unchecked( - RevmAuthorization { - chain_id: auth_t.chain_id.as_u64(), - address: RevmAddress(auth_t.address.0.into()), - nonce: auth_t.nonce, - }, - auth_t.y_parity.as_u32() as u8, - RevmU256::from_le_bytes(auth_t.r_signature.to_little_endian()), - RevmU256::from_le_bytes(auth_t.s_signature.to_little_endian()), - ) - }) - .collect::>() - .into() - }), - } -} - -// Used to estimate gas and create access lists -fn tx_env_from_generic(tx: &GenericTransaction, basefee: u64) -> TxEnv { - let gas_price = calculate_gas_price(tx, basefee); - TxEnv { - caller: RevmAddress(tx.from.0.into()), - gas_limit: tx.gas.unwrap_or(u64::MAX), // Ensure tx doesn't fail due to gas limit - gas_price, - transact_to: match tx.to { - TxKind::Call(address) => RevmTxKind::Call(address.0.into()), - TxKind::Create => RevmTxKind::Create, - }, - value: RevmU256::from_limbs(tx.value.0), - data: tx.input.clone().into(), - nonce: tx.nonce, - chain_id: tx.chain_id, - access_list: tx - .access_list - .iter() - .map(|list| { - let (address, storage_keys) = ( - RevmAddress::from_slice(list.address.as_bytes()), - list.storage_keys - .iter() - .map(|a| FixedBytes::from_slice(a.as_bytes())) - .collect(), - ); - AccessListItem { - address, - storage_keys, - } - }) - .collect(), - gas_priority_fee: tx.max_priority_fee_per_gas.map(RevmU256::from), - blob_hashes: tx - .blob_versioned_hashes - .iter() - .map(|hash| B256::from(hash.0)) - .collect(), - max_fee_per_blob_gas: tx.max_fee_per_blob_gas.map(|x| RevmU256::from_limbs(x.0)), - // EIP7702 - // https://eips.ethereum.org/EIPS/eip-7702 - // The latest version of revm(19.3.0) is needed to run with the latest changes. - // NOTE: - // - rust 1.82.X is needed - // - rust-toolchain 1.82.X is needed (this can be found in ethrex/crates/vm/levm/rust-toolchain.toml) - authorization_list: tx.authorization_list.clone().map(|list| { - list.into_iter() - .map(|auth_t| { - SignedAuthorization::new_unchecked( - RevmAuthorization { - //chain_id: RevmU256::from_le_bytes(auth_t.chain_id.to_little_endian()), - chain_id: auth_t.chain_id.as_u64(), - address: RevmAddress(auth_t.address.0.into()), - nonce: auth_t.nonce, - }, - auth_t.y_parity.as_u32() as u8, - RevmU256::from_le_bytes(auth_t.r.to_little_endian()), - RevmU256::from_le_bytes(auth_t.s.to_little_endian()), - ) - }) - .collect::>() - .into() - }), - } -} - -// Creates an AccessListInspector that will collect the accesses used by the evm execution -fn access_list_inspector( - tx_env: &TxEnv, - state: &mut EvmState, - spec_id: SpecId, -) -> Result { - // Access list provided by the transaction - let current_access_list = RevmAccessList(tx_env.access_list.clone()); - // Addresses accessed when using precompiles - let precompile_addresses = Precompiles::new(PrecompileSpecId::from_spec_id(spec_id)) - .addresses() - .cloned(); - // Address that is either called or created by the transaction - let to = match tx_env.transact_to { - RevmTxKind::Call(address) => address, - RevmTxKind::Create => { - let nonce = match state { - EvmState::Store(db) => db.basic(tx_env.caller)?, - EvmState::Execution(db) => db.basic(tx_env.caller)?, - } - .map(|info| info.nonce) - .unwrap_or_default(); - tx_env.caller.create(nonce) - } - }; - Ok(AccessListInspector::new( - current_access_list, - tx_env.caller, - to, - precompile_addresses, - )) -} - /// Returns the spec id according to the block timestamp and the stored chain config /// WARNING: Assumes at least Merge fork is active pub fn spec_id(chain_config: &ChainConfig, block_timestamp: u64) -> SpecId { @@ -1071,23 +293,3 @@ pub fn fork_to_spec_id(fork: Fork) -> SpecId { Fork::Osaka => SpecId::OSAKA, } } - -/// Calculating gas_price according to EIP-1559 rules -/// See https://github.com/ethereum/go-ethereum/blob/7ee9a6e89f59cee21b5852f5f6ffa2bcfc05a25f/internal/ethapi/transaction_args.go#L430 -fn calculate_gas_price(tx: &GenericTransaction, basefee: u64) -> Uint<256, 4> { - if tx.gas_price != 0 { - // Legacy gas field was specified, use it - RevmU256::from(tx.gas_price) - } else { - // Backfill the legacy gas price for EVM execution, (zero if max_fee_per_gas is zero) - RevmU256::from(min( - tx.max_priority_fee_per_gas.unwrap_or(0) + basefee, - tx.max_fee_per_gas.unwrap_or(0), - )) - } -} - -// USED for revm ^19.0.0 -//pub fn is_prague(spec_id: SpecId) -> bool { -// spec_id >= SpecId::PRAGUE -//}