diff --git a/CHANGELOG.md b/CHANGELOG.md index afc456c1b7..7cdb3afbfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,74 @@ Because this is workspace with multi libraries, tags will be simplified, and with this document you can match version of project with git tag. +# v82 +date 14.07.2025 + +Fix for inspector not calling `step_end`. + +* `revm-context`: 8.0.2 -> 8.0.3 (✓ API compatible changes) +* `revm-interpreter`: 23.0.1 -> 23.0.2 (✓ API compatible changes) +* `revm-precompile`: 24.0.0 -> 24.0.1 (✓ API compatible changes) +* `revm-handler`: 8.0.2 -> 8.0.3 (✓ API compatible changes) +* `revm-inspector`: 8.0.2 -> 8.0.3 (✓ API compatible changes) +* `revme`: 7.0.3 -> 7.0.4 (✓ API compatible changes) +* `op-revm`: 8.0.2 -> 8.0.3 (✓ API compatible changes) +* `custom_precompile_journal`: 0.1.0 +* `revm`: 27.0.2 -> 27.0.3 +* `revm-statetest-types`: 8.0.3 -> 8.0.4 + +# v81 +date: 03.07.2025 + +Fix inspector step_end panic for opcode fn. + +* `revm-bytecode`: 6.0.0 -> 6.0.1 (✓ API compatible changes) +* `revm-handler`: 8.0.1 -> 8.0.2 (✓ API compatible changes) +* `revm-inspector`: 8.0.1 -> 8.0.2 (✓ API compatible changes) +* `revme`: 7.0.2 -> 7.0.3 (✓ API compatible changes) +* `custom_precompile_journal`: 0.1.0 +* `revm-state`: 7.0.0 -> 7.0.1 +* `revm-database-interface`: 7.0.0 -> 7.0.1 +* `revm-context-interface`: 8.0.0 -> 8.0.1 +* `revm-context`: 8.0.1 -> 8.0.2 +* `revm-database`: 7.0.0 -> 7.0.1 +* `revm-interpreter`: 23.0.0 -> 23.0.1 +* `revm`: 27.0.1 -> 27.0.2 +* `revm-statetest-types`: 8.0.2 -> 8.0.3 +* `op-revm`: 8.0.1 -> 8.0.2 + +# v80 +date 01.07.2025 + +Fix `build` and `build_fill` for OpTransactionBuilder + +* `revm-context`: 8.0.0 -> 8.0.1 (✓ API compatible changes) +* `revm-handler`: 8.0.0 -> 8.0.1 (✓ API compatible changes) +* `revm-inspector`: 8.0.0 -> 8.0.1 (✓ API compatible changes) +* `revm`: 27.0.0 -> 27.0.1 (✓ API compatible changes) +* `op-revm`: 8.0.0 -> 8.0.1 (✓ API compatible changes) + +# v79 +date: 01.07.2025 + +Fix for bytecode eq operation. + +* `revm-bytecode`: 5.0.0 -> 6.0.0 (⚠ API breaking changes) +* `revm-state`: 6.0.0 -> 7.0.0 (✓ API compatible changes) +* `revm-database-interface`: 6.0.0 -> 7.0.0 (✓ API compatible changes) +* `revm-context-interface`: 7.0.1 -> 8.0.0 (⚠ API breaking changes) +* `revm-context`: 7.0.1 -> 8.0.0 (✓ API compatible changes) +* `revm-interpreter`: 22.0.1 -> 23.0.0 (✓ API compatible changes) +* `revm-precompile`: 23.0.0 -> 24.0.0 (✓ API compatible changes) +* `revm-handler`: 7.0.1 -> 8.0.0 (⚠ API breaking changes) +* `revm-inspector`: 7.0.1 -> 8.0.0 (✓ API compatible changes) +* `revm`: 26.0.1 -> 27.0.0 (✓ API compatible changes) +* `revm-statetest-types`: 8.0.1 -> 8.0.2 (✓ API compatible changes) +* `revme`: 7.0.1 -> 7.0.2 (✓ API compatible changes) +* `op-revm`: 7.0.1 -> 8.0.0 (⚠ API breaking changes) +* `revm-database`: 6.0.0 -> 7.0.0 + # v78 -date 20.05.2025 +date: 20.05.2025 Quick fix for not calling `frame_stack.clear()` https://github.com/bluealloy/revm/pull/2656 diff --git a/Cargo.lock b/Cargo.lock index 390ff12c51..c996bea1c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1560,6 +1560,14 @@ dependencies = [ "memchr", ] +[[package]] +name = "custom_precompile_journal" +version = "0.1.0" +dependencies = [ + "anyhow", + "revm", +] + [[package]] name = "darling" version = "0.20.11" @@ -3026,7 +3034,7 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "op-revm" -version = "7.0.1" +version = "8.0.3" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -3765,7 +3773,7 @@ dependencies = [ [[package]] name = "revm" -version = "26.0.1" +version = "27.0.3" dependencies = [ "revm-bytecode", "revm-context", @@ -3784,7 +3792,7 @@ dependencies = [ [[package]] name = "revm-bytecode" -version = "5.0.0" +version = "6.0.1" dependencies = [ "bitvec", "once_cell", @@ -3796,7 +3804,7 @@ dependencies = [ [[package]] name = "revm-context" -version = "7.0.1" +version = "8.0.3" dependencies = [ "cfg-if", "derive-where", @@ -3811,7 +3819,7 @@ dependencies = [ [[package]] name = "revm-context-interface" -version = "7.0.1" +version = "8.0.1" dependencies = [ "alloy-eip2930", "alloy-eip7702", @@ -3825,7 +3833,7 @@ dependencies = [ [[package]] name = "revm-database" -version = "6.0.0" +version = "7.0.1" dependencies = [ "alloy-eips", "alloy-provider", @@ -3843,10 +3851,11 @@ dependencies = [ [[package]] name = "revm-database-interface" -version = "6.0.0" +version = "7.0.1" dependencies = [ "anyhow", "auto_impl", + "either", "revm-primitives", "revm-state", "rstest", @@ -3856,7 +3865,7 @@ dependencies = [ [[package]] name = "revm-handler" -version = "7.0.1" +version = "8.0.3" dependencies = [ "alloy-eip7702", "alloy-provider", @@ -3878,7 +3887,7 @@ dependencies = [ [[package]] name = "revm-inspector" -version = "7.0.1" +version = "8.0.3" dependencies = [ "auto_impl", "either", @@ -3895,7 +3904,7 @@ dependencies = [ [[package]] name = "revm-interpreter" -version = "22.0.1" +version = "23.0.2" dependencies = [ "bincode 2.0.1", "revm-bytecode", @@ -3906,7 +3915,7 @@ dependencies = [ [[package]] name = "revm-precompile" -version = "23.0.0" +version = "24.0.1" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -3914,6 +3923,7 @@ dependencies = [ "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", + "arrayref", "aurora-engine-modexp", "blst", "c-kzg", @@ -3945,7 +3955,7 @@ dependencies = [ [[package]] name = "revm-state" -version = "6.0.0" +version = "7.0.1" dependencies = [ "bitflags", "revm-bytecode", @@ -3955,16 +3965,18 @@ dependencies = [ [[package]] name = "revm-statetest-types" -version = "8.0.1" +version = "8.0.4" dependencies = [ + "k256", "revm", "serde", "serde_json", + "thiserror", ] [[package]] name = "revme" -version = "7.0.1" +version = "7.0.4" dependencies = [ "alloy-rlp", "alloy-sol-types", diff --git a/Cargo.toml b/Cargo.toml index 1a38543dd1..cc4fad9509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,33 +33,32 @@ members = [ "examples/erc20_gas", "examples/my_evm", "examples/custom_opcodes", + "examples/custom_precompile_journal", ] resolver = "2" default-members = ["crates/revm"] [workspace.dependencies] # revm -revm = { path = "crates/revm", version = "26.0.1", default-features = false } +revm = { path = "crates/revm", version = "27.0.3", default-features = false } primitives = { path = "crates/primitives", package = "revm-primitives", version = "20.0.0", default-features = false } -bytecode = { path = "crates/bytecode", package = "revm-bytecode", version = "5.0.0", default-features = false } -database = { path = "crates/database", package = "revm-database", version = "6.0.0", default-features = false } -database-interface = { path = "crates/database/interface", package = "revm-database-interface", version = "6.0.0", default-features = false } -state = { path = "crates/state", package = "revm-state", version = "6.0.0", default-features = false } -interpreter = { path = "crates/interpreter", package = "revm-interpreter", version = "22.0.1", default-features = false } -inspector = { path = "crates/inspector", package = "revm-inspector", version = "7.0.1", default-features = false } -precompile = { path = "crates/precompile", package = "revm-precompile", version = "23.0.0", default-features = false } -statetest-types = { path = "crates/statetest-types", package = "revm-statetest-types", version = "8.0.1", default-features = false } -context = { path = "crates/context", package = "revm-context", version = "7.0.1", default-features = false } -context-interface = { path = "crates/context/interface", package = "revm-context-interface", version = "7.0.1", default-features = false } -handler = { path = "crates/handler", package = "revm-handler", version = "7.0.1", default-features = false } -op-revm = { path = "crates/op-revm", package = "op-revm", version = "7.0.1", default-features = false } +bytecode = { path = "crates/bytecode", package = "revm-bytecode", version = "6.0.1", default-features = false } +database = { path = "crates/database", package = "revm-database", version = "7.0.1", default-features = false } +database-interface = { path = "crates/database/interface", package = "revm-database-interface", version = "7.0.1", default-features = false } +state = { path = "crates/state", package = "revm-state", version = "7.0.1", default-features = false } +interpreter = { path = "crates/interpreter", package = "revm-interpreter", version = "23.0.2", default-features = false } +inspector = { path = "crates/inspector", package = "revm-inspector", version = "8.0.3", default-features = false } +precompile = { path = "crates/precompile", package = "revm-precompile", version = "24.0.1", default-features = false } +statetest-types = { path = "crates/statetest-types", package = "revm-statetest-types", version = "8.0.4", default-features = false } +context = { path = "crates/context", package = "revm-context", version = "8.0.3", default-features = false } +context-interface = { path = "crates/context/interface", package = "revm-context-interface", version = "8.0.1", default-features = false } +handler = { path = "crates/handler", package = "revm-handler", version = "8.0.3", default-features = false } +op-revm = { path = "crates/op-revm", package = "op-revm", version = "8.0.3", default-features = false } # alloy alloy-eip2930 = { version = "0.2.1", default-features = false } alloy-eip7702 = { version = "0.6.1", default-features = false } -alloy-primitives = { version = "1.2.0", default-features = false, features = [ - "sha3-keccak", -] } +alloy-primitives = { version = "1.2.0", default-features = false, features = [] } # alloy in examples, revme or feature flagged. alloy-rlp = { version = "0.3.12", default-features = false } diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 2423e1dbc1..ecccab75c4 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,4 +1,10 @@ +# v80 tag (revm v27.0.0) -> v81 tag ( revm v27.0.1) + +* Inspector fn `step_end` is now called even if Inspector `step` sets the action. Previously this was not the case. + * https://github.com/bluealloy/revm/pull/2687 + * this additionally fixes panic bug where `bytecode.opcode()` would panic in `step_end` + # v70 tag (revm v22.0.2) -> v71 tag ( revm v23.0.0) * Removal of `EvmData`. diff --git a/bins/revme/CHANGELOG.md b/bins/revme/CHANGELOG.md index ff5f95367f..114036e30b 100644 --- a/bins/revme/CHANGELOG.md +++ b/bins/revme/CHANGELOG.md @@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.4](https://github.com/bluealloy/revm/compare/revme-v7.0.3...revme-v7.0.4) - 2025-07-14 + +### Other + +- incorrect StorageKey and StorageValue parameter order in burntpix benchmark ([#2704](https://github.com/bluealloy/revm/pull/2704)) + +## [7.0.3](https://github.com/bluealloy/revm/compare/revme-v7.0.2...revme-v7.0.3) - 2025-07-03 + +### Other + +- update Cargo.lock dependencies + +## [7.0.2](https://github.com/bluealloy/revm/compare/revme-v7.0.1...revme-v7.0.2) - 2025-06-30 + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- statetest runner cleanup ([#2665](https://github.com/bluealloy/revm/pull/2665)) +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/revme-v7.0.0...revme-v7.0.1) - 2025-06-20 ### Other diff --git a/bins/revme/Cargo.toml b/bins/revme/Cargo.toml index 842f54ecde..f559bcd327 100644 --- a/bins/revme/Cargo.toml +++ b/bins/revme/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revme" description = "Rust Ethereum Virtual Machine Executable" -version = "7.0.1" +version = "7.0.4" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/bins/revme/src/cmd/bench/analysis.rs b/bins/revme/src/cmd/bench/analysis.rs index 6b0a45322c..e8fdb3704f 100644 --- a/bins/revme/src/cmd/bench/analysis.rs +++ b/bins/revme/src/cmd/bench/analysis.rs @@ -15,12 +15,12 @@ pub fn run(criterion: &mut Criterion) { let context = Context::mainnet() .with_db(BenchmarkDB::new_bytecode(bytecode)) .modify_cfg_chained(|c| c.disable_nonce_check = true); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - data: bytes!("8035F0CE"), - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .data(bytes!("8035F0CE")) + .build() + .unwrap(); let mut evm = context.build_mainnet(); criterion.bench_function("analysis", |b| { b.iter_batched( diff --git a/bins/revme/src/cmd/bench/burntpix.rs b/bins/revme/src/cmd/bench/burntpix.rs index 3a191d064b..1e65627fe4 100644 --- a/bins/revme/src/cmd/bench/burntpix.rs +++ b/bins/revme/src/cmd/bench/burntpix.rs @@ -41,13 +41,13 @@ pub fn run(criterion: &mut Criterion) { .modify_cfg_chained(|c| c.disable_nonce_check = true) .build_mainnet(); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BURNTPIX_MAIN_ADDRESS), - data: run_call_data.clone().into(), - gas_limit: u64::MAX, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BURNTPIX_MAIN_ADDRESS)) + .data(run_call_data.clone().into()) + .gas_limit(u64::MAX) + .build() + .unwrap(); criterion.bench_function("burntpix", |b| { b.iter_batched( @@ -163,8 +163,8 @@ fn init_db() -> CacheDB { cache_db .insert_account_storage( BURNTPIX_MAIN_ADDRESS, - StorageValue::from(2), - StorageKey::from_be_bytes(*STORAGE_TWO), + StorageKey::from(2), + StorageValue::from_be_bytes(*STORAGE_TWO), ) .unwrap(); diff --git a/bins/revme/src/cmd/bench/gas_cost_estimator.rs b/bins/revme/src/cmd/bench/gas_cost_estimator.rs index 5348386a1e..d596ac1dab 100644 --- a/bins/revme/src/cmd/bench/gas_cost_estimator.rs +++ b/bins/revme/src/cmd/bench/gas_cost_estimator.rs @@ -26,12 +26,12 @@ pub fn run(criterion: &mut Criterion) { .modify_cfg_chained(|c| c.disable_nonce_check = true) .build_mainnet(); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - gas_limit: 1_000_000_000, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(1_000_000_000) + .build() + .unwrap(); criterion.bench_function(name, |b| { b.iter_batched( diff --git a/bins/revme/src/cmd/bench/snailtracer.rs b/bins/revme/src/cmd/bench/snailtracer.rs index e9713364c3..631ea54163 100644 --- a/bins/revme/src/cmd/bench/snailtracer.rs +++ b/bins/revme/src/cmd/bench/snailtracer.rs @@ -15,13 +15,13 @@ pub fn run(criterion: &mut Criterion) { .modify_cfg_chained(|c| c.disable_nonce_check = true) .build_mainnet(); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - data: bytes!("30627b7c"), - gas_limit: 1_000_000_000, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .data(bytes!("30627b7c")) + .gas_limit(1_000_000_000) + .build() + .unwrap(); criterion.bench_function("snailtracer", |b| { b.iter_batched( diff --git a/bins/revme/src/cmd/bench/transfer.rs b/bins/revme/src/cmd/bench/transfer.rs index 14bddbaf87..0914eff60d 100644 --- a/bins/revme/src/cmd/bench/transfer.rs +++ b/bins/revme/src/cmd/bench/transfer.rs @@ -14,14 +14,14 @@ pub fn run(criterion: &mut Criterion) { .modify_cfg_chained(|cfg| cfg.disable_nonce_check = true) .build_mainnet(); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - value: U256::from(1), - gas_price: 1, - gas_priority_fee: None, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .value(U256::from(1)) + .gas_price(1) + .gas_priority_fee(None) + .build() + .unwrap(); evm.ctx.tx = tx.clone(); diff --git a/bins/revme/src/cmd/bench/transfer_multi.rs b/bins/revme/src/cmd/bench/transfer_multi.rs index 3a69af2e6d..9c3e85eefb 100644 --- a/bins/revme/src/cmd/bench/transfer_multi.rs +++ b/bins/revme/src/cmd/bench/transfer_multi.rs @@ -33,21 +33,20 @@ pub fn run(criterion: &mut Criterion) { .modify_cfg_chained(|cfg| cfg.disable_nonce_check = true) .build_mainnet(); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - value: U256::from(1), - gas_price: 0, - gas_priority_fee: None, - gas_limit: 30_000, - ..Default::default() - }; - let target = U256::from(10000); - let mut txs = vec![tx.clone(); 1000]; + let mut txs = Vec::with_capacity(1000); - for (i, tx_mut) in txs.iter_mut().enumerate() { - tx_mut.kind = TxKind::Call((target + U256::from(i)).into_address()); + for i in 0..1000 { + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call((target + U256::from(i)).into_address())) + .value(U256::from(1)) + .gas_price(0) + .gas_priority_fee(None) + .gas_limit(30_000) + .build() + .unwrap(); + txs.push(tx); } criterion.bench_function("transact_commit_1000txs", |b| { diff --git a/bins/revme/src/cmd/evmrunner.rs b/bins/revme/src/cmd/evmrunner.rs index 6fdd476a3c..8fda0fac16 100644 --- a/bins/revme/src/cmd/evmrunner.rs +++ b/bins/revme/src/cmd/evmrunner.rs @@ -87,13 +87,13 @@ impl Cmd { .with_db(db) .build_mainnet_with_inspector(TracerEip3155::new(Box::new(std::io::stdout()))); - let tx = TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - data: input, - nonce, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .data(input) + .nonce(nonce) + .build() + .unwrap(); if self.bench { let mut criterion = criterion::Criterion::default() diff --git a/bins/revme/src/cmd/statetest/merkle_trie.rs b/bins/revme/src/cmd/statetest/merkle_trie.rs index e294c4e4be..c6b6418787 100644 --- a/bins/revme/src/cmd/statetest/merkle_trie.rs +++ b/bins/revme/src/cmd/statetest/merkle_trie.rs @@ -1,10 +1,28 @@ +use std::convert::Infallible; + use alloy_rlp::{RlpEncodable, RlpMaxEncodedLen}; -use database::PlainAccount; +use context::result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction}; +use database::{EmptyDB, PlainAccount, State}; use hash_db::Hasher; use plain_hasher::PlainHasher; use revm::primitives::{keccak256, Address, Log, B256, U256}; use triehash::sec_trie_root; +pub struct TestValidationResult { + pub logs_root: B256, + pub state_root: B256, +} + +pub fn compute_test_roots( + exec_result: &Result, EVMError>, + db: &State, +) -> TestValidationResult { + TestValidationResult { + logs_root: log_rlp_hash(exec_result.as_ref().map(|r| r.logs()).unwrap_or_default()), + state_root: state_merkle_trie_root(db.cache.trie_account()), + } +} + pub fn log_rlp_hash(logs: &[Log]) -> B256 { let mut out = Vec::with_capacity(alloy_rlp::list_length(logs)); alloy_rlp::encode_list(logs, &mut out); diff --git a/bins/revme/src/cmd/statetest/runner.rs b/bins/revme/src/cmd/statetest/runner.rs index 45b56ed2d9..f07b2d0dc2 100644 --- a/bins/revme/src/cmd/statetest/runner.rs +++ b/bins/revme/src/cmd/statetest/runner.rs @@ -1,29 +1,20 @@ -use super::{ - merkle_trie::{log_rlp_hash, state_merkle_trie_root}, - utils::recover_address, -}; -use context::either::Either; +use crate::cmd::statetest::merkle_trie::{compute_test_roots, TestValidationResult}; use database::State; use indicatif::{ProgressBar, ProgressDrawTarget}; use inspector::{inspectors::TracerEip3155, InspectCommitEvm}; use primitives::U256; use revm::{ - bytecode::Bytecode, context::{block::BlockEnv, cfg::CfgEnv, tx::TxEnv}, context_interface::{ - block::calc_excess_blob_gas, result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction}, Cfg, }, database_interface::EmptyDB, - primitives::{ - eip4844::TARGET_BLOB_GAS_PER_BLOCK_CANCUN, hardfork::SpecId, keccak256, Bytes, TxKind, B256, - }, + primitives::{hardfork::SpecId, Bytes, B256}, Context, ExecuteCommitEvm, MainBuilder, MainContext, }; use serde_json::json; -use statetest_types::{SpecName, Test, TestSuite}; - +use statetest_types::{SpecName, Test, TestSuite, TestUnit}; use std::{ convert::Infallible, fmt::Debug, @@ -38,6 +29,7 @@ use std::{ use thiserror::Error; use walkdir::{DirEntry, WalkDir}; +/// Error that occurs during test execution #[derive(Debug, Error)] #[error("Path: {path}\nName: {name}\nError: {kind}")] pub struct TestError { @@ -46,6 +38,7 @@ pub struct TestError { pub kind: TestErrorKind, } +/// Specific kind of error that occurred during test execution #[derive(Debug, Error)] pub enum TestErrorKind { #[error("logs root mismatch: got {got}, expected {expected}")] @@ -74,6 +67,9 @@ pub enum TestErrorKind { NoJsonFiles, } +/// Find all JSON test files in the given path +/// If path is a file, returns it in a vector +/// If path is a directory, recursively finds all .json files pub fn find_all_json_tests(path: &Path) -> Vec { if path.is_file() { vec![path.to_path_buf()] @@ -87,6 +83,8 @@ pub fn find_all_json_tests(path: &Path) -> Vec { } } +/// Check if a test should be skipped based on its filename +/// Some tests are known to be problematic or take too long fn skip_test(path: &Path) -> bool { let name = path.file_name().unwrap().to_str().unwrap(); @@ -119,6 +117,98 @@ fn skip_test(path: &Path) -> bool { ) } +struct TestExecutionContext<'a> { + name: &'a str, + unit: &'a TestUnit, + test: &'a Test, + cfg: &'a CfgEnv, + block: &'a BlockEnv, + tx: &'a TxEnv, + cache_state: &'a database::CacheState, + elapsed: &'a Arc>, + trace: bool, + print_json_outcome: bool, +} + +struct DebugContext<'a> { + name: &'a str, + path: &'a str, + index: usize, + test: &'a Test, + cfg: &'a CfgEnv, + block: &'a BlockEnv, + tx: &'a TxEnv, + cache_state: &'a database::CacheState, + error: &'a TestErrorKind, +} + +fn build_json_output( + test: &Test, + test_name: &str, + exec_result: &Result, EVMError>, + validation: &TestValidationResult, + spec: SpecId, + error: Option, +) -> serde_json::Value { + json!({ + "stateRoot": validation.state_root, + "logsRoot": validation.logs_root, + "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(), + "gasUsed": exec_result.as_ref().ok().map(|r| r.gas_used()).unwrap_or_default(), + "pass": error.is_none(), + "errorMsg": error.unwrap_or_default(), + "evmResult": format_evm_result(exec_result), + "postLogsHash": validation.logs_root, + "fork": spec, + "test": test_name, + "d": test.indexes.data, + "g": test.indexes.gas, + "v": test.indexes.value, + }) +} + +fn format_evm_result( + exec_result: &Result, EVMError>, +) -> String { + match exec_result { + Ok(r) => match r { + ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"), + ExecutionResult::Revert { .. } => "Revert".to_string(), + ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"), + }, + Err(e) => e.to_string(), + } +} + +fn validate_exception( + test: &Test, + exec_result: &Result, EVMError>, +) -> Result { + match (&test.expect_exception, exec_result) { + (None, Ok(_)) => Ok(false), // No exception expected, execution succeeded + (Some(_), Err(_)) => Ok(true), // Exception expected and occurred + _ => Err(TestErrorKind::UnexpectedException { + expected_exception: test.expect_exception.clone(), + got_exception: exec_result.as_ref().err().map(|e| e.to_string()), + }), + } +} + +fn validate_output( + expected_output: Option<&Bytes>, + actual_result: &ExecutionResult, +) -> Result<(), TestErrorKind> { + if let Some((expected, actual)) = expected_output.zip(actual_result.output()) { + if expected != actual { + return Err(TestErrorKind::UnexpectedOutput { + expected_output: Some(expected.clone()), + got_output: actual_result.output().cloned(), + }); + } + } + Ok(()) +} + fn check_evm_execution( test: &Test, expected_output: Option<&Bytes>, @@ -128,97 +218,71 @@ fn check_evm_execution( spec: SpecId, print_json_outcome: bool, ) -> Result<(), TestErrorKind> { - let logs_root = log_rlp_hash(exec_result.as_ref().map(|r| r.logs()).unwrap_or_default()); - let state_root = state_merkle_trie_root(db.cache.trie_account()); + let validation = compute_test_roots(exec_result, db); - let print_json_output = |error: Option| { + let print_json = |error: Option<&TestErrorKind>| { if print_json_outcome { - let json = json!({ - "stateRoot": state_root, - "logsRoot": logs_root, - "output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(), - "gasUsed": exec_result.as_ref().ok().map(|r| r.gas_used()).unwrap_or_default(), - "pass": error.is_none(), - "errorMsg": error.unwrap_or_default(), - "evmResult": match exec_result { - Ok(r) => match r { - ExecutionResult::Success { reason, .. } => format!("Success: {reason:?}"), - ExecutionResult::Revert { .. } => "Revert".to_string(), - ExecutionResult::Halt { reason, .. } => format!("Halt: {reason:?}"), - }, - Err(e) => e.to_string(), - }, - "postLogsHash": logs_root, - "fork": spec, - "test": test_name, - "d": test.indexes.data, - "g": test.indexes.gas, - "v": test.indexes.value, - }); + let json = build_json_output( + test, + test_name, + exec_result, + &validation, + spec, + error.map(|e| e.to_string()), + ); eprintln!("{json}"); } }; - // If we expect exception revm should return error from execution. - // So we do not check logs and state root. - // - // Note that some tests that have exception and run tests from before state clear - // would touch the caller account and make it appear in state root calculation. - // This is not something that we would expect as invalid tx should not touch state. - // but as this is a cleanup of invalid tx it is not properly defined and in the end - // it does not matter. - // Test where this happens: `tests/GeneralStateTests/stTransactionTest/NoSrcAccountCreate.json` - // and you can check that we have only two "hash" values for before and after state clear. - match (&test.expect_exception, exec_result) { - // Do nothing - (None, Ok(result)) => { - // Check output - if let Some((expected_output, output)) = expected_output.zip(result.output()) { - if expected_output != output { - let kind = TestErrorKind::UnexpectedOutput { - expected_output: Some(expected_output.clone()), - got_output: result.output().cloned(), - }; - print_json_output(Some(kind.to_string())); - return Err(kind); - } - } - } - // Return okay, exception is expected. - (Some(_), Err(_)) => return Ok(()), - _ => { - let kind = TestErrorKind::UnexpectedException { - expected_exception: test.expect_exception.clone(), - got_exception: exec_result.clone().err().map(|e| e.to_string()), - }; - print_json_output(Some(kind.to_string())); - return Err(kind); - } + // Check if exception handling is correct + let exception_expected = validate_exception(test, exec_result).inspect_err(|e| { + print_json(Some(e)); + })?; + + // If exception was expected and occurred, we're done + if exception_expected { + print_json(None); + return Ok(()); } - if logs_root != test.logs { - let kind = TestErrorKind::LogsRootMismatch { - got: logs_root, + // Validate output if execution succeeded + if let Ok(result) = exec_result { + validate_output(expected_output, result).inspect_err(|e| { + print_json(Some(e)); + })?; + } + + // Validate logs root + if validation.logs_root != test.logs { + let error = TestErrorKind::LogsRootMismatch { + got: validation.logs_root, expected: test.logs, }; - print_json_output(Some(kind.to_string())); - return Err(kind); + print_json(Some(&error)); + return Err(error); } - if state_root != test.hash { - let kind = TestErrorKind::StateRootMismatch { - got: state_root, + // Validate state root + if validation.state_root != test.hash { + let error = TestErrorKind::StateRootMismatch { + got: validation.state_root, expected: test.hash, }; - print_json_output(Some(kind.to_string())); - return Err(kind); + print_json(Some(&error)); + return Err(error); } - print_json_output(None); - + print_json(None); Ok(()) } +/// Execute a single test suite file containing multiple tests +/// +/// # Arguments +/// * `path` - Path to the JSON test file +/// * `elapsed` - Shared counter for total execution time +/// * `trace` - Whether to enable EVM tracing +/// * `print_json_outcome` - Whether to print JSON formatted results pub fn execute_test_suite( path: &Path, elapsed: &Arc>, @@ -238,25 +302,11 @@ pub fn execute_test_suite( })?; for (name, unit) in suite.0 { - // Create database and insert cache - let mut cache_state = database::CacheState::new(false); - for (address, info) in unit.pre { - let code_hash = keccak256(&info.code); - let bytecode = Bytecode::new_raw_checked(info.code.clone()) - .unwrap_or(Bytecode::new_legacy(info.code)); - let acc_info = revm::state::AccountInfo { - balance: info.balance, - code_hash, - code: Some(bytecode), - nonce: info.nonce, - }; - cache_state.insert_account_with_storage(address, acc_info, info.storage); - } + // Prepare initial state + let cache_state = unit.state(); + // Setup base configuration let mut cfg = CfgEnv::default(); - let mut block = BlockEnv::default(); - let mut tx = TxEnv::default(); - // For mainnet cfg.chain_id = unit .env .current_chain_id @@ -264,313 +314,293 @@ pub fn execute_test_suite( .try_into() .unwrap_or(1); - // Block env - block.number = unit.env.current_number; - block.beneficiary = unit.env.current_coinbase; - block.timestamp = unit.env.current_timestamp; - block.gas_limit = unit.env.current_gas_limit.try_into().unwrap_or(u64::MAX); - block.basefee = unit - .env - .current_base_fee - .unwrap_or_default() - .try_into() - .unwrap_or(u64::MAX); - block.difficulty = unit.env.current_difficulty; - // After the Merge prevrandao replaces mix_hash field in block and replaced difficulty opcode in EVM. - block.prevrandao = unit.env.current_random; - - // Tx env - tx.caller = if let Some(address) = unit.transaction.sender { - address - } else { - recover_address(unit.transaction.secret_key.as_slice()).ok_or_else(|| TestError { - name: name.clone(), - path: path.clone(), - kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key), - })? - }; - tx.gas_price = unit - .transaction - .gas_price - .or(unit.transaction.max_fee_per_gas) - .unwrap_or_default() - .try_into() - .unwrap_or(u128::MAX); - tx.gas_priority_fee = unit - .transaction - .max_priority_fee_per_gas - .map(|b| u128::try_from(b).expect("max priority fee less than u128::MAX")); - // EIP-4844 - tx.blob_hashes = unit.transaction.blob_versioned_hashes.clone(); - tx.max_fee_per_blob_gas = unit - .transaction - .max_fee_per_blob_gas - .map(|b| u128::try_from(b).expect("max fee less than u128::MAX")) - .unwrap_or(u128::MAX); - // Post and execution - for (spec_name, tests) in unit.post { - // Constantinople was immediately extended by Petersburg. - // There isn't any production Constantinople transaction - // so we don't support it and skip right to Petersburg. - if spec_name == SpecName::Constantinople { + for (spec_name, tests) in &unit.post { + // Skip Constantinople spec + if *spec_name == SpecName::Constantinople { continue; } cfg.spec = spec_name.to_spec_id(); - // set default max blobs number to be 9 for prague + // Configure max blobs per spec if cfg.spec.is_enabled_in(SpecId::PRAGUE) { cfg.set_max_blobs_per_tx(9); } else { cfg.set_max_blobs_per_tx(6); } - // EIP-4844 - if let Some(current_excess_blob_gas) = unit.env.current_excess_blob_gas { - block.set_blob_excess_gas_and_price( - current_excess_blob_gas.to(), - cfg.blob_base_fee_update_fraction(), - ); - } else if let (Some(parent_blob_gas_used), Some(parent_excess_blob_gas)) = ( - unit.env.parent_blob_gas_used, - unit.env.parent_excess_blob_gas, - ) { - block.set_blob_excess_gas_and_price( - calc_excess_blob_gas( - parent_blob_gas_used.to(), - parent_excess_blob_gas.to(), - unit.env - .parent_target_blobs_per_block - .map(|i| i.to()) - .unwrap_or(TARGET_BLOB_GAS_PER_BLOCK_CANCUN), - ), - cfg.blob_base_fee_update_fraction(), - ); - } - - if cfg.spec.is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() { - // If spec is merge and prevrandao is not set, set it to default - block.prevrandao = Some(B256::default()); - } - - for (index, test) in tests.into_iter().enumerate() { - let Some(tx_type) = unit.transaction.tx_type(test.indexes.data) else { - if test.expect_exception.is_some() { - continue; - } else { - panic!("Invalid transaction type without expected exception"); + // Setup block environment for this spec + let block = unit.block_env(&cfg); + + for (index, test) in tests.iter().enumerate() { + // Setup transaction environment + let tx = match test.tx_env(&unit) { + Ok(tx) => tx, + Err(_) if test.expect_exception.is_some() => continue, + Err(_) => { + return Err(TestError { + name: name.clone(), + path: path.clone(), + kind: TestErrorKind::UnknownPrivateKey(unit.transaction.secret_key), + }); } }; - tx.tx_type = tx_type as u8; - - tx.gas_limit = unit.transaction.gas_limit[test.indexes.gas].saturating_to(); - tx.data = unit - .transaction - .data - .get(test.indexes.data) - .unwrap() - .clone(); - - tx.nonce = u64::try_from(unit.transaction.nonce).unwrap(); - tx.value = unit.transaction.value[test.indexes.value]; - - tx.access_list = unit - .transaction - .access_lists - .get(test.indexes.data) - .cloned() - .flatten() - .unwrap_or_default(); - - tx.authorization_list = unit - .transaction - .authorization_list - .clone() - .map(|auth_list| { - auth_list - .into_iter() - .map(|i| Either::Left(i.into())) - .collect::>() - }) - .unwrap_or_default(); - - let to = match unit.transaction.to { - Some(add) => TxKind::Call(add), - None => TxKind::Create, - }; - tx.kind = to; - - let mut cache = cache_state.clone(); - cache.set_state_clear_flag(cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON)); - let mut state = database::State::builder() - .with_cached_prestate(cache) - .with_bundle_update() - .build(); - - let evm_context = Context::mainnet() - .with_block(&block) - .with_tx(&tx) - .with_cfg(&cfg) - .with_db(&mut state); - - // Do the deed - let timer = Instant::now(); - let (db, exec_result) = if trace { - let mut evm = evm_context.build_mainnet_with_inspector( - TracerEip3155::buffered(stderr()).without_summary(), - ); - let res = evm.inspect_tx_commit(&tx); - let db = evm.ctx.journaled_state.database; - (db, res) - } else { - let mut evm = evm_context.build_mainnet(); - let res = evm.transact_commit(&tx); - let db = evm.ctx.journaled_state.database; - (db, res) - }; - *elapsed.lock().unwrap() += timer.elapsed(); - let spec = cfg.spec(); - // Dump state and traces if test failed - let output = check_evm_execution( - &test, - unit.out.as_ref(), - &name, - &exec_result, - db, - spec, + + // Execute the test + let result = execute_single_test(TestExecutionContext { + name: &name, + unit: &unit, + test, + cfg: &cfg, + block: &block, + tx: &tx, + cache_state: &cache_state, + elapsed, + trace, print_json_outcome, - ); - let Err(e) = output else { - continue; - }; + }); + + if let Err(e) = result { + // Handle error with debug trace if needed + static FAILED: AtomicBool = AtomicBool::new(false); + if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) { + return Err(TestError { + name: name.clone(), + path: path.clone(), + kind: e, + }); + } + + // Re-run with trace for debugging + debug_failed_test(DebugContext { + name: &name, + path: &path, + index, + test, + cfg: &cfg, + block: &block, + tx: &tx, + cache_state: &cache_state, + error: &e, + }); - // Print only once or if we are already in trace mode, just return error - // If trace is true that print_json_outcome will be also true. - static FAILED: AtomicBool = AtomicBool::new(false); - if print_json_outcome || FAILED.swap(true, Ordering::SeqCst) { return Err(TestError { - name: name.clone(), path: path.clone(), + name: name.clone(), kind: e, }); } - - // Re-build to run with tracing - let mut cache = cache_state.clone(); - cache.set_state_clear_flag(cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON)); - let mut state = database::State::builder() - .with_cached_prestate(cache) - .with_bundle_update() - .build(); - - println!("\nTraces:"); - - let mut evm = Context::mainnet() - .with_db(&mut state) - .with_block(&block) - .with_tx(&tx) - .with_cfg(&cfg) - .build_mainnet_with_inspector( - TracerEip3155::buffered(stderr()).without_summary(), - ); - - let _ = evm.inspect_tx_commit(&tx); - - println!("\nExecution result: {exec_result:#?}"); - println!("\nExpected exception: {:?}", test.expect_exception); - println!("\nState before: {cache_state:#?}"); - println!( - "\nState after: {:#?}", - evm.ctx.journaled_state.database.cache - ); - println!("\nSpecification: {:?}", cfg.spec); - println!("\nTx: {tx:#?}"); - println!("Block: {block:#?}"); - println!("Cfg: {cfg:#?}"); - println!("\nTest name: {name:?} (index: {index}, path: {path:?}) failed:\n{e}"); - - return Err(TestError { - path: path.clone(), - name: name.clone(), - kind: e, - }); } } } Ok(()) } -pub fn run( - test_files: Vec, - mut single_thread: bool, +fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> { + // Prepare state + let mut cache = ctx.cache_state.clone(); + cache.set_state_clear_flag(ctx.cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON)); + let mut state = database::State::builder() + .with_cached_prestate(cache) + .with_bundle_update() + .build(); + + let evm_context = Context::mainnet() + .with_block(ctx.block) + .with_tx(ctx.tx) + .with_cfg(ctx.cfg) + .with_db(&mut state); + + // Execute + let timer = Instant::now(); + let (db, exec_result) = if ctx.trace { + let mut evm = evm_context + .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary()); + let res = evm.inspect_tx_commit(ctx.tx); + let db = evm.ctx.journaled_state.database; + (db, res) + } else { + let mut evm = evm_context.build_mainnet(); + let res = evm.transact_commit(ctx.tx); + let db = evm.ctx.journaled_state.database; + (db, res) + }; + *ctx.elapsed.lock().unwrap() += timer.elapsed(); + + // Check results + check_evm_execution( + ctx.test, + ctx.unit.out.as_ref(), + ctx.name, + &exec_result, + db, + ctx.cfg.spec(), + ctx.print_json_outcome, + ) +} + +fn debug_failed_test(ctx: DebugContext) { + println!("\nTraces:"); + + // Re-run with tracing + let mut cache = ctx.cache_state.clone(); + cache.set_state_clear_flag(ctx.cfg.spec.is_enabled_in(SpecId::SPURIOUS_DRAGON)); + let mut state = database::State::builder() + .with_cached_prestate(cache) + .with_bundle_update() + .build(); + + let mut evm = Context::mainnet() + .with_db(&mut state) + .with_block(ctx.block) + .with_tx(ctx.tx) + .with_cfg(ctx.cfg) + .build_mainnet_with_inspector(TracerEip3155::buffered(stderr()).without_summary()); + + let exec_result = evm.inspect_tx_commit(ctx.tx); + + println!("\nExecution result: {exec_result:#?}"); + println!("\nExpected exception: {:?}", ctx.test.expect_exception); + println!("\nState before: {:#?}", ctx.cache_state); + println!( + "\nState after: {:#?}", + evm.ctx.journaled_state.database.cache + ); + println!("\nSpecification: {:?}", ctx.cfg.spec); + println!("\nTx: {:#?}", ctx.tx); + println!("Block: {:#?}", ctx.block); + println!("Cfg: {:#?}", ctx.cfg); + println!( + "\nTest name: {:?} (index: {}, path: {:?}) failed:\n{}", + ctx.name, ctx.index, ctx.path, ctx.error + ); +} + +#[derive(Clone, Copy)] +struct TestRunnerConfig { + single_thread: bool, trace: bool, - mut print_outcome: bool, + print_outcome: bool, keep_going: bool, -) -> Result<(), TestError> { - // Trace implies print_outcome - if trace { - print_outcome = true; +} + +impl TestRunnerConfig { + fn new(single_thread: bool, trace: bool, print_outcome: bool, keep_going: bool) -> Self { + // Trace implies print_outcome + let print_outcome = print_outcome || trace; + // print_outcome or trace implies single_thread + let single_thread = single_thread || print_outcome; + + Self { + single_thread, + trace, + print_outcome, + keep_going, + } } - // `print_outcome` or trace implies single_thread - if print_outcome { - single_thread = true; +} + +#[derive(Clone)] +struct TestRunnerState { + n_errors: Arc, + console_bar: Arc, + queue: Arc)>>, + elapsed: Arc>, +} + +impl TestRunnerState { + fn new(test_files: Vec) -> Self { + let n_files = test_files.len(); + Self { + n_errors: Arc::new(AtomicUsize::new(0)), + console_bar: Arc::new(ProgressBar::with_draw_target( + Some(n_files as u64), + ProgressDrawTarget::stdout(), + )), + queue: Arc::new(Mutex::new((0usize, test_files))), + elapsed: Arc::new(Mutex::new(Duration::ZERO)), + } } - let n_files = test_files.len(); - let n_errors = Arc::new(AtomicUsize::new(0)); - let console_bar = Arc::new(ProgressBar::with_draw_target( - Some(n_files as u64), - ProgressDrawTarget::stdout(), - )); - let queue = Arc::new(Mutex::new((0usize, test_files))); - let elapsed = Arc::new(Mutex::new(std::time::Duration::ZERO)); + fn next_test(&self) -> Option { + let (current_idx, queue) = &mut *self.queue.lock().unwrap(); + let idx = *current_idx; + let test_path = queue.get(idx).cloned()?; + *current_idx = idx + 1; + Some(test_path) + } +} - let num_threads = match (single_thread, std::thread::available_parallelism()) { - (true, _) | (false, Err(_)) => 1, - (false, Ok(n)) => n.get(), - }; - let num_threads = num_threads.min(n_files); - let mut handles = Vec::with_capacity(num_threads); - for i in 0..num_threads { - let queue = queue.clone(); - let n_errors = n_errors.clone(); - let console_bar = console_bar.clone(); - let elapsed = elapsed.clone(); +fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<(), TestError> { + loop { + if !config.keep_going && state.n_errors.load(Ordering::SeqCst) > 0 { + return Ok(()); + } + + let Some(test_path) = state.next_test() else { + return Ok(()); + }; - let thread = std::thread::Builder::new().name(format!("runner-{i}")); + let result = execute_test_suite( + &test_path, + &state.elapsed, + config.trace, + config.print_outcome, + ); - let f = move || loop { - if !keep_going && n_errors.load(Ordering::SeqCst) > 0 { - return Ok(()); + state.console_bar.inc(1); + + if let Err(err) = result { + state.n_errors.fetch_add(1, Ordering::SeqCst); + if !config.keep_going { + return Err(err); } + } + } +} - let (_index, test_path) = { - let (current_idx, queue) = &mut *queue.lock().unwrap(); - let prev_idx = *current_idx; - let Some(test_path) = queue.get(prev_idx).cloned() else { - return Ok(()); - }; - *current_idx = prev_idx + 1; - (prev_idx, test_path) - }; +fn determine_thread_count(single_thread: bool, n_files: usize) -> usize { + match (single_thread, std::thread::available_parallelism()) { + (true, _) | (false, Err(_)) => 1, + (false, Ok(n)) => n.get().min(n_files), + } +} - let result = execute_test_suite(&test_path, &elapsed, trace, print_outcome); +/// Run all test files in parallel or single-threaded mode +/// +/// # Arguments +/// * `test_files` - List of test files to execute +/// * `single_thread` - Force single-threaded execution +/// * `trace` - Enable EVM execution tracing +/// * `print_outcome` - Print test outcomes in JSON format +/// * `keep_going` - Continue running tests even if some fail +pub fn run( + test_files: Vec, + single_thread: bool, + trace: bool, + print_outcome: bool, + keep_going: bool, +) -> Result<(), TestError> { + let config = TestRunnerConfig::new(single_thread, trace, print_outcome, keep_going); + let n_files = test_files.len(); + let state = TestRunnerState::new(test_files); + let num_threads = determine_thread_count(config.single_thread, n_files); - // Increment after the test is done. - console_bar.inc(1); + // Spawn worker threads + let mut handles = Vec::with_capacity(num_threads); + for i in 0..num_threads { + let state = state.clone(); - if let Err(err) = result { - n_errors.fetch_add(1, Ordering::SeqCst); - if !keep_going { - return Err(err); - } - } - }; - handles.push(thread.spawn(f).unwrap()); + let thread = std::thread::Builder::new() + .name(format!("runner-{i}")) + .spawn(move || run_test_worker(state, config)) + .unwrap(); + + handles.push(thread); } - // join all threads before returning an error + // Collect results from all threads let mut thread_errors = Vec::new(); for (i, handle) in handles.into_iter().enumerate() { match handle.join() { @@ -578,20 +608,23 @@ pub fn run( Ok(Err(e)) => thread_errors.push(e), Err(_) => thread_errors.push(TestError { name: format!("thread {i} panicked"), - path: "".to_string(), + path: String::new(), kind: TestErrorKind::Panic, }), } } - console_bar.finish(); + state.console_bar.finish(); + + // Print summary println!( "Finished execution. Total CPU time: {:.6}s", - elapsed.lock().unwrap().as_secs_f64() + state.elapsed.lock().unwrap().as_secs_f64() ); - let n_errors = n_errors.load(Ordering::SeqCst); + let n_errors = state.n_errors.load(Ordering::SeqCst); let n_thread_errors = thread_errors.len(); + if n_errors == 0 && n_thread_errors == 0 { println!("All tests passed!"); Ok(()) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e18eb9de3a..b84fe78c5d 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -3,7 +3,9 @@ - [Introduction](./../../README.md) - [Awesome Revm](./awesome.md) - [Architecture](./architecture.md) +- [Inspector - EVM Tracing](./inspector.md) +- [External State Transitions](./external_state_transitions.md) - [Dev section](./dev.md) - [Revme](./revme.md) - [Release procedure](./release_procedure.md) -- [Contact](./contact.md) \ No newline at end of file +- [Contact](./contact.md) diff --git a/book/src/architecture.md b/book/src/architecture.md index eb7a840a8d..9832b1f3cf 100644 --- a/book/src/architecture.md +++ b/book/src/architecture.md @@ -15,6 +15,13 @@ REVM works in `no_std` environments which means it can be used in zero-knowledge # Execution API +The Execution API provides the primary interface for running Ethereum transactions and interacting with the EVM. Whether you're building a blockchain client, testing framework, or analysis tool, this API offers multiple execution modes to suit your needs. + +The API is designed around four key execution patterns: +- **Basic execution**: Run transactions and get results +- **Execution with commit**: Run transactions and automatically persist state changes +- **Execution with inspection**: Run transactions with detailed tracing and observation + [`Evm`](https://docs.rs/revm-context/1.0.0/revm_context/evm/struct.Evm.html) the main structure for executing mainnet ethereum transaction is built with a [`Context`](https://docs.rs/revm-context/latest/revm_context/context/struct.Context.html) and a builder, code for it looks like this: ```rust,ignore @@ -47,6 +54,19 @@ let mut evm = Context::mainnet().with_block(block).build_mainnet().with_inspecto let _ = evm.inspect_tx(tx); ``` +## Inspector - EVM Execution Tracing + +The [`Inspector`](https://docs.rs/revm-inspector/latest/revm_inspector/trait.Inspector.html) trait is REVM's powerful mechanism for observing EVM execution. It provides hooks into every aspect of transaction execution, enabling sophisticated debugging, tracing and custom tooling. + +Key capabilities include: +- **Step-by-step execution tracing**: Hook into every opcode before and after execution +- **State monitoring**: Track stack, memory, and storage changes in real-time +- **Call and creation tracing**: Observe contract interactions and deployments +- **Event capture**: Record logs, self-destructs, and other EVM events +- **Execution override**: Optionally modify execution flow and outcomes + +The Inspector is ideal for building debuggers, gas analyzers, security tools, testing frameworks, and any application that needs deep visibility into EVM execution. For detailed usage examples and advanced features, see the [Inspector documentation](./inspector.md). + # EVM Framework To learn how to build your own custom EVM: diff --git a/book/src/external_state_transitions.md b/book/src/external_state_transitions.md new file mode 100644 index 0000000000..131b1b012f --- /dev/null +++ b/book/src/external_state_transitions.md @@ -0,0 +1,40 @@ +# External State Transitions (EIP-4788 & EIP-2935) + +Some Ethereum Improvement Proposals (EIPs) require state transitions that are not triggered by regular user transactions, but are instead performed by the client using special system calls (such as `transact_system_call`). These transitions are part of the EVM state changes, but are initiated by the client at specific block boundaries (pre- or post-block hooks), as required by the EIP. + +- [EIP-4788: Beacon block root in the EVM](https://eips.ethereum.org/EIPS/eip-4788) +- [EIP-2935: Add `blockHash` and `blockNumber` to the EVM](https://eips.ethereum.org/EIPS/eip-2935) + +## What are external state transitions? + +External state transitions refer to updates to the Ethereum state that are not performed by regular user transactions, but are instead performed by the client using system calls at block boundaries. These are typically required for EIPs that introduce new system contracts or require special state updates at block boundaries. + +## EIP-4788: Beacon block root in the EVM + +EIP-4788 requires that the root of each beacon chain block is committed to the execution layer and made available in the EVM via a special contract. This is achieved by the client calling a system contract at a fixed address (`0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02`) with the beacon root as input, at the start of each block. The contract maintains a ring buffer of recent roots. + +- The system call is performed by the client, not by EVM transaction execution. +- If the contract does not exist, the call fails silently. +- See [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) for full details. +- Example implementation in Reth: [reth#4457](https://github.com/paradigmxyz/reth/pull/4457) + +## EIP-2935: Add blockHash and blockNumber to the EVM + +EIP-2935 introduces a system contract that stores recent block hashes, allowing contracts to query them. The client is responsible for updating this contract at each block, by calling a system contract at a fixed address (`0x0000F90827F1C53a10cb7A02335B175320002935`) with the new block hash. + +- The system call is performed by the client, not by EVM transaction execution. +- See [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) for full details. +- Example implementation in Reth: [reth#7818](https://github.com/paradigmxyz/reth/pull/7818) + +## How does this affect REVM users? + +- To perform these block state transitions, the client or test harness should use the system call mechanism (`transact_system_call`) provided by REVM. +- REVM itself does not automatically perform these transitions; it expects the client to initiate them at the appropriate block boundaries, as specified by the EIPs. +- If you are building a full Ethereum client or a test harness, you are responsible for performing these system calls at the appropriate block boundaries, as specified in the EIPs. +- If you are only using REVM for transaction execution, you may need to ensure that the state of these system contracts is kept up to date externally. + +## References +- [EIP-4788: Beacon block root in the EVM](https://eips.ethereum.org/EIPS/eip-4788) +- [EIP-2935: Add blockHash and blockNumber to the EVM](https://eips.ethereum.org/EIPS/eip-2935) +- [reth#4457: EIP-4788 implementation](https://github.com/paradigmxyz/reth/pull/4457) +- [reth#7818: EIP-2935 implementation](https://github.com/paradigmxyz/reth/pull/7818) diff --git a/book/src/inspector.md b/book/src/inspector.md new file mode 100644 index 0000000000..8388132e0f --- /dev/null +++ b/book/src/inspector.md @@ -0,0 +1,138 @@ +# Inspector - EVM Execution Tracing + +The Inspector trait is REVM's powerful mechanism for observing and tracing EVM execution. It provides hooks into every aspect of transaction execution, making it ideal for building debuggers, analyzers, and custom tooling. + +## What is the Inspector? + +The [`Inspector`](https://docs.rs/revm-inspector/latest/revm_inspector/trait.Inspector.html) trait defines callbacks that are invoked during EVM execution. It allows you to: + +- **Step through execution**: Hook into every opcode before and after execution +- **Monitor state changes**: Track stack, memory, and storage modifications +- **Trace calls**: Observe contract calls, creations, and their outcomes +- **Capture events**: Record logs, self-destructs, and other EVM events +- **Override behavior**: Optionally modify execution flow and results + +## Core Inspector Methods + +```rust,ignore +pub trait Inspector { + // Opcode-level tracing + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) {} + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) {} + + // Call and creation tracing + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { None } + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) {} + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { None } + fn create_end(&mut self, context: &mut CTX, inputs: &CreateInputs, outcome: &mut CreateOutcome) {} + + // Event tracing + fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) {} + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) {} +} +``` + +## Basic Usage + +### 1. Create an Inspector + +```rust,ignore +#[derive(Default)] +struct MyInspector { + gas_used: u64, + call_count: usize, +} + +impl Inspector for MyInspector { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.gas_used += interp.gas.spent(); + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.call_count += 1; + None // Don't override the call + } +} +``` + +### 2. Use with EVM + +```rust,ignore +let inspector = MyInspector::default(); +let mut evm = Context::mainnet() + .with_db(db) + .build_mainnet_with_inspector(inspector); + +// Execute with inspection +let result = evm.inspect_one_tx(tx)?; +println!("Gas used: {}", evm.inspector.gas_used); +println!("Calls made: {}", evm.inspector.call_count); +``` + +## Advanced Features + +### State Inspection +Access complete interpreter state during execution: + +```rust,ignore +fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + let pc = interp.bytecode.pc(); + let opcode = interp.bytecode.opcode(); + let stack_len = interp.stack.len(); + let memory_size = interp.memory.size(); + + println!("PC: {}, Opcode: 0x{:02x}, Stack: {}, Memory: {}", + pc, opcode, stack_len, memory_size); +} +``` + +### Call Override +Modify execution by returning custom outcomes: + +```rust,ignore +fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address == SPECIAL_ADDRESS { + // Override this call with custom logic + return Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Return, Bytes::from("custom")), + 0..0 + )); + } + None // Let normal execution continue +} +``` + +### Event Logging +Capture and process EVM events: + +```rust,ignore +fn log(&mut self, _interp: &mut Interpreter, _ctx: &mut CTX, log: Log) { + println!("LOG emitted from: {:?}", log.address); + println!("Topics: {:?}", log.topics()); + println!("Data: {}", hex::encode(log.data.data)); +} +``` + +## Built-in Inspectors + +REVM provides several ready-to-use inspectors: + +- **`GasInspector`**: Tracks gas consumption throughout execution +- **`TracerEip3155`**: Generates EIP-3155 compatible execution traces +- **`NoOpInspector`**: Default no-operation inspector for when inspection is disabled + +## Performance Considerations + +- Inspector callbacks have minimal overhead when not implemented (empty default methods) +- Use inspection judiciously in production - detailed tracing can impact performance +- Consider batching inspector data collection for high-throughput scenarios + +## Common Use Cases + +- **Debuggers**: Step-by-step execution analysis +- **Gas analyzers**: Detailed gas consumption tracking +- **Security tools**: Detecting suspicious patterns or calls +- **Development tools**: Contract interaction tracing +- **Testing frameworks**: Execution verification and state checking + +The Inspector trait makes REVM very observable EVM implementations available, enabling sophisticated tooling and analysis capabilities. \ No newline at end of file diff --git a/book/src/revme.md b/book/src/revme.md index 7545fee6e1..90cb09f2f0 100644 --- a/book/src/revme.md +++ b/book/src/revme.md @@ -29,5 +29,5 @@ Revm can run statetest type of tests with `revme` using the following command: For running EEST tests, we can use the `./scripts/run-tests.sh.` -For legacy tests, we need to first to download the repo `git clone https://github.com/ethereum/legacytests` and run then run it with `cargo run --release -p revme -- statetest legacytests/Cancun/GeneralStateTests ` +For legacy tests, we need to first to download the repo `git clone https://github.com/ethereum/legacytests` and then run it with `cargo run --release -p revme -- statetest legacytests/Cancun/GeneralStateTests ` All statetest that can be run by revme can be found in the `GeneralStateTests` folder. diff --git a/crates/README.md b/crates/README.md index c365455696..8bb52201fc 100644 --- a/crates/README.md +++ b/crates/README.md @@ -5,7 +5,7 @@ Crates version and their description: * ![revm-precompile](https://img.shields.io/crates/v/revm-precompile?label=revm-precompile) Precompiles defined by ethereum * ![revm-database-interface](https://img.shields.io/crates/v/revm-database-interface?label=revm-database-interface) Interfaces for database implementation, database is used to fetch runtime state data (accounts, storages and block hash) * ![revm-database](https://img.shields.io/crates/v/revm-database?label=revm-database) A few structures that implement database interface -* ![revm-bytecode](https://img.shields.io/crates/v/revm-bytecode?label=revm-bytecode) Bytecode legacy analysis and EOF validation. Create contains opcode tables. +* ![revm-bytecode](https://img.shields.io/crates/v/revm-bytecode?label=revm-bytecode) Bytecode legacy analysis and EOF validation. Crate contains opcode tables. * ![revm-state](https://img.shields.io/crates/v/revm-state?label=revm-state) Small crate with accounts and storage types. * ![revm-context-interface](https://img.shields.io/crates/v/revm-context-interface?label=revm-context-interface) traits for Block/Transaction/Cfg/Journal. * ![revm-context](https://img.shields.io/crates/v/revm-context?label=revm-context) default implementation for traits from context interface. diff --git a/crates/bytecode/CHANGELOG.md b/crates/bytecode/CHANGELOG.md index 9feb63e4f0..94f6f401f3 100644 --- a/crates/bytecode/CHANGELOG.md +++ b/crates/bytecode/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [6.0.1](https://github.com/bluealloy/revm/compare/revm-bytecode-v6.0.0...revm-bytecode-v6.0.1) - 2025-07-03 + +### Other + +- add PartialEq u8 ([#2688](https://github.com/bluealloy/revm/pull/2688)) + +## [6.0.0](https://github.com/bluealloy/revm/compare/revm-bytecode-v5.0.0...revm-bytecode-v6.0.0) - 2025-06-30 + +### Fixed + +- implement `PartialEq` for `JumpTable` correctly ([#2654](https://github.com/bluealloy/revm/pull/2654)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) + ## [5.0.0](https://github.com/bluealloy/revm/compare/revm-bytecode-v4.1.0...revm-bytecode-v5.0.0) - 2025-06-19 ### Added diff --git a/crates/bytecode/Cargo.toml b/crates/bytecode/Cargo.toml index 78a0d8f48d..f3a05c920d 100644 --- a/crates/bytecode/Cargo.toml +++ b/crates/bytecode/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-bytecode" description = "EVM Bytecodes" -version = "5.0.0" +version = "6.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/bytecode/src/legacy/analysis.rs b/crates/bytecode/src/legacy/analysis.rs index a281077672..f4535a2644 100644 --- a/crates/bytecode/src/legacy/analysis.rs +++ b/crates/bytecode/src/legacy/analysis.rs @@ -110,14 +110,14 @@ mod tests { fn test_bytecode_with_jumpdest_at_start() { let bytecode = vec![opcode::JUMPDEST, opcode::PUSH1, 0x01, opcode::STOP]; let (jump_table, _) = analyze_legacy(bytecode.clone().into()); - assert!(jump_table.table[0]); // First byte should be a valid jumpdest + assert!(jump_table.is_valid(0)); // First byte should be a valid jumpdest } #[test] fn test_bytecode_with_jumpdest_after_push() { let bytecode = vec![opcode::PUSH1, 0x01, opcode::JUMPDEST, opcode::STOP]; let (jump_table, _) = analyze_legacy(bytecode.clone().into()); - assert!(jump_table.table[2]); // JUMPDEST should be at position 2 + assert!(jump_table.is_valid(2)); // JUMPDEST should be at position 2 } #[test] @@ -130,8 +130,8 @@ mod tests { opcode::STOP, ]; let (jump_table, _) = analyze_legacy(bytecode.clone().into()); - assert!(jump_table.table[0]); // First JUMPDEST - assert!(jump_table.table[3]); // Second JUMPDEST + assert!(jump_table.is_valid(0)); // First JUMPDEST + assert!(jump_table.is_valid(3)); // Second JUMPDEST } #[test] @@ -145,7 +145,7 @@ mod tests { fn test_bytecode_with_invalid_opcode() { let bytecode = vec![0xFF, opcode::STOP]; // 0xFF is an invalid opcode let (jump_table, _) = analyze_legacy(bytecode.clone().into()); - assert!(!jump_table.table[0]); // Invalid opcode should not be a jumpdest + assert!(!jump_table.is_valid(0)); // Invalid opcode should not be a jumpdest } #[test] @@ -165,9 +165,9 @@ mod tests { ]; let (jump_table, padded_bytecode) = analyze_legacy(bytecode.clone().into()); assert_eq!(padded_bytecode.len(), bytecode.len()); - assert!(!jump_table.table[0]); // PUSH1 - assert!(!jump_table.table[2]); // PUSH2 - assert!(!jump_table.table[5]); // PUSH4 + assert!(!jump_table.is_valid(0)); // PUSH1 + assert!(!jump_table.is_valid(2)); // PUSH2 + assert!(!jump_table.is_valid(5)); // PUSH4 } #[test] @@ -179,6 +179,6 @@ mod tests { opcode::STOP, ]; let (jump_table, _) = analyze_legacy(bytecode.clone().into()); - assert!(!jump_table.table[1]); // JUMPDEST in push data should not be valid + assert!(!jump_table.is_valid(1)); // JUMPDEST in push data should not be valid } } diff --git a/crates/bytecode/src/legacy/analyzed.rs b/crates/bytecode/src/legacy/analyzed.rs index f85211a97a..3bbc0b42b9 100644 --- a/crates/bytecode/src/legacy/analyzed.rs +++ b/crates/bytecode/src/legacy/analyzed.rs @@ -60,10 +60,11 @@ impl LegacyAnalyzedBytecode { if original_len > bytecode.len() { panic!("original_len is greater than bytecode length"); } - if original_len > jump_table.len { + if original_len > jump_table.len() { panic!( "jump table length {} is less than original length {}", - jump_table.len, original_len + jump_table.len(), + original_len ); } diff --git a/crates/bytecode/src/legacy/jump_map.rs b/crates/bytecode/src/legacy/jump_map.rs index a8d55f4c45..255f1c68d1 100644 --- a/crates/bytecode/src/legacy/jump_map.rs +++ b/crates/bytecode/src/legacy/jump_map.rs @@ -1,17 +1,31 @@ use bitvec::vec::BitVec; +use core::hash::{Hash, Hasher}; use once_cell::race::OnceBox; use primitives::hex; use std::{fmt::Debug, sync::Arc}; /// A table of valid `jump` destinations. Cheap to clone and memory efficient, one bit per opcode. -#[derive(Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +#[derive(Clone, Eq, Ord, PartialOrd)] pub struct JumpTable { /// Actual bit vec - pub table: Arc>, + table: Arc>, /// Fast pointer that skips Arc overhead table_ptr: *const u8, /// Number of bits in the table - pub len: usize, + len: usize, +} + +impl PartialEq for JumpTable { + fn eq(&self, other: &Self) -> bool { + self.table.eq(&other.table) && self.len.eq(&other.len) + } +} + +impl Hash for JumpTable { + fn hash(&self, state: &mut H) { + self.table.hash(state); + self.len.hash(state); + } } #[cfg(feature = "serde")] @@ -77,6 +91,18 @@ impl JumpTable { self.table.as_raw_slice() } + /// Gets the length of the jump map. + #[inline] + pub fn len(&self) -> usize { + self.len + } + + /// Returns true if the jump map is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + /// Constructs a jump map from raw bytes and length. /// /// Bit length represents number of used bits inside slice. diff --git a/crates/bytecode/src/opcode.rs b/crates/bytecode/src/opcode.rs index 4960132313..774ddb37d7 100644 --- a/crates/bytecode/src/opcode.rs +++ b/crates/bytecode/src/opcode.rs @@ -197,6 +197,12 @@ impl OpCode { } } +impl PartialEq for OpCode { + fn eq(&self, other: &u8) -> bool { + self.get().eq(other) + } +} + /// Information about opcode, such as name, and stack inputs and outputs #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OpCodeInfo { diff --git a/crates/context/CHANGELOG.md b/crates/context/CHANGELOG.md index ebba9f84cb..3e7cc93a73 100644 --- a/crates/context/CHANGELOG.md +++ b/crates/context/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.3](https://github.com/bluealloy/revm/compare/revm-context-v8.0.2...revm-context-v8.0.3) - 2025-07-14 + +### Fixed + +- fix typo: Rename is_created_globaly to is_created_globally ([#2692](https://github.com/bluealloy/revm/pull/2692)) + +### Other + +- add comprehensive tests for TxEnvBuilder ([#2690](https://github.com/bluealloy/revm/pull/2690)) + +## [8.0.2](https://github.com/bluealloy/revm/compare/revm-context-v8.0.1...revm-context-v8.0.2) - 2025-07-03 + +### Other + +- updated the following local packages: revm-bytecode, revm-state, revm-database-interface, revm-context-interface + +## [8.0.1](https://github.com/bluealloy/revm/compare/revm-context-v7.0.1...revm-context-v8.0.1) - 2025-06-30 + +### Added + +- implement `Transaction` for `Either` ([#2662](https://github.com/bluealloy/revm/pull/2662)) +- optional_eip3541 ([#2661](https://github.com/bluealloy/revm/pull/2661)) + +### Other + +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) +- fix copy-pasted inner doc comments ([#2663](https://github.com/bluealloy/revm/pull/2663)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/revm-context-v7.0.0...revm-context-v7.0.1) - 2025-06-20 ### Fixed diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index 4fcd07255f..91b2899682 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-context" description = "Revm context crates" -version = "7.0.1" +version = "8.0.3" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -59,6 +59,7 @@ dev = [ "memory_limit", "optional_balance_check", "optional_block_gas_limit", + "optional_eip3541", "optional_eip3607", "optional_no_base_fee", "optional_priority_fee_check", @@ -66,6 +67,7 @@ dev = [ memory_limit = [] optional_balance_check = [] optional_block_gas_limit = [] +optional_eip3541 = [] optional_eip3607 = [] optional_no_base_fee = [] optional_priority_fee_check = [] diff --git a/crates/context/interface/CHANGELOG.md b/crates/context/interface/CHANGELOG.md index 6c9bfe1c30..aebe730102 100644 --- a/crates/context/interface/CHANGELOG.md +++ b/crates/context/interface/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.1](https://github.com/bluealloy/revm/compare/revm-context-interface-v8.0.0...revm-context-interface-v8.0.1) - 2025-07-03 + +### Other + +- updated the following local packages: revm-state, revm-database-interface + +## [8.0.0](https://github.com/bluealloy/revm/compare/revm-context-interface-v7.0.1...revm-context-interface-v8.0.0) - 2025-06-30 + +### Added + +- implement `Transaction` for `Either` ([#2662](https://github.com/bluealloy/revm/pull/2662)) +- optional_eip3541 ([#2661](https://github.com/bluealloy/revm/pull/2661)) + +### Other + +- fix copy-pasted inner doc comments ([#2663](https://github.com/bluealloy/revm/pull/2663)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/revm-context-interface-v7.0.0...revm-context-interface-v7.0.1) - 2025-06-20 ### Fixed diff --git a/crates/context/interface/Cargo.toml b/crates/context/interface/Cargo.toml index 85e47efea2..eeedf23050 100644 --- a/crates/context/interface/Cargo.toml +++ b/crates/context/interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-context-interface" description = "Revm context interface crates" -version = "7.0.1" +version = "8.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/context/interface/src/cfg.rs b/crates/context/interface/src/cfg.rs index 7d9c33939e..79958a2241 100644 --- a/crates/context/interface/src/cfg.rs +++ b/crates/context/interface/src/cfg.rs @@ -41,6 +41,9 @@ pub trait Cfg { /// Returns whether the EIP-3607 (account clearing) is disabled. fn is_eip3607_disabled(&self) -> bool; + /// Returns whether the EIP-3541 (disallowing new contracts with 0xEF prefix) is disabled. + fn is_eip3541_disabled(&self) -> bool; + /// Returns whether the balance check is disabled. fn is_balance_check_disabled(&self) -> bool; diff --git a/crates/context/interface/src/lib.rs b/crates/context/interface/src/lib.rs index 7bb7a36927..cc19e5bc33 100644 --- a/crates/context/interface/src/lib.rs +++ b/crates/context/interface/src/lib.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! EVM execution context interface. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/crates/context/interface/src/transaction.rs b/crates/context/interface/src/transaction.rs index ae727a8704..091512df3e 100644 --- a/crates/context/interface/src/transaction.rs +++ b/crates/context/interface/src/transaction.rs @@ -2,6 +2,7 @@ mod alloy_types; pub mod eip2930; pub mod eip7702; +mod either; pub mod transaction_type; pub use alloy_types::{ @@ -23,7 +24,7 @@ pub trait TransactionError: Debug + core::error::Error {} /// Main Transaction trait that abstracts and specifies all transaction currently supported by Ethereum /// -/// Access to any associated type is gaited behind [`tx_type`][Transaction::tx_type] function. +/// Access to any associated type is gated behind [`tx_type`][Transaction::tx_type] function. /// /// It can be extended to support new transaction types and only transaction types can be /// deprecated by not returning tx_type. diff --git a/crates/context/interface/src/transaction/either.rs b/crates/context/interface/src/transaction/either.rs new file mode 100644 index 0000000000..46eb43cebe --- /dev/null +++ b/crates/context/interface/src/transaction/either.rs @@ -0,0 +1,127 @@ +use super::Transaction; +use either::Either; +use primitives::{Address, Bytes, TxKind, B256, U256}; + +impl Transaction for Either +where + L: Transaction + 'static, + R: for<'a> Transaction< + AccessListItem<'a> = L::AccessListItem<'a>, + Authorization<'a> = L::Authorization<'a>, + > + 'static, +{ + type AccessListItem<'a> + = L::AccessListItem<'a> + where + Self: 'a; + + type Authorization<'a> + = L::Authorization<'a> + where + Self: 'a; + + fn tx_type(&self) -> u8 { + match self { + Either::Left(l) => l.tx_type(), + Either::Right(r) => r.tx_type(), + } + } + + fn caller(&self) -> Address { + match self { + Either::Left(l) => l.caller(), + Either::Right(r) => r.caller(), + } + } + + fn gas_limit(&self) -> u64 { + match self { + Either::Left(l) => l.gas_limit(), + Either::Right(r) => r.gas_limit(), + } + } + + fn value(&self) -> U256 { + match self { + Either::Left(l) => l.value(), + Either::Right(r) => r.value(), + } + } + + fn input(&self) -> &Bytes { + match self { + Either::Left(l) => l.input(), + Either::Right(r) => r.input(), + } + } + + fn nonce(&self) -> u64 { + match self { + Either::Left(l) => l.nonce(), + Either::Right(r) => r.nonce(), + } + } + + fn kind(&self) -> TxKind { + match self { + Either::Left(l) => l.kind(), + Either::Right(r) => r.kind(), + } + } + + fn chain_id(&self) -> Option { + match self { + Either::Left(l) => l.chain_id(), + Either::Right(r) => r.chain_id(), + } + } + + fn gas_price(&self) -> u128 { + match self { + Either::Left(l) => l.gas_price(), + Either::Right(r) => r.gas_price(), + } + } + + fn access_list(&self) -> Option>> { + match self { + Either::Left(l) => l.access_list().map(Either::Left), + Either::Right(r) => r.access_list().map(Either::Right), + } + } + + fn blob_versioned_hashes(&self) -> &[B256] { + match self { + Either::Left(l) => l.blob_versioned_hashes(), + Either::Right(r) => r.blob_versioned_hashes(), + } + } + + fn max_fee_per_blob_gas(&self) -> u128 { + match self { + Either::Left(l) => l.max_fee_per_blob_gas(), + Either::Right(r) => r.max_fee_per_blob_gas(), + } + } + + fn authorization_list_len(&self) -> usize { + match self { + Either::Left(l) => l.authorization_list_len(), + Either::Right(r) => r.authorization_list_len(), + } + } + + fn authorization_list(&self) -> impl Iterator> { + match self { + Either::Left(l) => Either::Left(l.authorization_list()), + Either::Right(r) => Either::Right(r.authorization_list()), + } + } + + fn max_priority_fee_per_gas(&self) -> Option { + match self { + Either::Left(l) => l.max_priority_fee_per_gas(), + Either::Right(r) => r.max_priority_fee_per_gas(), + } + } +} diff --git a/crates/context/src/cfg.rs b/crates/context/src/cfg.rs index 7b57079cf2..4846549d01 100644 --- a/crates/context/src/cfg.rs +++ b/crates/context/src/cfg.rs @@ -78,6 +78,13 @@ pub struct CfgEnv { /// By default, it is set to `false`. #[cfg(feature = "optional_block_gas_limit")] pub disable_block_gas_limit: bool, + /// EIP-3541 rejects the creation of contracts that starts with 0xEF + /// + /// This is useful for chains that do not implement EIP-3541. + /// + /// By default, it is set to `false`. + #[cfg(feature = "optional_eip3541")] + pub disable_eip3541: bool, /// EIP-3607 rejects transactions from senders with deployed code /// /// In development, it can be desirable to simulate calls from contracts, which this setting allows. @@ -154,6 +161,8 @@ impl CfgEnv { disable_balance_check: false, #[cfg(feature = "optional_block_gas_limit")] disable_block_gas_limit: false, + #[cfg(feature = "optional_eip3541")] + disable_eip3541: false, #[cfg(feature = "optional_eip3607")] disable_eip3607: false, #[cfg(feature = "optional_no_base_fee")] @@ -203,6 +212,8 @@ impl CfgEnv { disable_balance_check: self.disable_balance_check, #[cfg(feature = "optional_block_gas_limit")] disable_block_gas_limit: self.disable_block_gas_limit, + #[cfg(feature = "optional_eip3541")] + disable_eip3541: self.disable_eip3541, #[cfg(feature = "optional_eip3607")] disable_eip3607: self.disable_eip3607, #[cfg(feature = "optional_no_base_fee")] @@ -301,6 +312,16 @@ impl + Copy> Cfg for CfgEnv { .unwrap_or(eip3860::MAX_INITCODE_SIZE) } + fn is_eip3541_disabled(&self) -> bool { + cfg_if::cfg_if! { + if #[cfg(feature = "optional_eip3541")] { + self.disable_eip3541 + } else { + false + } + } + } + fn is_eip3607_disabled(&self) -> bool { cfg_if::cfg_if! { if #[cfg(feature = "optional_eip3607")] { diff --git a/crates/context/src/journal/entry.rs b/crates/context/src/journal/entry.rs index 52b7a4b953..00b5f17219 100644 --- a/crates/context/src/journal/entry.rs +++ b/crates/context/src/journal/entry.rs @@ -39,7 +39,7 @@ pub trait JournalEntryTr { fn nonce_changed(address: Address) -> Self; /// Creates a journal entry for when a new account is created - fn account_created(address: Address, is_created_globaly: bool) -> Self; + fn account_created(address: Address, is_created_globally: bool) -> Self; /// Creates a journal entry for when a storage slot is modified /// Records the previous value for reverting diff --git a/crates/context/src/journal/inner.rs b/crates/context/src/journal/inner.rs index e0c9a16445..0b805e8047 100644 --- a/crates/context/src/journal/inner.rs +++ b/crates/context/src/journal/inner.rs @@ -433,10 +433,10 @@ impl JournalInner { } // set account status to create. - let is_created_globaly = target_acc.mark_created_locally(); + let is_created_globally = target_acc.mark_created_locally(); // this entry will revert set nonce. - last_journal.push(ENTRY::account_created(target_address, is_created_globaly)); + last_journal.push(ENTRY::account_created(target_address, is_created_globally)); target_acc.info.code = None; // EIP-161: State trie clearing (invariant-preserving alternative) if spec_id.is_enabled_in(SPURIOUS_DRAGON) { diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index 554f25a2f4..301f0ae073 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! EVM execution context. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/crates/context/src/tx.rs b/crates/context/src/tx.rs index f7e90cc861..b7752a3aea 100644 --- a/crates/context/src/tx.rs +++ b/crates/context/src/tx.rs @@ -274,6 +274,11 @@ impl TxEnvBuilder { self } + /// Get the transaction type + pub fn get_tx_type(&self) -> Option { + self.tx_type + } + /// Set the caller address pub fn caller(mut self, caller: Address) -> Self { self.caller = caller; @@ -378,9 +383,20 @@ impl TxEnvBuilder { self } + /// Insert a list of signed authorizations into the authorization list. + pub fn authorization_list_signed(mut self, auth: Vec) -> Self { + self.authorization_list = auth.into_iter().map(Either::Left).collect(); + self + } + + /// Insert a list of recovered authorizations into the authorization list. + pub fn authorization_list_recovered(mut self, auth: Vec) -> Self { + self.authorization_list = auth.into_iter().map(Either::Right).collect(); + self + } + /// Build the final [`TxEnv`] with default values for missing fields. pub fn build_fill(mut self) -> TxEnv { - let tx_type_not_set = self.tx_type.is_some(); if let Some(tx_type) = self.tx_type { match TransactionType::from(tx_type) { TransactionType::Legacy => { @@ -460,7 +476,7 @@ impl TxEnvBuilder { }; // if tx_type is not set, derive it from fields and fix errors. - if tx_type_not_set { + if self.tx_type.is_none() { match tx.derive_tx_type() { Ok(_) => {} Err(DeriveTxTypeError::MissingTargetForEip4844) => { @@ -528,8 +544,8 @@ impl TxEnvBuilder { return Err(DeriveTxTypeError::MissingTargetForEip4844.into()); } } - _ => { - panic!() + TransactionType::Custom => { + // do nothing, custom transaction type is handled by the caller. } } } @@ -552,7 +568,9 @@ impl TxEnvBuilder { }; // Derive tx type from fields, if some fields are wrongly set it will return an error. - tx.derive_tx_type()?; + if self.tx_type.is_none() { + tx.derive_tx_type()?; + } Ok(tx) } @@ -647,6 +665,436 @@ mod tests { tx.effective_gas_price(base_fee) } + #[test] + fn test_tx_env_builder_build_valid_legacy() { + // Legacy transaction + let tx = TxEnvBuilder::new() + .tx_type(Some(0)) + .caller(Address::from([1u8; 20])) + .gas_limit(21000) + .gas_price(20) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .value(U256::from(100)) + .data(Bytes::from(vec![0x01, 0x02])) + .nonce(5) + .chain_id(Some(1)) + .build() + .unwrap(); + + assert_eq!(tx.kind, TxKind::Call(Address::from([2u8; 20]))); + assert_eq!(tx.caller, Address::from([1u8; 20])); + assert_eq!(tx.gas_limit, 21000); + assert_eq!(tx.gas_price, 20); + assert_eq!(tx.value, U256::from(100)); + assert_eq!(tx.data, Bytes::from(vec![0x01, 0x02])); + assert_eq!(tx.nonce, 5); + assert_eq!(tx.chain_id, Some(1)); + assert_eq!(tx.tx_type, TransactionType::Legacy); + } + + #[test] + fn test_tx_env_builder_build_valid_eip2930() { + // EIP-2930 transaction with access list + let access_list = AccessList(vec![AccessListItem { + address: Address::from([3u8; 20]), + storage_keys: vec![B256::from([4u8; 32])], + }]); + let tx = TxEnvBuilder::new() + .tx_type(Some(1)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(25) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .access_list(access_list.clone()) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip2930); + assert_eq!(tx.access_list, access_list); + } + + #[test] + fn test_tx_env_builder_build_valid_eip1559() { + // EIP-1559 transaction + let tx = TxEnvBuilder::new() + .tx_type(Some(2)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(30) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip1559); + assert_eq!(tx.gas_priority_fee, Some(10)); + } + + #[test] + fn test_tx_env_builder_build_valid_eip4844() { + // EIP-4844 blob transaction + let blob_hashes = vec![B256::from([5u8; 32]), B256::from([6u8; 32])]; + let tx = TxEnvBuilder::new() + .tx_type(Some(3)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(30) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .blob_hashes(blob_hashes.clone()) + .max_fee_per_blob_gas(100) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip4844); + assert_eq!(tx.blob_hashes, blob_hashes); + assert_eq!(tx.max_fee_per_blob_gas, 100); + } + + #[test] + fn test_tx_env_builder_build_valid_eip7702() { + // EIP-7702 EOA code transaction + let auth = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::from(1), + nonce: 0, + address: Address::default(), + }, + RecoveredAuthority::Valid(Address::default()), + ); + let auth_list = vec![Either::Right(auth)]; + + let tx = TxEnvBuilder::new() + .tx_type(Some(4)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(30) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .authorization_list(auth_list.clone()) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip7702); + assert_eq!(tx.authorization_list.len(), 1); + } + + #[test] + fn test_tx_env_builder_build_create_transaction() { + // Contract creation transaction + let bytecode = Bytes::from(vec![0x60, 0x80, 0x60, 0x40]); + let tx = TxEnvBuilder::new() + .kind(TxKind::Create) + .data(bytecode.clone()) + .gas_limit(100000) + .gas_price(20) + .build() + .unwrap(); + + assert_eq!(tx.kind, TxKind::Create); + assert_eq!(tx.data, bytecode); + } + + #[test] + fn test_tx_env_builder_build_errors_eip1559_missing_priority_fee() { + // EIP-1559 without gas_priority_fee should fail + let result = TxEnvBuilder::new() + .tx_type(Some(2)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(30) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build(); + + assert!(matches!( + result, + Err(TxEnvBuildError::MissingGasPriorityFeeForEip1559) + )); + } + + #[test] + fn test_tx_env_builder_build_errors_eip4844_missing_blob_hashes() { + // EIP-4844 without blob hashes should fail + let result = TxEnvBuilder::new() + .tx_type(Some(3)) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build(); + + assert!(matches!( + result, + Err(TxEnvBuildError::MissingBlobHashesForEip4844) + )); + } + + #[test] + fn test_tx_env_builder_build_errors_eip4844_not_call() { + // EIP-4844 with Create should fail + let result = TxEnvBuilder::new() + .tx_type(Some(3)) + .gas_priority_fee(Some(10)) + .blob_hashes(vec![B256::from([5u8; 32])]) + .kind(TxKind::Create) + .build(); + + assert!(matches!( + result, + Err(TxEnvBuildError::MissingTargetForEip4844) + )); + } + + #[test] + fn test_tx_env_builder_build_errors_eip7702_missing_auth_list() { + // EIP-7702 without authorization list should fail + let result = TxEnvBuilder::new() + .tx_type(Some(4)) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build(); + + assert!(matches!( + result, + Err(TxEnvBuildError::MissingAuthorizationListForEip7702) + )); + } + + #[test] + fn test_tx_env_builder_build_errors_eip7702_not_call() { + // EIP-7702 with Create should fail + let auth = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::from(1), + nonce: 0, + address: Address::default(), + }, + RecoveredAuthority::Valid(Address::default()), + ); + let result = TxEnvBuilder::new() + .tx_type(Some(4)) + .gas_priority_fee(Some(10)) + .authorization_list(vec![Either::Right(auth)]) + .kind(TxKind::Create) + .build(); + + assert!(matches!(result, Err(TxEnvBuildError::DeriveErr(_)))); + } + + #[test] + fn test_tx_env_builder_build_fill_legacy() { + // Legacy transaction with build_fill + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_limit(21000) + .gas_price(20) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Legacy); + assert_eq!(tx.gas_priority_fee, None); + } + + #[test] + fn test_tx_env_builder_build_fill_eip1559_missing_priority_fee() { + // EIP-1559 without gas_priority_fee should be filled with 0 + let tx = TxEnvBuilder::new() + .tx_type(Some(2)) + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(30) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Eip1559); + assert_eq!(tx.gas_priority_fee, Some(0)); + } + + #[test] + fn test_tx_env_builder_build_fill_eip4844_missing_blob_hashes() { + // EIP-4844 without blob hashes should add default blob hash + let tx = TxEnvBuilder::new() + .tx_type(Some(3)) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Eip4844); + assert_eq!(tx.blob_hashes.len(), 1); + assert_eq!(tx.blob_hashes[0], B256::default()); + } + + #[test] + fn test_tx_env_builder_build_fill_eip4844_create_to_call() { + // EIP-4844 with Create should be converted to Call + let tx = TxEnvBuilder::new() + .tx_type(Some(3)) + .gas_priority_fee(Some(10)) + .blob_hashes(vec![B256::from([5u8; 32])]) + .kind(TxKind::Create) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Eip4844); + assert_eq!(tx.kind, TxKind::Call(Address::default())); + } + + #[test] + fn test_tx_env_builder_build_fill_eip7702_missing_auth_list() { + // EIP-7702 without authorization list should add dummy auth + let tx = TxEnvBuilder::new() + .tx_type(Some(4)) + .gas_priority_fee(Some(10)) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Eip7702); + assert_eq!(tx.authorization_list.len(), 1); + } + + #[test] + fn test_tx_env_builder_build_fill_eip7702_create_to_call() { + // EIP-7702 with Create should be converted to Call + let auth = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::from(1), + nonce: 0, + address: Address::default(), + }, + RecoveredAuthority::Valid(Address::default()), + ); + let tx = TxEnvBuilder::new() + .tx_type(Some(4)) + .gas_priority_fee(Some(10)) + .authorization_list(vec![Either::Right(auth)]) + .kind(TxKind::Create) + .build_fill(); + + assert_eq!(tx.tx_type, TransactionType::Eip7702); + assert_eq!(tx.kind, TxKind::Call(Address::default())); + } + + #[test] + fn test_tx_env_builder_derive_tx_type_legacy() { + // No special fields, should derive Legacy + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_limit(21000) + .gas_price(20) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Legacy); + } + + #[test] + fn test_tx_env_builder_derive_tx_type_eip2930() { + // Access list present, should derive EIP-2930 + let access_list = AccessList(vec![AccessListItem { + address: Address::from([3u8; 20]), + storage_keys: vec![B256::from([4u8; 32])], + }]); + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .access_list(access_list) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip2930); + } + + #[test] + fn test_tx_env_builder_derive_tx_type_eip1559() { + // Gas priority fee present, should derive EIP-1559 + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_priority_fee(Some(10)) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip1559); + } + + #[test] + fn test_tx_env_builder_derive_tx_type_eip4844() { + // Blob hashes present, should derive EIP-4844 + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_priority_fee(Some(10)) + .blob_hashes(vec![B256::from([5u8; 32])]) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip4844); + } + + #[test] + fn test_tx_env_builder_derive_tx_type_eip7702() { + // Authorization list present, should derive EIP-7702 + let auth = RecoveredAuthorization::new_unchecked( + Authorization { + chain_id: U256::from(1), + nonce: 0, + address: Address::default(), + }, + RecoveredAuthority::Valid(Address::default()), + ); + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_priority_fee(Some(10)) + .authorization_list(vec![Either::Right(auth)]) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Eip7702); + } + + #[test] + fn test_tx_env_builder_custom_tx_type() { + // Custom transaction type (0xFF) + let tx = TxEnvBuilder::new() + .tx_type(Some(0xFF)) + .caller(Address::from([1u8; 20])) + .build() + .unwrap(); + + assert_eq!(tx.tx_type, TransactionType::Custom); + } + + #[test] + fn test_tx_env_builder_chain_methods() { + // Test method chaining + let tx = TxEnvBuilder::new() + .caller(Address::from([1u8; 20])) + .gas_limit(50000) + .gas_price(25) + .kind(TxKind::Call(Address::from([2u8; 20]))) + .value(U256::from(1000)) + .data(Bytes::from(vec![0x12, 0x34])) + .nonce(10) + .chain_id(Some(5)) + .access_list(AccessList(vec![AccessListItem { + address: Address::from([3u8; 20]), + storage_keys: vec![], + }])) + .gas_priority_fee(Some(5)) + .blob_hashes(vec![B256::from([7u8; 32])]) + .max_fee_per_blob_gas(200) + .build_fill(); + + assert_eq!(tx.caller, Address::from([1u8; 20])); + assert_eq!(tx.gas_limit, 50000); + assert_eq!(tx.gas_price, 25); + assert_eq!(tx.kind, TxKind::Call(Address::from([2u8; 20]))); + assert_eq!(tx.value, U256::from(1000)); + assert_eq!(tx.data, Bytes::from(vec![0x12, 0x34])); + assert_eq!(tx.nonce, 10); + assert_eq!(tx.chain_id, Some(5)); + assert_eq!(tx.access_list.len(), 1); + assert_eq!(tx.gas_priority_fee, Some(5)); + assert_eq!(tx.blob_hashes.len(), 1); + assert_eq!(tx.max_fee_per_blob_gas, 200); + } + #[test] fn test_effective_gas_price() { assert_eq!(90, effective_gas_setup(TransactionType::Legacy, 90, None)); diff --git a/crates/database/CHANGELOG.md b/crates/database/CHANGELOG.md index 45067c84e8..5fa0aaf757 100644 --- a/crates/database/CHANGELOG.md +++ b/crates/database/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.1](https://github.com/bluealloy/revm/compare/revm-database-v7.0.0...revm-database-v7.0.1) - 2025-07-03 + +### Other + +- updated the following local packages: revm-bytecode, revm-state, revm-database-interface + +## [7.0.0](https://github.com/bluealloy/revm/compare/revm-database-v6.0.0...revm-database-v7.0.0) - 2025-06-30 + +### Other + +- updated the following local packages: revm-bytecode, revm-state, revm-database-interface + ## [6.0.0](https://github.com/bluealloy/revm/compare/revm-database-v5.0.0...revm-database-v6.0.0) - 2025-06-19 ### Added diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 377e15440d..efc86c4e3c 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-database" description = "Revm Database implementations" -version = "6.0.0" +version = "7.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/database/interface/CHANGELOG.md b/crates/database/interface/CHANGELOG.md index c3408e2a36..656286fd1e 100644 --- a/crates/database/interface/CHANGELOG.md +++ b/crates/database/interface/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.0.1](https://github.com/bluealloy/revm/compare/revm-database-interface-v7.0.0...revm-database-interface-v7.0.1) - 2025-07-03 + +### Other + +- updated the following local packages: revm-state + +## [7.0.0](https://github.com/bluealloy/revm/compare/revm-database-interface-v6.0.0...revm-database-interface-v7.0.0) - 2025-06-30 + +### Added + +- implement Database traits for either::Either ([#2673](https://github.com/bluealloy/revm/pull/2673)) + +### Other + +- fix copy-pasted inner doc comments ([#2663](https://github.com/bluealloy/revm/pull/2663)) + ## [6.0.0](https://github.com/bluealloy/revm/compare/revm-database-interface-v5.0.0...revm-database-interface-v6.0.0) - 2025-06-19 ### Added diff --git a/crates/database/interface/Cargo.toml b/crates/database/interface/Cargo.toml index d521defb8b..125e957d77 100644 --- a/crates/database/interface/Cargo.toml +++ b/crates/database/interface/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-database-interface" description = "Revm Database interface" -version = "6.0.0" +version = "7.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -23,6 +23,7 @@ primitives.workspace = true # misc auto_impl.workspace = true +either.workspace = true # Optional serde = { workspace = true, features = ["derive", "rc"], optional = true } @@ -36,6 +37,16 @@ rstest.workspace = true [features] default = ["std"] -std = ["serde?/std", "primitives/std", "state/std"] -serde = ["dep:serde", "primitives/serde", "state/serde"] +std = [ + "serde?/std", + "primitives/std", + "state/std", + "either/std" +] +serde = [ + "dep:serde", + "primitives/serde", + "state/serde", + "either/serde" +] asyncdb = ["dep:tokio", "tokio/rt-multi-thread"] diff --git a/crates/database/interface/src/either.rs b/crates/database/interface/src/either.rs new file mode 100644 index 0000000000..a22210cb4b --- /dev/null +++ b/crates/database/interface/src/either.rs @@ -0,0 +1,99 @@ +//! Database implementations for `either::Either` type. + +use crate::{Database, DatabaseCommit, DatabaseRef}; +use either::Either; +use primitives::{Address, HashMap, StorageKey, StorageValue, B256}; +use state::{Account, AccountInfo, Bytecode}; + +impl Database for Either +where + L: Database, + R: Database, +{ + type Error = L::Error; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + match self { + Self::Left(db) => db.basic(address), + Self::Right(db) => db.basic(address), + } + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + match self { + Self::Left(db) => db.code_by_hash(code_hash), + Self::Right(db) => db.code_by_hash(code_hash), + } + } + + fn storage( + &mut self, + address: Address, + index: StorageKey, + ) -> Result { + match self { + Self::Left(db) => db.storage(address, index), + Self::Right(db) => db.storage(address, index), + } + } + + fn block_hash(&mut self, number: u64) -> Result { + match self { + Self::Left(db) => db.block_hash(number), + Self::Right(db) => db.block_hash(number), + } + } +} + +impl DatabaseCommit for Either +where + L: DatabaseCommit, + R: DatabaseCommit, +{ + fn commit(&mut self, changes: HashMap) { + match self { + Self::Left(db) => db.commit(changes), + Self::Right(db) => db.commit(changes), + } + } +} + +impl DatabaseRef for Either +where + L: DatabaseRef, + R: DatabaseRef, +{ + type Error = L::Error; + + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + match self { + Self::Left(db) => db.basic_ref(address), + Self::Right(db) => db.basic_ref(address), + } + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + match self { + Self::Left(db) => db.code_by_hash_ref(code_hash), + Self::Right(db) => db.code_by_hash_ref(code_hash), + } + } + + fn storage_ref( + &self, + address: Address, + index: StorageKey, + ) -> Result { + match self { + Self::Left(db) => db.storage_ref(address, index), + Self::Right(db) => db.storage_ref(address, index), + } + } + + fn block_hash_ref(&self, number: u64) -> Result { + match self { + Self::Left(db) => db.block_hash_ref(number), + Self::Right(db) => db.block_hash_ref(number), + } + } +} diff --git a/crates/database/interface/src/lib.rs b/crates/database/interface/src/lib.rs index ef21676b9a..c64e398188 100644 --- a/crates/database/interface/src/lib.rs +++ b/crates/database/interface/src/lib.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! Database interface. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] @@ -28,6 +28,7 @@ pub const BENCH_CALLER_BALANCE: U256 = U256::from_limbs([10_000_000_000_000_000, #[cfg(feature = "asyncdb")] pub mod async_db; +pub mod either; pub mod empty_db; pub mod try_commit; diff --git a/crates/handler/CHANGELOG.md b/crates/handler/CHANGELOG.md index 8f44c93d95..86e4ebfab4 100644 --- a/crates/handler/CHANGELOG.md +++ b/crates/handler/CHANGELOG.md @@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [8.0.3](https://github.com/bluealloy/revm/compare/revm-handler-v8.0.2...revm-handler-v8.0.3) - 2025-07-14 + +### Other + +- simplify gas calculations by introducing a used() method ([#2703](https://github.com/bluealloy/revm/pull/2703)) + +## [8.0.2](https://github.com/bluealloy/revm/compare/revm-handler-v8.0.1...revm-handler-v8.0.2) - 2025-07-03 + +### Other + +- document external state transitions for EIP-4788 and EIP-2935 ([#2678](https://github.com/bluealloy/revm/pull/2678)) +- minor fixes ([#2686](https://github.com/bluealloy/revm/pull/2686)) +- fix in pre_execution.rs about nonce bump for CREATE ([#2684](https://github.com/bluealloy/revm/pull/2684)) + +## [8.0.1](https://github.com/bluealloy/revm/compare/revm-handler-v7.0.1...revm-handler-v8.0.1) - 2025-06-30 + +### Added + +- optional_eip3541 ([#2661](https://github.com/bluealloy/revm/pull/2661)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) +- fix copy-pasted inner doc comments ([#2663](https://github.com/bluealloy/revm/pull/2663)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/revm-handler-v7.0.0...revm-handler-v7.0.1) - 2025-06-20 ### Fixed diff --git a/crates/handler/Cargo.toml b/crates/handler/Cargo.toml index 3a15dc471b..080c2d615f 100644 --- a/crates/handler/Cargo.toml +++ b/crates/handler/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-handler" description = "Revm handler crates" -version = "7.0.1" +version = "8.0.3" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/handler/src/api.rs b/crates/handler/src/api.rs index 580c7231b7..020d03e4df 100644 --- a/crates/handler/src/api.rs +++ b/crates/handler/src/api.rs @@ -55,7 +55,7 @@ pub trait ExecuteEvm { /// Transact the given transaction and finalize in a single operation. /// - /// Internally calls [`ExecuteEvm::transact`] followed by [`ExecuteEvm::finalize`]. + /// Internally calls [`ExecuteEvm::transact_one`] followed by [`ExecuteEvm::finalize`]. /// /// # Outcome of Error /// @@ -111,8 +111,6 @@ pub trait ExecuteEvm { } /// Execute previous transaction and finalize it. - /// - /// Doint it without finalization fn replay( &mut self, ) -> Result, Self::Error>; @@ -142,7 +140,7 @@ pub trait ExecuteCommitEvm: ExecuteEvm { /// Transact multiple transactions and commit to the state. /// - /// Internally calls `transact_multi` and `commit` functions. + /// Internally calls `transact_many` and `commit_inner` functions. #[inline] fn transact_many_commit( &mut self, diff --git a/crates/handler/src/frame.rs b/crates/handler/src/frame.rs index d29c1a13dd..6fdb853851 100644 --- a/crates/handler/src/frame.rs +++ b/crates/handler/src/frame.rs @@ -555,12 +555,14 @@ impl EthFrame { } FrameData::Create(frame) => { let max_code_size = context.cfg().max_code_size(); + let is_eip3541_disabled = context.cfg().is_eip3541_disabled(); return_create( context.journal_mut(), self.checkpoint, &mut interpreter_result, frame.created_address, max_code_size, + is_eip3541_disabled, spec, ); @@ -673,6 +675,7 @@ pub fn return_create( interpreter_result: &mut InterpreterResult, address: Address, max_code_size: usize, + is_eip3541_disabled: bool, spec_id: SpecId, ) { // If return is not ok revert and return. @@ -684,7 +687,10 @@ pub fn return_create( // If ok, check contract creation limit and calculate gas deduction on output len. // // EIP-3541: Reject new contract code starting with the 0xEF byte - if spec_id.is_enabled_in(LONDON) && interpreter_result.output.first() == Some(&0xEF) { + if !is_eip3541_disabled + && spec_id.is_enabled_in(LONDON) + && interpreter_result.output.first() == Some(&0xEF) + { journal.checkpoint_revert(checkpoint); interpreter_result.result = InstructionResult::CreateContractStartingWithEF; return; diff --git a/crates/handler/src/lib.rs b/crates/handler/src/lib.rs index abe48f8f4f..a220439545 100644 --- a/crates/handler/src/lib.rs +++ b/crates/handler/src/lib.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! EVM execution handling. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/crates/handler/src/mainnet_builder.rs b/crates/handler/src/mainnet_builder.rs index 3163e26933..4e53287c93 100644 --- a/crates/handler/src/mainnet_builder.rs +++ b/crates/handler/src/mainnet_builder.rs @@ -82,7 +82,7 @@ mod test { Bytecode, }; use context::{Context, TxEnv}; - use context_interface::{transaction::Authorization, TransactionType}; + use context_interface::transaction::Authorization; use database::{BenchmarkDB, EEADDRESS, FFADDRESS}; use primitives::{hardfork::SpecId, TxKind, U256}; use primitives::{StorageKey, StorageValue}; @@ -107,14 +107,15 @@ mod test { let mut evm = ctx.build_mainnet(); let state = evm - .transact(TxEnv { - tx_type: TransactionType::Eip7702.into(), - gas_limit: 100_000, - authorization_list: vec![Either::Left(auth)], - caller: EEADDRESS, - kind: TxKind::Call(signer.address()), - ..Default::default() - }) + .transact( + TxEnv::builder() + .gas_limit(100_000) + .authorization_list(vec![Either::Left(auth)]) + .caller(EEADDRESS) + .kind(TxKind::Call(signer.address())) + .build() + .unwrap(), + ) .unwrap() .state; diff --git a/crates/handler/src/post_execution.rs b/crates/handler/src/post_execution.rs index f1a3501c54..71ded059ef 100644 --- a/crates/handler/src/post_execution.rs +++ b/crates/handler/src/post_execution.rs @@ -71,7 +71,7 @@ pub fn reward_beneficiary( // reward beneficiary context.journal_mut().balance_incr( beneficiary, - U256::from(coinbase_gas_price * (gas.spent() - gas.refunded() as u64) as u128), + U256::from(coinbase_gas_price * gas.used() as u128), )?; Ok(()) @@ -88,7 +88,7 @@ pub fn output>, HALTREASON: ) -> ExecutionResult { // Used gas with refund calculated. let gas_refunded = result.gas().refunded() as u64; - let final_gas_used = result.gas().spent() - gas_refunded; + let gas_used = result.gas().used(); let output = result.output(); let instruction_result = result.into_interpreter_result(); @@ -98,19 +98,16 @@ pub fn output>, HALTREASON: match SuccessOrHalt::::from(instruction_result.result) { SuccessOrHalt::Success(reason) => ExecutionResult::Success { reason, - gas_used: final_gas_used, + gas_used, gas_refunded, logs, output, }, SuccessOrHalt::Revert => ExecutionResult::Revert { - gas_used: final_gas_used, + gas_used, output: output.into_data(), }, - SuccessOrHalt::Halt(reason) => ExecutionResult::Halt { - reason, - gas_used: final_gas_used, - }, + SuccessOrHalt::Halt(reason) => ExecutionResult::Halt { reason, gas_used }, // Only two internal return flags. flag @ (SuccessOrHalt::FatalExternalError | SuccessOrHalt::Internal(_)) => { panic!( diff --git a/crates/handler/src/pre_execution.rs b/crates/handler/src/pre_execution.rs index 6266c0d6a3..cd823e28e5 100644 --- a/crates/handler/src/pre_execution.rs +++ b/crates/handler/src/pre_execution.rs @@ -138,7 +138,7 @@ pub fn validate_against_state_and_deduct_caller< is_nonce_check_disabled, )?; - // Bump the nonce for calls. Nonce for CREATE will be bumped in `handle_create`. + // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`. if tx.kind().is_call() { // Nonce is already checked caller_account.info.nonce = caller_account.info.nonce.saturating_add(1); diff --git a/crates/handler/src/system_call.rs b/crates/handler/src/system_call.rs index 86adfd2806..5314d48461 100644 --- a/crates/handler/src/system_call.rs +++ b/crates/handler/src/system_call.rs @@ -1,10 +1,29 @@ +//! System call logic for external state transitions required by certain EIPs (notably [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) and [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788)). +//! +//! These EIPs require the client to perform special system calls to update state (such as block hashes or beacon roots) at block boundaries, outside of normal EVM transaction execution. REVM provides the system call mechanism, but the actual state transitions must be performed by the client or test harness, not by the EVM itself. +//! +//! # Example: Using `transact_system_call` for pre/post block hooks +//! +//! The client should use [`SystemCallEvm::transact_system_call`] method to perform required state updates before or after block execution, as specified by the EIP: +//! +//! ```rust,ignore +//! // Example: update beacon root (EIP-4788) at the start of a block +//! let beacon_root: Bytes = ...; // obtained from consensus layer +//! let beacon_contract: Address = "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02".parse().unwrap(); +//! evm.transact_system_call(beacon_contract, beacon_root)?; +//! +//! // Example: update block hash (EIP-2935) at the end of a block +//! let block_hash: Bytes = ...; // new block hash +//! let history_contract: Address = "0x0000F90827F1C53a10cb7A02335B175320002935".parse().unwrap(); +//! evm.transact_system_call(history_contract, block_hash)?; +//! ``` +//! +//! See the book section on [External State Transitions](../../book/src/external_state_transitions.md) for more details. use crate::{ frame::EthFrame, instructions::InstructionProvider, ExecuteCommitEvm, ExecuteEvm, Handler, MainnetHandler, PrecompileProvider, }; -use context::{ - result::ExecResultAndState, ContextSetters, ContextTr, Evm, JournalTr, TransactionType, TxEnv, -}; +use context::{result::ExecResultAndState, ContextSetters, ContextTr, Evm, JournalTr, TxEnv}; use database_interface::DatabaseCommit; use interpreter::{interpreter::EthInterpreter, InterpreterResult}; use primitives::{address, eip7825, Address, Bytes, TxKind}; @@ -39,14 +58,13 @@ impl SystemCallTx for TxEnv { system_contract_address: Address, data: Bytes, ) -> Self { - TxEnv { - tx_type: TransactionType::Legacy as u8, - caller, - data, - kind: TxKind::Call(system_contract_address), - gas_limit: eip7825::TX_GAS_LIMIT_CAP, - ..Default::default() - } + TxEnv::builder() + .caller(caller) + .data(data) + .kind(TxKind::Call(system_contract_address)) + .gas_limit(eip7825::TX_GAS_LIMIT_CAP) + .build() + .unwrap() } } diff --git a/crates/handler/src/validation.rs b/crates/handler/src/validation.rs index 0f09c3cb09..6c956cbc66 100644 --- a/crates/handler/src/validation.rs +++ b/crates/handler/src/validation.rs @@ -328,10 +328,6 @@ mod tests { spec_id: Option, ) -> Result> { let ctx = Context::mainnet() - .modify_tx_chained(|tx| { - tx.kind = TxKind::Create; - tx.data = bytecode.clone(); - }) .modify_cfg_chained(|c| { if let Some(spec_id) = spec_id { c.spec = spec_id; @@ -340,11 +336,13 @@ mod tests { .with_db(CacheDB::::default()); let mut evm = ctx.build_mainnet(); - evm.transact_commit(TxEnv { - kind: TxKind::Create, - data: bytecode.clone(), - ..Default::default() - }) + evm.transact_commit( + TxEnv::builder() + .kind(TxKind::Create) + .data(bytecode.clone()) + .build() + .unwrap(), + ) } #[test] @@ -501,12 +499,14 @@ mod tests { let call_result = Context::mainnet() .with_db(CacheDB::::default()) .build_mainnet() - .transact_commit(TxEnv { - caller: tx_caller, - kind: TxKind::Call(factory_address), - data: Bytes::new(), - ..Default::default() - }) + .transact_commit( + TxEnv::builder() + .caller(tx_caller) + .kind(TxKind::Call(factory_address)) + .data(Bytes::new()) + .build() + .unwrap(), + ) .expect("call factory contract failed"); match &call_result { @@ -583,12 +583,14 @@ mod tests { let call_result = Context::mainnet() .with_db(CacheDB::::default()) .build_mainnet() - .transact_commit(TxEnv { - caller: tx_caller, - kind: TxKind::Call(factory_address), - data: Bytes::new(), - ..Default::default() - }) + .transact_commit( + TxEnv::builder() + .caller(tx_caller) + .kind(TxKind::Call(factory_address)) + .data(Bytes::new()) + .build() + .unwrap(), + ) .expect("call factory contract failed"); match &call_result { diff --git a/crates/inspector/CHANGELOG.md b/crates/inspector/CHANGELOG.md index 7578f55547..7d2d4e002e 100644 --- a/crates/inspector/CHANGELOG.md +++ b/crates/inspector/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.3](https://github.com/bluealloy/revm/compare/revm-inspector-v8.0.2...revm-inspector-v8.0.3) - 2025-07-14 + +### Fixed + +- *(Inspector)* call_end not calle on first call fast return ([#2697](https://github.com/bluealloy/revm/pull/2697)) + +## [8.0.2](https://github.com/bluealloy/revm/compare/revm-inspector-v8.0.1...revm-inspector-v8.0.2) - 2025-07-03 + +### Fixed + +- *(inspector)* revert pointer before calling step_end ([#2687](https://github.com/bluealloy/revm/pull/2687)) + +### Other + +- minor fixes ([#2686](https://github.com/bluealloy/revm/pull/2686)) + +## [8.0.1](https://github.com/bluealloy/revm/compare/revm-inspector-v7.0.1...revm-inspector-v8.0.1) - 2025-06-30 + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/revm-inspector-v7.0.0...revm-inspector-v7.0.1) - 2025-06-20 ### Other diff --git a/crates/inspector/Cargo.toml b/crates/inspector/Cargo.toml index ac5ea92122..be8b5b8041 100644 --- a/crates/inspector/Cargo.toml +++ b/crates/inspector/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-inspector" description = "Revm inspector interface" -version = "7.0.1" +version = "8.0.3" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/inspector/src/gas.rs b/crates/inspector/src/gas.rs index abaef6d48e..db286d4ca1 100644 --- a/crates/inspector/src/gas.rs +++ b/crates/inspector/src/gas.rs @@ -92,6 +92,7 @@ mod tests { #[derive(Default, Debug)] struct StackInspector { pc: usize, + opcode: u8, gas_inspector: GasInspector, gas_remaining_steps: Vec<(usize, u64)>, } @@ -103,10 +104,13 @@ mod tests { fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { self.pc = interp.bytecode.pc(); + self.opcode = interp.bytecode.opcode(); self.gas_inspector.step(&interp.gas); } fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + interp.bytecode.pc(); + interp.bytecode.opcode(); self.gas_inspector.step_end(&mut interp.gas); self.gas_remaining_steps .push((self.pc, self.gas_inspector.gas_remaining())); @@ -145,12 +149,14 @@ mod tests { let mut evm = ctx.build_mainnet_with_inspector(StackInspector::default()); // Run evm. - evm.inspect_one_tx(TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - gas_limit: 21100, - ..Default::default() - }) + evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(21100) + .build() + .unwrap(), + ) .unwrap(); let inspector = &evm.inspector; @@ -246,21 +252,12 @@ mod tests { let bytecode = Bytecode::new_raw(contract_data); - let ctx = Context::mainnet() + let mut evm = Context::mainnet() .with_db(BenchmarkDB::new_bytecode(bytecode.clone())) - .modify_tx_chained(|tx| { - tx.caller = BENCH_CALLER; - tx.kind = TxKind::Call(BENCH_TARGET); - }); - - let mut evm = ctx.build_mainnet_with_inspector(inspector); + .build_mainnet_with_inspector(inspector); let _ = evm - .inspect_one_tx(TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - ..Default::default() - }) + .inspect_one_tx(TxEnv::builder_for_bench().build().unwrap()) .unwrap(); assert_eq!(evm.inspector.return_buffer.len(), 3); assert_eq!( diff --git a/crates/inspector/src/handler.rs b/crates/inspector/src/handler.rs index de5f706802..ba2b8570d5 100644 --- a/crates/inspector/src/handler.rs +++ b/crates/inspector/src/handler.rs @@ -215,11 +215,18 @@ where log_num = new_log; } + // if loops is ending, break the loop so we can revert to the previous pointer and then call step_end. + if interpreter.bytecode.is_end() { + break; + } + // Call step_end. inspector.step_end(interpreter, context); } interpreter.bytecode.revert_to_previous_pointer(); + // call step_end again to handle the last instruction + inspector.step_end(interpreter, context); let next_action = interpreter.take_next_action(); diff --git a/crates/inspector/src/inspect.rs b/crates/inspector/src/inspect.rs index 6fb2ecd2fd..5ac1ada4a6 100644 --- a/crates/inspector/src/inspect.rs +++ b/crates/inspector/src/inspect.rs @@ -56,7 +56,7 @@ pub trait InspectEvm: ExecuteEvm { /// /// Functions return CommitOutput from [`ExecuteCommitEvm`] trait. pub trait InspectCommitEvm: InspectEvm + ExecuteCommitEvm { - /// Inspect the EVM with the current inspector and previous transaction by replaying,similar to [`InspectEvm::inspect_tx`] + /// Inspect the EVM with the current inspector and previous transaction by replaying, similar to [`InspectEvm::inspect_tx`] /// and commit the state diff to the database. fn inspect_tx_commit(&mut self, tx: Self::Tx) -> Result { let output = self.inspect_one_tx(tx)?; diff --git a/crates/inspector/src/inspector_tests.rs b/crates/inspector/src/inspector_tests.rs new file mode 100644 index 0000000000..29cfa54c94 --- /dev/null +++ b/crates/inspector/src/inspector_tests.rs @@ -0,0 +1,734 @@ +#[cfg(test)] +mod tests { + use crate::{InspectEvm, Inspector}; + use context::{Context, TxEnv}; + use database::{BenchmarkDB, BENCH_CALLER, BENCH_TARGET}; + use handler::{MainBuilder, MainContext}; + use interpreter::{ + interpreter_types::{Jumps, MemoryTr, StackTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, + }; + use primitives::{address, Address, Bytes, Log, TxKind, U256}; + use state::{bytecode::opcode, AccountInfo, Bytecode}; + + #[derive(Debug, Clone)] + struct InterpreterState { + pc: usize, + stack_len: usize, + memory_size: usize, + } + + #[derive(Debug, Clone)] + struct StepRecord { + before: InterpreterState, + after: Option, + opcode_name: String, + } + + #[derive(Debug, Clone)] + enum InspectorEvent { + Step(StepRecord), + Call { + inputs: CallInputs, + outcome: Option, + }, + Create { + inputs: CreateInputs, + outcome: Option, + }, + Log(Log), + Selfdestruct { + address: Address, + beneficiary: Address, + value: U256, + }, + } + + #[derive(Debug, Default)] + struct TestInspector { + events: Vec, + step_count: usize, + call_depth: usize, + } + + impl TestInspector { + fn new() -> Self { + Self { + events: Vec::new(), + step_count: 0, + call_depth: 0, + } + } + + fn capture_interpreter_state( + interp: &Interpreter, + ) -> InterpreterState + where + INTR::Bytecode: Jumps, + INTR::Stack: StackTr, + INTR::Memory: MemoryTr, + { + InterpreterState { + pc: interp.bytecode.pc(), + stack_len: interp.stack.len(), + memory_size: interp.memory.size(), + } + } + + fn get_events(&self) -> Vec { + self.events.clone() + } + + fn get_step_count(&self) -> usize { + self.step_count + } + } + + impl Inspector for TestInspector + where + INTR: InterpreterTypes, + INTR::Bytecode: Jumps, + INTR::Stack: StackTr, + INTR::Memory: MemoryTr, + { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.step_count += 1; + + let state = Self::capture_interpreter_state(interp); + let opcode = interp.bytecode.opcode(); + let opcode_name = if let Some(op) = state::bytecode::opcode::OpCode::new(opcode) { + format!("{op}") + } else { + format!("Unknown(0x{opcode:02x})") + }; + + self.events.push(InspectorEvent::Step(StepRecord { + before: state, + after: None, + opcode_name, + })); + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + let state = Self::capture_interpreter_state(interp); + + if let Some(InspectorEvent::Step(record)) = self.events.last_mut() { + record.after = Some(state); + } + } + + fn log(&mut self, _interp: &mut Interpreter, _ctx: &mut CTX, log: Log) { + self.events.push(InspectorEvent::Log(log)); + } + + fn call(&mut self, _ctx: &mut CTX, inputs: &mut CallInputs) -> Option { + self.call_depth += 1; + self.events.push(InspectorEvent::Call { + inputs: inputs.clone(), + outcome: None, + }); + None + } + + fn call_end(&mut self, _ctx: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { + self.call_depth -= 1; + if let Some(InspectorEvent::Call { + outcome: ref mut out, + .. + }) = self + .events + .iter_mut() + .rev() + .find(|e| matches!(e, InspectorEvent::Call { outcome: None, .. })) + { + *out = Some(outcome.clone()); + } + } + + fn create(&mut self, _ctx: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.events.push(InspectorEvent::Create { + inputs: inputs.clone(), + outcome: None, + }); + None + } + + fn create_end( + &mut self, + _ctx: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if let Some(InspectorEvent::Create { + outcome: ref mut out, + .. + }) = self + .events + .iter_mut() + .rev() + .find(|e| matches!(e, InspectorEvent::Create { outcome: None, .. })) + { + *out = Some(outcome.clone()); + } + } + + fn selfdestruct(&mut self, contract: Address, beneficiary: Address, value: U256) { + self.events.push(InspectorEvent::Selfdestruct { + address: contract, + beneficiary, + value, + }); + } + } + + #[test] + fn test_push_opcodes_and_stack_operations() { + // PUSH1 0x42, PUSH2 0x1234, ADD, PUSH1 0x00, MSTORE, STOP + let code = Bytes::from(vec![ + opcode::PUSH1, + 0x42, + opcode::PUSH2, + 0x12, + 0x34, + opcode::ADD, + opcode::PUSH1, + 0x00, + opcode::MSTORE, + opcode::STOP, + ]); + + let bytecode = Bytecode::new_raw(code); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + let step_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Step(record) = e { + Some(record) + } else { + None + } + }) + .collect(); + + // Verify PUSH1 0x42 + let push1_event = &step_events[0]; + assert_eq!(push1_event.opcode_name, "PUSH1"); + assert_eq!(push1_event.before.stack_len, 0); + assert_eq!(push1_event.after.as_ref().unwrap().stack_len, 1); + + // Verify PUSH2 0x1234 + let push2_event = &step_events[1]; + assert_eq!(push2_event.opcode_name, "PUSH2"); + assert_eq!(push2_event.before.stack_len, 1); + assert_eq!(push2_event.after.as_ref().unwrap().stack_len, 2); + + // Verify ADD + let add_event = &step_events[2]; + assert_eq!(add_event.opcode_name, "ADD"); + assert_eq!(add_event.before.stack_len, 2); + assert_eq!(add_event.after.as_ref().unwrap().stack_len, 1); + + // Verify all opcodes were tracked + assert!(inspector.get_step_count() >= 5); // PUSH1, PUSH2, ADD, PUSH1, MSTORE, STOP + } + + #[test] + fn test_jump_and_jumpi_control_flow() { + // PUSH1 0x08, JUMP, INVALID, JUMPDEST, PUSH1 0x01, PUSH1 0x0F, JUMPI, INVALID, JUMPDEST, STOP + let code = Bytes::from(vec![ + opcode::PUSH1, + 0x08, + opcode::JUMP, + opcode::INVALID, + opcode::INVALID, + opcode::INVALID, + opcode::INVALID, + opcode::INVALID, + opcode::JUMPDEST, // offset 0x08 + opcode::PUSH1, + 0x01, + opcode::PUSH1, + 0x0F, + opcode::JUMPI, + opcode::INVALID, + opcode::JUMPDEST, // offset 0x0F + opcode::STOP, + ]); + + let bytecode = Bytecode::new_raw(code); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + let step_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Step(record) = e { + Some(record) + } else { + None + } + }) + .collect(); + + // Find JUMP instruction + let jump_event = step_events + .iter() + .find(|e| e.opcode_name == "JUMP") + .unwrap(); + assert_eq!(jump_event.before.pc, 2); // After PUSH1 0x08 + assert_eq!(jump_event.after.as_ref().unwrap().pc, 8); // Jumped to JUMPDEST + + // Find JUMPI instruction + let jumpi_event = step_events + .iter() + .find(|e| e.opcode_name == "JUMPI") + .unwrap(); + assert!(jumpi_event.before.stack_len >= 2); // Has condition and destination + // JUMPI should have jumped since condition is 1 (true) + assert_eq!(jumpi_event.after.as_ref().unwrap().pc, 0x0F); + } + + #[test] + fn test_call_operations() { + // For CALL tests, we need a more complex setup with multiple contracts + // Deploy a simple contract that returns a value + let callee_code = Bytes::from(vec![ + opcode::PUSH1, + 0x42, // Push return value + opcode::PUSH1, + 0x00, // Push memory offset + opcode::MSTORE, + opcode::PUSH1, + 0x20, // Push return size + opcode::PUSH1, + 0x00, // Push return offset + opcode::RETURN, + ]); + + // Caller contract that calls the callee + let caller_code = Bytes::from(vec![ + // Setup CALL parameters + opcode::PUSH1, + 0x20, // retSize + opcode::PUSH1, + 0x00, // retOffset + opcode::PUSH1, + 0x00, // argsSize + opcode::PUSH1, + 0x00, // argsOffset + opcode::PUSH1, + 0x00, // value + opcode::PUSH20, + // address: 20 bytes to match callee_address exactly + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + opcode::PUSH2, + 0xFF, + 0xFF, // gas + opcode::CALL, + opcode::STOP, + ]); + + // Create a custom database with two contracts + let mut db = database::InMemoryDB::default(); + + // Add caller contract at BENCH_TARGET + db.insert_account_info( + BENCH_TARGET, + AccountInfo { + balance: U256::from(1_000_000_000_000_000_000u64), + nonce: 0, + code_hash: primitives::keccak256(&caller_code), + code: Some(Bytecode::new_raw(caller_code)), + }, + ); + + // Add callee contract at a specific address + let callee_address = Address::new([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]); + db.insert_account_info( + callee_address, + AccountInfo { + balance: U256::ZERO, + nonce: 0, + code_hash: primitives::keccak256(&callee_code), + code: Some(Bytecode::new_raw(callee_code)), + }, + ); + + let ctx = Context::mainnet().with_db(db); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + + // Find CALL events + let call_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Call { inputs, outcome } = e { + Some((inputs, outcome)) + } else { + None + } + }) + .collect(); + + assert!(!call_events.is_empty(), "Should have recorded CALL events"); + let (call_inputs, call_outcome) = &call_events[0]; + // The test setup might be using BENCH_CALLER as the default target + // Just verify that a call was made and completed successfully + assert_eq!(call_inputs.target_address, BENCH_TARGET); + assert!(call_outcome.is_some(), "Call should have completed"); + } + + #[test] + fn test_create_opcodes() { + // CREATE test: deploy a contract that creates another contract + let init_code = vec![ + opcode::PUSH1, + 0x42, // Push constructor value + opcode::PUSH1, + 0x00, // Push memory offset + opcode::MSTORE, + opcode::PUSH1, + 0x20, // Push return size + opcode::PUSH1, + 0x00, // Push return offset + opcode::RETURN, + ]; + + let create_code = vec![ + // First, store init code in memory using CODECOPY + opcode::PUSH1, + init_code.len() as u8, // size + opcode::PUSH1, + 0x20, // code offset (after CREATE params) + opcode::PUSH1, + 0x00, // memory offset + opcode::CODECOPY, + // CREATE parameters + opcode::PUSH1, + init_code.len() as u8, // size + opcode::PUSH1, + 0x00, // offset + opcode::PUSH1, + 0x00, // value + opcode::CREATE, + opcode::STOP, + ]; + + let mut full_code = create_code; + full_code.extend_from_slice(&init_code); + + let bytecode = Bytecode::new_raw(Bytes::from(full_code)); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + + // Find CREATE events + let create_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Create { inputs, outcome } = e { + Some((inputs, outcome)) + } else { + None + } + }) + .collect(); + + assert!( + !create_events.is_empty(), + "Should have recorded CREATE events" + ); + let (_create_inputs, create_outcome) = &create_events[0]; + assert!(create_outcome.is_some(), "CREATE should have completed"); + } + + #[test] + fn test_log_operations() { + // Simple LOG0 test - no topics + let code = vec![ + // Store some data in memory for the log + opcode::PUSH1, + 0x42, + opcode::PUSH1, + 0x00, + opcode::MSTORE, + // LOG0 parameters + opcode::PUSH1, + 0x20, // size + opcode::PUSH1, + 0x00, // offset + opcode::LOG0, + opcode::STOP, + ]; + + let bytecode = Bytecode::new_raw(Bytes::from(code)); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + + // Find LOG events + let log_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Log(log) = e { + Some(log) + } else { + None + } + }) + .collect(); + + // Remove debug code - test should work now + + assert_eq!(log_events.len(), 1, "Should have recorded one LOG event"); + let log = &log_events[0]; + assert_eq!(log.topics().len(), 0, "LOG0 should have 0 topics"); + } + + #[test] + fn test_selfdestruct() { + // SELFDESTRUCT test + let beneficiary = address!("3000000000000000000000000000000000000000"); + let mut code = vec![opcode::PUSH20]; + code.extend_from_slice(beneficiary.as_ref()); + code.push(opcode::SELFDESTRUCT); + + let bytecode = Bytecode::new_raw(Bytes::from(code)); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + + // Find SELFDESTRUCT events + let selfdestruct_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Selfdestruct { + address, + beneficiary, + value, + } = e + { + Some((address, beneficiary, value)) + } else { + None + } + }) + .collect(); + + assert_eq!( + selfdestruct_events.len(), + 1, + "Should have recorded SELFDESTRUCT event" + ); + let (_address, event_beneficiary, _value) = selfdestruct_events[0]; + assert_eq!(*event_beneficiary, beneficiary); + } + + #[test] + fn test_comprehensive_inspector_integration() { + // Complex contract with multiple operations: + // 1. PUSH and arithmetic + // 2. Memory operations + // 3. Conditional jump + // 4. LOG0 + + let code = vec![ + // Stack operations + opcode::PUSH1, + 0x10, + opcode::PUSH1, + 0x20, + opcode::ADD, + opcode::DUP1, + opcode::PUSH1, + 0x00, + opcode::MSTORE, + // Conditional jump + opcode::PUSH1, + 0x01, + opcode::PUSH1, + 0x00, + opcode::MLOAD, + opcode::GT, + opcode::PUSH1, + 0x17, // Jump destination (adjusted) + opcode::JUMPI, + // This should be skipped + opcode::PUSH1, + 0x00, + opcode::PUSH1, + 0x00, + opcode::REVERT, + // Jump destination + opcode::JUMPDEST, // offset 0x14 + // LOG0 + opcode::PUSH1, + 0x20, + opcode::PUSH1, + 0x00, + opcode::LOG0, + opcode::STOP, + ]; + + let bytecode = Bytecode::new_raw(Bytes::from(code)); + let ctx = Context::mainnet().with_db(BenchmarkDB::new_bytecode(bytecode)); + let mut evm = ctx.build_mainnet_with_inspector(TestInspector::new()); + + // Run transaction + let _ = evm.inspect_one_tx( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)) + .gas_limit(100_000) + .build() + .unwrap(), + ); + + let inspector = &evm.inspector; + let events = inspector.get_events(); + + // Verify we captured various event types + let step_count = events + .iter() + .filter(|e| matches!(e, InspectorEvent::Step(_))) + .count(); + let log_count = events + .iter() + .filter(|e| matches!(e, InspectorEvent::Log(_))) + .count(); + + assert!(step_count > 10, "Should have multiple step events"); + assert_eq!(log_count, 1, "Should have one log event"); + + // Verify stack operations were tracked + let step_events: Vec<_> = events + .iter() + .filter_map(|e| { + if let InspectorEvent::Step(record) = e { + Some(record) + } else { + None + } + }) + .collect(); + + // Find ADD operation + let add_event = step_events.iter().find(|e| e.opcode_name == "ADD").unwrap(); + assert_eq!(add_event.before.stack_len, 2); + assert_eq!(add_event.after.as_ref().unwrap().stack_len, 1); + + // Verify memory was written + let mstore_event = step_events + .iter() + .find(|e| e.opcode_name == "MSTORE") + .unwrap(); + assert!(mstore_event.after.as_ref().unwrap().memory_size > 0); + + // Verify conditional jump worked correctly + let jumpi_event = step_events + .iter() + .find(|e| e.opcode_name == "JUMPI") + .unwrap(); + assert_eq!( + jumpi_event.after.as_ref().unwrap().pc, + 0x17, + "Should have jumped to JUMPDEST" + ); + } +} diff --git a/crates/inspector/src/lib.rs b/crates/inspector/src/lib.rs index 9673e24124..8740614ec1 100644 --- a/crates/inspector/src/lib.rs +++ b/crates/inspector/src/lib.rs @@ -17,6 +17,9 @@ mod mainnet_inspect; mod noop; mod traits; +#[cfg(test)] +mod inspector_tests; + /// Inspector implementations. pub mod inspectors { #[cfg(all(feature = "std", feature = "serde-json"))] diff --git a/crates/inspector/src/traits.rs b/crates/inspector/src/traits.rs index c78f3f6134..9fc098f377 100644 --- a/crates/inspector/src/traits.rs +++ b/crates/inspector/src/traits.rs @@ -62,8 +62,12 @@ pub trait InspectorEvmTr: frame_end(ctx, inspector, &frame_init.frame_input, &mut output); return Ok(ItemOrResult::Result(output)); } - if let ItemOrResult::Result(frame) = self.frame_init(frame_init)? { - return Ok(ItemOrResult::Result(frame)); + + let frame_input = frame_init.frame_input.clone(); + if let ItemOrResult::Result(mut output) = self.frame_init(frame_init)? { + let (ctx, inspector) = self.ctx_inspector(); + frame_end(ctx, inspector, &frame_input, &mut output); + return Ok(ItemOrResult::Result(output)); } // if it is new frame, initialize the interpreter. diff --git a/crates/interpreter/CHANGELOG.md b/crates/interpreter/CHANGELOG.md index 1ae14b2e1b..bc081b6d76 100644 --- a/crates/interpreter/CHANGELOG.md +++ b/crates/interpreter/CHANGELOG.md @@ -6,6 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [23.0.2](https://github.com/bluealloy/revm/compare/revm-interpreter-v23.0.1...revm-interpreter-v23.0.2) - 2025-07-14 + +### Other + +- simplify gas calculations by introducing a used() method ([#2703](https://github.com/bluealloy/revm/pull/2703)) + +## [23.0.1](https://github.com/bluealloy/revm/compare/revm-interpreter-v23.0.0...revm-interpreter-v23.0.1) - 2025-07-03 + +### Other + +- updated the following local packages: revm-bytecode, revm-context-interface + +## [22.1.0](https://github.com/bluealloy/revm/compare/revm-interpreter-v22.0.1...revm-interpreter-v22.1.0) - 2025-06-30 + +### Added + +- blake2 avx2 ([#2670](https://github.com/bluealloy/revm/pull/2670)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) + ## [22.0.1](https://github.com/bluealloy/revm/compare/revm-interpreter-v22.0.0...revm-interpreter-v22.0.1) - 2025-06-20 ### Other @@ -166,7 +188,7 @@ Stable version - fix wrong comment & remove useless struct ([#2105](https://github.com/bluealloy/revm/pull/2105)) - move all dependencies to workspace ([#2092](https://github.com/bluealloy/revm/pull/2092)) -## [16.0.0](https://github.com/bluealloy/revm/compare/revm-interpreter-v15.2.0...revm-interpreter-v16.0.0-alpha.1) - 2025-02-16 +## [16.0.0-alpha.1](https://github.com/bluealloy/revm/compare/revm-interpreter-v15.2.0...revm-interpreter-v16.0.0-alpha.1) - 2025-02-16 ### Added diff --git a/crates/interpreter/Cargo.toml b/crates/interpreter/Cargo.toml index afce8f25cf..64b7776d40 100644 --- a/crates/interpreter/Cargo.toml +++ b/crates/interpreter/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-interpreter" description = "Revm Interpreter that executes bytecode." -version = "22.0.1" +version = "23.0.2" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/interpreter/src/gas.rs b/crates/interpreter/src/gas.rs index d069db0cf9..8cc52c2026 100644 --- a/crates/interpreter/src/gas.rs +++ b/crates/interpreter/src/gas.rs @@ -73,6 +73,12 @@ impl Gas { self.limit - self.remaining } + /// Returns the final amount of gas used by subtracting the refund from spent gas. + #[inline] + pub const fn used(&self) -> u64 { + self.spent().saturating_sub(self.refunded() as u64) + } + /// Returns the total amount of gas spent, minus the refunded gas. #[inline] pub const fn spent_sub_refunded(&self) -> u64 { diff --git a/crates/interpreter/src/instructions.rs b/crates/interpreter/src/instructions.rs index 70c63c267a..493cba5777 100644 --- a/crates/interpreter/src/instructions.rs +++ b/crates/interpreter/src/instructions.rs @@ -224,7 +224,7 @@ mod tests { let is_instr_unknown = std::ptr::fn_addr_eq(*instr, unknown_istr); assert_eq!( is_instr_unknown, is_opcode_unknown, - "Opcode 0x{i:X?} is not handled" + "Opcode 0x{i:X?} is not handled", ); } } diff --git a/crates/op-revm/CHANGELOG.md b/crates/op-revm/CHANGELOG.md index cde06557fc..4d85723ecc 100644 --- a/crates/op-revm/CHANGELOG.md +++ b/crates/op-revm/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.3](https://github.com/bluealloy/revm/compare/op-revm-v8.0.2...op-revm-v8.0.3) - 2025-07-14 + +### Other + +- simplify gas calculations by introducing a used() method ([#2703](https://github.com/bluealloy/revm/pull/2703)) + +## [8.0.2](https://github.com/bluealloy/revm/compare/op-revm-v8.0.1...op-revm-v8.0.2) - 2025-07-03 + +### Other + +- updated the following local packages: revm + +## [8.0.1](https://github.com/bluealloy/revm/compare/op-revm-v7.0.1...op-revm-v8.0.1) - 2025-06-30 + +### Added + +- optional_eip3541 ([#2661](https://github.com/bluealloy/revm/pull/2661)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- *(op/handler)* verify caller account is touched by zero value transfer ([#2669](https://github.com/bluealloy/revm/pull/2669)) +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) + ## [7.0.1](https://github.com/bluealloy/revm/compare/op-revm-v7.0.0...op-revm-v7.0.1) - 2025-06-20 ### Fixed diff --git a/crates/op-revm/Cargo.toml b/crates/op-revm/Cargo.toml index a0aa9949fc..cbb058e8ed 100644 --- a/crates/op-revm/Cargo.toml +++ b/crates/op-revm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "op-revm" description = "Optimism variant of Revm" -version = "7.0.1" +version = "8.0.3" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -54,12 +54,14 @@ dev = [ "memory_limit", "optional_balance_check", "optional_block_gas_limit", + "optional_eip3541", "optional_eip3607", "optional_no_base_fee", ] memory_limit = ["revm/memory_limit"] optional_balance_check = ["revm/optional_balance_check"] optional_block_gas_limit = ["revm/optional_block_gas_limit"] +optional_eip3541 = ["revm/optional_eip3541"] optional_eip3607 = ["revm/optional_eip3607"] optional_no_base_fee = ["revm/optional_no_base_fee"] diff --git a/crates/op-revm/src/api/default_ctx.rs b/crates/op-revm/src/api/default_ctx.rs index 5506deed8b..1b21507379 100644 --- a/crates/op-revm/src/api/default_ctx.rs +++ b/crates/op-revm/src/api/default_ctx.rs @@ -19,7 +19,7 @@ pub trait DefaultOp { impl DefaultOp for OpContext { fn op() -> Self { Context::mainnet() - .with_tx(OpTransaction::default()) + .with_tx(OpTransaction::builder().build_fill()) .with_cfg(CfgEnv::new_with_spec(OpSpecId::BEDROCK)) .with_chain(L1BlockInfo::default()) } @@ -40,8 +40,8 @@ mod test { // convert to optimism context let mut evm = ctx.build_op_with_inspector(NoOpInspector {}); // execute - let _ = evm.transact(OpTransaction::default()); + let _ = evm.transact(OpTransaction::builder().build_fill()); // inspect - let _ = evm.inspect_one_tx(OpTransaction::default()); + let _ = evm.inspect_one_tx(OpTransaction::builder().build_fill()); } } diff --git a/crates/op-revm/src/fast_lz.rs b/crates/op-revm/src/fast_lz.rs index bb8ccc17a3..a1a262a079 100644 --- a/crates/op-revm/src/fast_lz.rs +++ b/crates/op-revm/src/fast_lz.rs @@ -159,6 +159,8 @@ mod tests { // This bytecode and ABI is for a contract, which wraps the LibZip library for easier fuzz testing. // The source of this contract is here: https://github.com/danyalprout/fastlz/blob/main/src/FastLz.sol#L6-L10 + use revm::context::TxEnv; + use crate::OpTransaction; sol! { interface FastLz { @@ -174,13 +176,16 @@ mod tests { .with_db(BenchmarkDB::new_bytecode(contract_bytecode.clone())) .build_op(); - let mut tx = OpTransaction::default(); - - tx.base.caller = EEADDRESS; - tx.base.kind = TxKind::Call(FFADDRESS); - tx.base.data = FastLz::fastLzCall::new((input,)).abi_encode().into(); - tx.base.gas_limit = 3_000_000; - tx.enveloped_tx = Some(Bytes::default()); + let tx = OpTransaction::builder() + .base( + TxEnv::builder() + .caller(EEADDRESS) + .kind(TxKind::Call(FFADDRESS)) + .data(FastLz::fastLzCall::new((input,)).abi_encode().into()) + .gas_limit(3_000_000), + ) + .enveloped_tx(Some(Bytes::default())) + .build_fill(); let result = evm.transact_one(tx).unwrap(); diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index 688c84d2da..69a471f98b 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -353,27 +353,21 @@ where }; let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, spec); - let mut operator_fee_cost = U256::ZERO; - if spec.is_enabled_in(OpSpecId::ISTHMUS) { - operator_fee_cost = l1_block_info.operator_fee_charge( - enveloped_tx, - U256::from(frame_result.gas().spent() - frame_result.gas().refunded() as u64), - ); + let operator_fee_cost = if spec.is_enabled_in(OpSpecId::ISTHMUS) { + l1_block_info.operator_fee_charge(enveloped_tx, U256::from(frame_result.gas().used())) + } else { + U256::ZERO + }; + let base_fee_amount = U256::from(basefee.saturating_mul(frame_result.gas().used() as u128)); + + // Send fees to their respective recipients + for (recipient, amount) in [ + (L1_FEE_RECIPIENT, l1_cost), + (BASE_FEE_RECIPIENT, base_fee_amount), + (OPERATOR_FEE_RECIPIENT, operator_fee_cost), + ] { + ctx.journal_mut().balance_incr(recipient, amount)?; } - // Send the L1 cost of the transaction to the L1 Fee Vault. - ctx.journal_mut().balance_incr(L1_FEE_RECIPIENT, l1_cost)?; - - // Send the base fee of the transaction to the Base Fee Vault. - ctx.journal_mut().balance_incr( - BASE_FEE_RECIPIENT, - U256::from(basefee.saturating_mul( - (frame_result.gas().spent() - frame_result.gas().refunded() as u64) as u128, - )), - )?; - - // Send the operator fee of the transaction to the coinbase. - ctx.journal_mut() - .balance_incr(OPERATOR_FEE_RECIPIENT, operator_fee_cost)?; Ok(()) } @@ -501,11 +495,11 @@ mod tests { BASE_FEE_SCALAR_OFFSET, ECOTONE_L1_BLOB_BASE_FEE_SLOT, ECOTONE_L1_FEE_SCALARS_SLOT, L1_BASE_FEE_SLOT, L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT, }, - DefaultOp, OpBuilder, + DefaultOp, OpBuilder, OpTransaction, }; use alloy_primitives::uint; use revm::{ - context::{BlockEnv, Context, TransactionType}, + context::{BlockEnv, Context, TxEnv}, context_interface::result::InvalidTransaction, database::InMemoryDB, database_interface::EmptyDB, @@ -547,10 +541,11 @@ mod tests { #[test] fn test_revert_gas() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.gas_limit = 100; - tx.enveloped_tx = None; - }) + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK); let gas = call_last_frame_return(ctx, InstructionResult::Revert, Gas::new(90)); @@ -562,11 +557,11 @@ mod tests { #[test] fn test_consume_gas() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.gas_limit = 100; - tx.deposit.source_hash = B256::ZERO; - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - }) + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90)); @@ -578,11 +573,12 @@ mod tests { #[test] fn test_consume_gas_with_refund() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.gas_limit = 100; - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.deposit.source_hash = B256::ZERO; - }) + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .source_hash(B256::from([1u8; 32])) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); let mut ret_gas = Gas::new(90); @@ -602,11 +598,12 @@ mod tests { #[test] fn test_consume_gas_deposit_tx() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.base.gas_limit = 100; - tx.deposit.source_hash = B256::ZERO; - }) + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .source_hash(B256::from([1u8; 32])) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK); let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90)); assert_eq!(gas.remaining(), 0); @@ -617,12 +614,13 @@ mod tests { #[test] fn test_consume_gas_sys_deposit_tx() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.base.gas_limit = 100; - tx.deposit.source_hash = B256::ZERO; - tx.deposit.is_system_transaction = true; - }) + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .source_hash(B256::from([1u8; 32])) + .is_system_transaction() + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::BEDROCK); let gas = call_last_frame_return(ctx, InstructionResult::Stop, Gas::new(90)); assert_eq!(gas.remaining(), 100); @@ -652,8 +650,7 @@ mod tests { }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); ctx.modify_tx(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.deposit.source_hash = B256::ZERO; + tx.deposit.source_hash = B256::from([1u8; 32]); tx.deposit.mint = Some(10); }); @@ -677,7 +674,7 @@ mod tests { db.insert_account_info( caller, AccountInfo { - balance: U256::from(1000), + balance: U256::from(1058), // Increased to cover L1 fees (1048) + base fees ..Default::default() }, ); @@ -690,13 +687,14 @@ mod tests { ..Default::default() }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH) - .modify_tx_chained(|tx| { - tx.base.gas_limit = 100; - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.deposit.mint = Some(10); - tx.enveloped_tx = Some(bytes!("FACADE")); - tx.deposit.source_hash = B256::ZERO; - }); + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .enveloped_tx(Some(bytes!("FACADE"))) + .source_hash(B256::ZERO) + .build() + .unwrap(), + ); let mut evm = ctx.build_op(); @@ -708,7 +706,7 @@ mod tests { // Check the account balance is updated. let account = evm.ctx().journal_mut().load_account(caller).unwrap(); - assert_eq!(account.info.balance, U256::from(1010)); + assert_eq!(account.info.balance, U256::from(10)); // 1058 - 1048 = 10 } #[test] @@ -810,11 +808,14 @@ mod tests { ..Default::default() }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH) - .modify_tx_chained(|tx| { - tx.base.gas_limit = 100; - tx.deposit.source_hash = B256::ZERO; - tx.enveloped_tx = Some(bytes!("FACADE")); - }); + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(100)) + .source_hash(B256::ZERO) + .enveloped_tx(Some(bytes!("FACADE"))) + .build() + .unwrap(), + ); let mut evm = ctx.build_op(); let handler = @@ -849,10 +850,12 @@ mod tests { ..Default::default() }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS) - .modify_tx_chained(|tx| { - tx.base.gas_limit = 10; - tx.enveloped_tx = Some(bytes!("FACADE")); - }); + .with_tx( + OpTransaction::builder() + .base(TxEnv::builder().gas_limit(10)) + .enveloped_tx(Some(bytes!("FACADE"))) + .build_fill(), + ); let mut evm = ctx.build_op(); let handler = @@ -916,7 +919,7 @@ mod tests { // mark the tx as a system transaction. let ctx = Context::op() .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; + tx.deposit.source_hash = B256::from([1u8; 32]); tx.deposit.is_system_transaction = true; }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); @@ -943,8 +946,7 @@ mod tests { // Set source hash. let ctx = Context::op() .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.deposit.source_hash = B256::ZERO; + tx.deposit.source_hash = B256::from([1u8; 32]); }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); @@ -960,8 +962,7 @@ mod tests { // Set source hash. let ctx = Context::op() .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.deposit.source_hash = B256::ZERO; + tx.deposit.source_hash = B256::from([1u8; 32]); }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); @@ -977,7 +978,8 @@ mod tests { fn test_halted_deposit_tx_post_regolith() { let ctx = Context::op() .modify_tx_chained(|tx| { - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; + // Set up as deposit transaction by having a deposit with source_hash + tx.deposit.source_hash = B256::from([1u8; 32]); }) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::REGOLITH); @@ -1003,6 +1005,36 @@ mod tests { ) } + #[test] + fn test_tx_zero_value_touch_caller() { + let ctx = Context::op(); + + let mut evm = ctx.build_op(); + + assert!(!evm + .0 + .ctx + .journal_mut() + .load_account(Address::ZERO) + .unwrap() + .is_touched()); + + let handler = + OpHandler::<_, EVMError<_, OpTransactionError>, EthFrame>::new(); + + handler + .validate_against_state_and_deduct_caller(&mut evm) + .unwrap(); + + assert!(evm + .0 + .ctx + .journal_mut() + .load_account(Address::ZERO) + .unwrap() + .is_touched()); + } + #[rstest] #[case::deposit(true)] #[case::dyn_fee(false)] @@ -1012,16 +1044,26 @@ mod tests { const OP_FEE_MOCK_PARAM: u128 = 0xFFFF; let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.tx_type = if is_deposit { - DEPOSIT_TRANSACTION_TYPE - } else { - TransactionType::Eip1559 as u8 - }; - tx.base.gas_price = GAS_PRICE; - tx.base.gas_priority_fee = None; - tx.base.caller = SENDER; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .gas_price(GAS_PRICE) + .gas_priority_fee(None) + .caller(SENDER), + ) + .enveloped_tx(if is_deposit { + None + } else { + Some(bytes!("FACADE")) + }) + .source_hash(if is_deposit { + B256::from([1u8; 32]) + } else { + B256::ZERO + }) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS); let mut evm = ctx.build_op(); diff --git a/crates/op-revm/src/transaction/abstraction.rs b/crates/op-revm/src/transaction/abstraction.rs index 705d6491a7..65b50e3ddd 100644 --- a/crates/op-revm/src/transaction/abstraction.rs +++ b/crates/op-revm/src/transaction/abstraction.rs @@ -2,7 +2,10 @@ use super::deposit::{DepositTransactionParts, DEPOSIT_TRANSACTION_TYPE}; use auto_impl::auto_impl; use revm::{ - context::TxEnv, + context::{ + tx::{TxEnvBuildError, TxEnvBuilder}, + TxEnv, + }, context_interface::transaction::Transaction, handler::SystemCallTx, primitives::{Address, Bytes, TxKind, B256, U256}, @@ -46,6 +49,12 @@ pub struct OpTransaction { pub deposit: DepositTransactionParts, } +impl AsRef for OpTransaction { + fn as_ref(&self) -> &T { + &self.base + } +} + impl OpTransaction { /// Create a new Optimism transaction. pub fn new(base: T) -> Self { @@ -57,6 +66,13 @@ impl OpTransaction { } } +impl OpTransaction { + /// Create a new Optimism transaction. + pub fn builder() -> OpTransactionBuilder { + OpTransactionBuilder::new() + } +} + impl Default for OpTransaction { fn default() -> Self { Self { @@ -92,7 +108,12 @@ impl Transaction for OpTransaction { T: 'a; fn tx_type(&self) -> u8 { - self.base.tx_type() + // If this is a deposit transaction (has source_hash set), return deposit type + if self.deposit.source_hash != B256::ZERO { + DEPOSIT_TRANSACTION_TYPE + } else { + self.base.tx_type() + } } fn caller(&self) -> Address { @@ -148,6 +169,10 @@ impl Transaction for OpTransaction { } fn effective_gas_price(&self, base_fee: u128) -> u128 { + // Deposit transactions use gas_price directly + if self.tx_type() == DEPOSIT_TRANSACTION_TYPE { + return self.gas_price(); + } self.base.effective_gas_price(base_fee) } @@ -181,37 +206,181 @@ impl OpTxTr for OpTransaction { } } +/// Builder for constructing [`OpTransaction`] instances +#[derive(Default, Debug)] +pub struct OpTransactionBuilder { + base: TxEnvBuilder, + enveloped_tx: Option, + deposit: DepositTransactionParts, +} + +impl OpTransactionBuilder { + /// Create a new builder with default values + pub fn new() -> Self { + Self { + base: TxEnvBuilder::new(), + enveloped_tx: None, + deposit: DepositTransactionParts::default(), + } + } + + /// Set the base transaction builder based for TxEnvBuilder. + pub fn base(mut self, base: TxEnvBuilder) -> Self { + self.base = base; + self + } + + /// Set the enveloped transaction bytes. + pub fn enveloped_tx(mut self, enveloped_tx: Option) -> Self { + self.enveloped_tx = enveloped_tx; + self + } + + /// Set the source hash of the deposit transaction. + pub fn source_hash(mut self, source_hash: B256) -> Self { + self.deposit.source_hash = source_hash; + self + } + + /// Set the mint of the deposit transaction. + pub fn mint(mut self, mint: u128) -> Self { + self.deposit.mint = Some(mint); + self + } + + /// Set the deposit transaction to be a system transaction. + pub fn is_system_transaction(mut self) -> Self { + self.deposit.is_system_transaction = true; + self + } + + /// Set the deposit transaction to not be a system transaction. + pub fn not_system_transaction(mut self) -> Self { + self.deposit.is_system_transaction = false; + self + } + + /// Set the deposit transaction to be a deposit transaction. + pub fn is_deposit_tx(mut self) -> Self { + self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE)); + self + } + + /// Build the [`OpTransaction`] with default values for missing fields. + /// + /// This is useful for testing and debugging where it is not necessary to + /// have full [`OpTransaction`] instance. + /// + /// If the source hash is not [`B256::ZERO`], set the transaction type to deposit and remove the enveloped transaction. + pub fn build_fill(mut self) -> OpTransaction { + let tx_type = self.base.get_tx_type(); + if tx_type.is_some() { + if tx_type == Some(DEPOSIT_TRANSACTION_TYPE) { + // source hash is required for deposit transactions + if self.deposit.source_hash == B256::ZERO { + self.deposit.source_hash = B256::from([1u8; 32]); + } + } else { + // enveloped is required for non-deposit transactions + self.enveloped_tx = Some(vec![0x00].into()); + } + } else if self.deposit.source_hash != B256::ZERO { + // if type is not set and source hash is set, set the transaction type to deposit + self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE)); + } else if self.enveloped_tx.is_none() { + // if type is not set and source hash is not set, set the enveloped transaction to something. + self.enveloped_tx = Some(vec![0x00].into()); + } + + let base = self.base.build_fill(); + + OpTransaction { + base, + enveloped_tx: self.enveloped_tx, + deposit: self.deposit, + } + } + + /// Build the [`OpTransaction`] instance, return error if the transaction is not valid. + /// + pub fn build(mut self) -> Result, OpBuildError> { + let tx_type = self.base.get_tx_type(); + if tx_type.is_some() { + if Some(DEPOSIT_TRANSACTION_TYPE) == tx_type { + // if tx type is deposit, check if source hash is set + if self.deposit.source_hash == B256::ZERO { + return Err(OpBuildError::MissingSourceHashForDeposit); + } + } else if self.enveloped_tx.is_none() { + // enveloped is required for non-deposit transactions + return Err(OpBuildError::MissingEnvelopedTxBytes); + } + } else if self.deposit.source_hash != B256::ZERO { + // if type is not set and source hash is set, set the transaction type to deposit + self.base = self.base.tx_type(Some(DEPOSIT_TRANSACTION_TYPE)); + } else if self.enveloped_tx.is_none() { + // tx is not deposit and enveloped is required + return Err(OpBuildError::MissingEnvelopedTxBytes); + } + + let base = self.base.build()?; + + Ok(OpTransaction { + base, + enveloped_tx: self.enveloped_tx, + deposit: self.deposit, + }) + } +} + +/// Error type for building [`TxEnv`] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum OpBuildError { + /// Base transaction build error + Base(TxEnvBuildError), + /// Missing enveloped transaction bytes + MissingEnvelopedTxBytes, + /// Missing source hash for deposit transaction + MissingSourceHashForDeposit, +} + +impl From for OpBuildError { + fn from(error: TxEnvBuildError) -> Self { + OpBuildError::Base(error) + } +} + #[cfg(test)] mod tests { - use crate::transaction::deposit::DEPOSIT_TRANSACTION_TYPE; - use super::*; - use revm::primitives::{Address, B256}; + use revm::{ + context_interface::Transaction, + primitives::{Address, B256}, + }; #[test] fn test_deposit_transaction_fields() { - let op_tx = OpTransaction { - base: TxEnv { - tx_type: DEPOSIT_TRANSACTION_TYPE, - gas_limit: 10, - gas_price: 100, - gas_priority_fee: Some(5), - ..Default::default() - }, - enveloped_tx: None, - deposit: DepositTransactionParts { - is_system_transaction: false, - mint: Some(0u128), - source_hash: B256::default(), - }, - }; - // Verify transaction type - assert_eq!(op_tx.tx_type(), DEPOSIT_TRANSACTION_TYPE); + let base_tx = TxEnv::builder() + .gas_limit(10) + .gas_price(100) + .gas_priority_fee(Some(5)); + + let op_tx = OpTransaction::builder() + .base(base_tx) + .enveloped_tx(None) + .not_system_transaction() + .mint(0u128) + .source_hash(B256::from([1u8; 32])) + .build() + .unwrap(); + // Verify transaction type (deposit transactions should have tx_type based on OpSpecId) + // The tx_type is derived from the transaction structure, not set manually // Verify common fields access assert_eq!(op_tx.gas_limit(), 10); assert_eq!(op_tx.kind(), revm::primitives::TxKind::Call(Address::ZERO)); - // Verify gas related calculations - assert_eq!(op_tx.effective_gas_price(90), 95); + // Verify gas related calculations - deposit transactions use gas_price for effective gas price + assert_eq!(op_tx.effective_gas_price(90), 100); assert_eq!(op_tx.max_fee_per_gas(), 100); } } diff --git a/crates/op-revm/tests/integration.rs b/crates/op-revm/tests/integration.rs index 16c6aac192..49926ac773 100644 --- a/crates/op-revm/tests/integration.rs +++ b/crates/op-revm/tests/integration.rs @@ -3,8 +3,7 @@ mod common; use common::compare_or_save_testdata; use op_revm::{ - precompiles::bn128_pair::GRANITE_MAX_INPUT_SIZE, - transaction::deposit::DEPOSIT_TRANSACTION_TYPE, DefaultOp, L1BlockInfo, OpBuilder, + precompiles::bn128_pair::GRANITE_MAX_INPUT_SIZE, DefaultOp, L1BlockInfo, OpBuilder, OpHaltReason, OpSpecId, OpTransaction, }; use revm::{ @@ -29,11 +28,13 @@ use std::vec::Vec; #[test] fn test_deposit_tx() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.enveloped_tx = None; - tx.deposit.mint = Some(100); - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - }) + .with_tx( + OpTransaction::builder() + .enveloped_tx(None) + .mint(100) + .source_hash(revm::primitives::B256::from([1u8; 32])) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::HOLOCENE); let mut evm = ctx.build_op(); @@ -54,13 +55,18 @@ fn test_deposit_tx() { #[test] fn test_halted_deposit_tx() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.enveloped_tx = None; - tx.deposit.mint = Some(100); - tx.base.tx_type = DEPOSIT_TRANSACTION_TYPE; - tx.base.caller = BENCH_CALLER; - tx.base.kind = TxKind::Call(BENCH_TARGET); - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)), + ) + .enveloped_tx(None) + .mint(100) + .source_hash(revm::primitives::B256::from([1u8; 32])) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::HOLOCENE) .with_db(BenchmarkDB::new_bytecode(Bytecode::new_legacy( [opcode::POP].into(), @@ -106,10 +112,15 @@ fn p256verify_test_tx( ); Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(u64_to_address(secp256r1::P256VERIFY_ADDRESS)); - tx.base.gas_limit = initial_gas + secp256r1::P256VERIFY_BASE_GAS_FEE; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(u64_to_address(secp256r1::P256VERIFY_ADDRESS))) + .gas_limit(initial_gas + secp256r1::P256VERIFY_BASE_GAS_FEE), + ) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID) } @@ -128,7 +139,33 @@ fn test_tx_call_p256verify() { #[test] fn test_halted_tx_call_p256verify() { - let ctx = p256verify_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + const SPEC_ID: OpSpecId = OpSpecId::FJORD; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &[], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let original_gas_limit = initial_gas + secp256r1::P256VERIFY_BASE_GAS_FEE; + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(u64_to_address(secp256r1::P256VERIFY_ADDRESS))) + .gas_limit(original_gas_limit - 1), + ) + .build_fill(), + ) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -164,11 +201,16 @@ fn bn128_pair_test_tx( ); Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bn128::pair::ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bn128::pair::ADDRESS)) + .data(input) + .gas_limit(initial_gas), + ) + .build_fill(), + ) .modify_cfg_chained(|cfg| cfg.spec = spec) } @@ -213,10 +255,15 @@ fn test_halted_tx_call_bn128_pair_granite() { #[test] fn test_halted_tx_call_bls12_381_g1_add_out_of_gas() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G1_ADD_ADDRESS); - tx.base.gas_limit = 21_000 + bls12_381_const::G1_ADD_BASE_GAS_FEE - 1; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G1_ADD_ADDRESS)) + .gas_limit(21_000 + bls12_381_const::G1_ADD_BASE_GAS_FEE - 1), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -245,10 +292,15 @@ fn test_halted_tx_call_bls12_381_g1_add_out_of_gas() { #[test] fn test_halted_tx_call_bls12_381_g1_add_input_wrong_size() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G1_ADD_ADDRESS); - tx.base.gas_limit = 21_000 + bls12_381_const::G1_ADD_BASE_GAS_FEE; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G1_ADD_ADDRESS)) + .gas_limit(21_000 + bls12_381_const::G1_ADD_BASE_GAS_FEE), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -298,11 +350,16 @@ fn g1_msm_test_tx( ); Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G1_MSM_ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas + gs1_msm_gas; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G1_MSM_ADDRESS)) + .data(input) + .gas_limit(initial_gas + gs1_msm_gas), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -312,7 +369,43 @@ fn g1_msm_test_tx( #[test] fn test_halted_tx_call_bls12_381_g1_msm_input_wrong_size() { - let ctx = g1_msm_test_tx().modify_tx_chained(|tx| tx.base.data = tx.base.data.slice(1..)); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::G1_MSM_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let gs1_msm_gas = bls12_381_utils::msm_required_gas( + 1, + &bls12_381_const::DISCOUNT_TABLE_G1_MSM, + bls12_381_const::G1_MSM_BASE_GAS_FEE, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G1_MSM_ADDRESS)) + .data(input.slice(1..)) + .gas_limit(initial_gas + gs1_msm_gas), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -334,7 +427,43 @@ fn test_halted_tx_call_bls12_381_g1_msm_input_wrong_size() { #[test] fn test_halted_tx_call_bls12_381_g1_msm_out_of_gas() { - let ctx = g1_msm_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::G1_MSM_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let gs1_msm_gas = bls12_381_utils::msm_required_gas( + 1, + &bls12_381_const::DISCOUNT_TABLE_G1_MSM, + bls12_381_const::G1_MSM_BASE_GAS_FEE, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G1_MSM_ADDRESS)) + .data(input) + .gas_limit(initial_gas + gs1_msm_gas - 1), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -379,10 +508,15 @@ fn test_halted_tx_call_bls12_381_g1_msm_wrong_input_layout() { #[test] fn test_halted_tx_call_bls12_381_g2_add_out_of_gas() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G2_ADD_ADDRESS); - tx.base.gas_limit = 21_000 + bls12_381_const::G2_ADD_BASE_GAS_FEE - 1; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G2_ADD_ADDRESS)) + .gas_limit(21_000 + bls12_381_const::G2_ADD_BASE_GAS_FEE - 1), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -411,10 +545,15 @@ fn test_halted_tx_call_bls12_381_g2_add_out_of_gas() { #[test] fn test_halted_tx_call_bls12_381_g2_add_input_wrong_size() { let ctx = Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G2_ADD_ADDRESS); - tx.base.gas_limit = 21_000 + bls12_381_const::G2_ADD_BASE_GAS_FEE; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G2_ADD_ADDRESS)) + .gas_limit(21_000 + bls12_381_const::G2_ADD_BASE_GAS_FEE), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -465,11 +604,16 @@ fn g2_msm_test_tx( ); Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::G2_MSM_ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas + gs2_msm_gas; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G2_MSM_ADDRESS)) + .data(input) + .gas_limit(initial_gas + gs2_msm_gas), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -479,7 +623,43 @@ fn g2_msm_test_tx( #[test] fn test_halted_tx_call_bls12_381_g2_msm_input_wrong_size() { - let ctx = g2_msm_test_tx().modify_tx_chained(|tx| tx.base.data = tx.base.data.slice(1..)); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::G2_MSM_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let gs2_msm_gas = bls12_381_utils::msm_required_gas( + 1, + &bls12_381_const::DISCOUNT_TABLE_G2_MSM, + bls12_381_const::G2_MSM_BASE_GAS_FEE, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G2_MSM_ADDRESS)) + .data(input.slice(1..)) + .gas_limit(initial_gas + gs2_msm_gas), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -501,7 +681,43 @@ fn test_halted_tx_call_bls12_381_g2_msm_input_wrong_size() { #[test] fn test_halted_tx_call_bls12_381_g2_msm_out_of_gas() { - let ctx = g2_msm_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::G2_MSM_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let gs2_msm_gas = bls12_381_utils::msm_required_gas( + 1, + &bls12_381_const::DISCOUNT_TABLE_G2_MSM, + bls12_381_const::G2_MSM_BASE_GAS_FEE, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::G2_MSM_ADDRESS)) + .data(input) + .gas_limit(initial_gas + gs2_msm_gas - 1), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -566,11 +782,16 @@ fn bl12_381_pairing_test_tx( bls12_381_const::PAIRING_MULTIPLIER_BASE + bls12_381_const::PAIRING_OFFSET_BASE; Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::PAIRING_ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas + pairing_gas; - }) + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::PAIRING_ADDRESS)) + .data(input) + .gas_limit(initial_gas + pairing_gas), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) @@ -580,8 +801,40 @@ fn bl12_381_pairing_test_tx( #[test] fn test_halted_tx_call_bls12_381_pairing_input_wrong_size() { - let ctx = - bl12_381_pairing_test_tx().modify_tx_chained(|tx| tx.base.data = tx.base.data.slice(1..)); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::PAIRING_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let pairing_gas: u64 = + bls12_381_const::PAIRING_MULTIPLIER_BASE + bls12_381_const::PAIRING_OFFSET_BASE; + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::PAIRING_ADDRESS)) + .data(input.slice(1..)) + .gas_limit(initial_gas + pairing_gas), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -603,7 +856,40 @@ fn test_halted_tx_call_bls12_381_pairing_input_wrong_size() { #[test] fn test_halted_tx_call_bls12_381_pairing_out_of_gas() { - let ctx = bl12_381_pairing_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::PAIRING_INPUT_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + let pairing_gas: u64 = + bls12_381_const::PAIRING_MULTIPLIER_BASE + bls12_381_const::PAIRING_OFFSET_BASE; + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::PAIRING_ADDRESS)) + .data(input) + .gas_limit(initial_gas + pairing_gas - 1), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = OpSpecId::ISTHMUS); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -645,9 +931,8 @@ fn test_tx_call_bls12_381_pairing_wrong_input_layout() { ); } -fn fp_to_g1_test_tx( -) -> Context, CfgEnv, EmptyDB, Journal, L1BlockInfo> -{ +#[test] +fn test_halted_tx_call_bls12_381_map_fp_to_g1_out_of_gas() { const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; @@ -664,22 +949,22 @@ fn fp_to_g1_test_tx( 0, ); - Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::MAP_FP_TO_G1_ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas + bls12_381_const::MAP_FP_TO_G1_BASE_GAS_FEE; - }) + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::MAP_FP_TO_G1_ADDRESS)) + .data(input) + .gas_limit(initial_gas + bls12_381_const::MAP_FP_TO_G1_BASE_GAS_FEE - 1), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) }) - .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID) -} - -#[test] -fn test_halted_tx_call_bls12_381_map_fp_to_g1_out_of_gas() { - let ctx = fp_to_g1_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -701,7 +986,38 @@ fn test_halted_tx_call_bls12_381_map_fp_to_g1_out_of_gas() { #[test] fn test_halted_tx_call_bls12_381_map_fp_to_g1_input_wrong_size() { - let ctx = fp_to_g1_test_tx().modify_tx_chained(|tx| tx.base.data = tx.base.data.slice(1..)); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::PADDED_FP_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::MAP_FP_TO_G1_ADDRESS)) + .data(input.slice(1..)) + .gas_limit(initial_gas + bls12_381_const::MAP_FP_TO_G1_BASE_GAS_FEE), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -721,9 +1037,8 @@ fn test_halted_tx_call_bls12_381_map_fp_to_g1_input_wrong_size() { ); } -fn fp2_to_g2_test_tx( -) -> Context, CfgEnv, EmptyDB, Journal, L1BlockInfo> -{ +#[test] +fn test_halted_tx_call_bls12_381_map_fp2_to_g2_out_of_gas() { const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; @@ -740,22 +1055,22 @@ fn fp2_to_g2_test_tx( 0, ); - Context::op() - .modify_tx_chained(|tx| { - tx.base.kind = TxKind::Call(bls12_381_const::MAP_FP2_TO_G2_ADDRESS); - tx.base.data = input; - tx.base.gas_limit = initial_gas + bls12_381_const::MAP_FP2_TO_G2_BASE_GAS_FEE; - }) + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::MAP_FP2_TO_G2_ADDRESS)) + .data(input) + .gas_limit(initial_gas + bls12_381_const::MAP_FP2_TO_G2_BASE_GAS_FEE - 1), + ) + .build_fill(), + ) .modify_chain_chained(|l1_block| { l1_block.operator_fee_constant = Some(U256::ZERO); l1_block.operator_fee_scalar = Some(U256::ZERO) }) - .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID) -} - -#[test] -fn test_halted_tx_call_bls12_381_map_fp2_to_g2_out_of_gas() { - let ctx = fp2_to_g2_test_tx().modify_tx_chained(|tx| tx.base.gas_limit -= 1); + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -777,7 +1092,38 @@ fn test_halted_tx_call_bls12_381_map_fp2_to_g2_out_of_gas() { #[test] fn test_halted_tx_call_bls12_381_map_fp2_to_g2_input_wrong_size() { - let ctx = fp2_to_g2_test_tx().modify_tx_chained(|tx| tx.base.data = tx.base.data.slice(1..)); + const SPEC_ID: OpSpecId = OpSpecId::ISTHMUS; + let is_eip7702_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + let is_eip7623_enabled = SPEC_ID >= OpSpecId::ISTHMUS; + + let input = Bytes::from([1; bls12_381_const::PADDED_FP2_LENGTH]); + let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas( + SPEC_ID.into(), + &input[..], + false, + is_eip7702_enabled, + is_eip7623_enabled, + 0, + 0, + 0, + ); + + let ctx = Context::op() + .with_tx( + OpTransaction::builder() + .base( + TxEnv::builder() + .kind(TxKind::Call(bls12_381_const::MAP_FP2_TO_G2_ADDRESS)) + .data(input.slice(1..)) + .gas_limit(initial_gas + bls12_381_const::MAP_FP2_TO_G2_BASE_GAS_FEE), + ) + .build_fill(), + ) + .modify_chain_chained(|l1_block| { + l1_block.operator_fee_constant = Some(U256::ZERO); + l1_block.operator_fee_scalar = Some(U256::ZERO) + }) + .modify_cfg_chained(|cfg| cfg.spec = SPEC_ID); let mut evm = ctx.build_op(); let output = evm.replay().unwrap(); @@ -831,14 +1177,13 @@ fn test_log_inspector() { let mut evm = ctx.build_op_with_inspector(LogInspector::default()); - let tx = OpTransaction { - base: TxEnv { - caller: BENCH_CALLER, - kind: TxKind::Call(BENCH_TARGET), - ..Default::default() - }, - ..Default::default() - }; + let tx = OpTransaction::builder() + .base( + TxEnv::builder() + .caller(BENCH_CALLER) + .kind(TxKind::Call(BENCH_TARGET)), + ) + .build_fill(); // Run evm. let output = evm.inspect_tx(tx).unwrap(); diff --git a/crates/precompile/CHANGELOG.md b/crates/precompile/CHANGELOG.md index 9249d8bdeb..536dcb0269 100644 --- a/crates/precompile/CHANGELOG.md +++ b/crates/precompile/CHANGELOG.md @@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [24.0.1](https://github.com/bluealloy/revm/compare/revm-precompile-v24.0.0...revm-precompile-v24.0.1) - 2025-07-14 + +### Other + +- use c-kzg precompute value 8 ([#2698](https://github.com/bluealloy/revm/pull/2698)) + +## [24.0.0](https://github.com/bluealloy/revm/compare/revm-precompile-v23.0.0...revm-precompile-v24.0.0) - 2025-06-30 + +### Added + +- blake2 avx2 ([#2670](https://github.com/bluealloy/revm/pull/2670)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) + ## [23.0.0](https://github.com/bluealloy/revm/compare/revm-precompile-v22.0.0...revm-precompile-v23.0.0) - 2025-06-19 ### Added diff --git a/crates/precompile/Cargo.toml b/crates/precompile/Cargo.toml index 6f2c64207d..473cdaafe9 100644 --- a/crates/precompile/Cargo.toml +++ b/crates/precompile/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-precompile" description = "Revm Precompiles - Ethereum compatible precompiled contracts" -version = "23.0.0" +version = "24.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -72,11 +72,12 @@ p256 = { workspace = true, features = ["ecdsa"] } # utils cfg-if.workspace = true +arrayref = "0.3.6" [dev-dependencies] criterion.workspace = true rand = { workspace = true, features = ["std"] } -ark-std = { workspace = true} +ark-std = { workspace = true } rstest.workspace = true [features] diff --git a/crates/precompile/src/blake2.rs b/crates/precompile/src/blake2.rs index 6c3e2fb615..3996b894a7 100644 --- a/crates/precompile/src/blake2.rs +++ b/crates/precompile/src/blake2.rs @@ -1,4 +1,5 @@ //! Blake2 precompile. More details in [`run`] + use crate::{PrecompileError, PrecompileOutput, PrecompileResult, PrecompileWithAddress}; const F_ROUND: u64 = 1; @@ -29,19 +30,29 @@ pub fn run(input: &[u8], gas_limit: u64) -> PrecompileResult { }; let mut h = [0u64; 8]; - let mut m = [0u64; 16]; + //let mut m = [0u64; 16]; - for (i, pos) in (4..68).step_by(8).enumerate() { - h[i] = u64::from_le_bytes(input[pos..pos + 8].try_into().unwrap()); - } - for (i, pos) in (68..196).step_by(8).enumerate() { - m[i] = u64::from_le_bytes(input[pos..pos + 8].try_into().unwrap()); - } - let t = [ - u64::from_le_bytes(input[196..196 + 8].try_into().unwrap()), - u64::from_le_bytes(input[204..204 + 8].try_into().unwrap()), - ]; + let t; + // Optimized parsing using ptr::read_unaligned for potentially better performance + + let m; + unsafe { + let ptr = input.as_ptr(); + + // Read h values + for (i, item) in h.iter_mut().enumerate() { + *item = u64::from_le_bytes(core::ptr::read_unaligned( + ptr.add(4 + i * 8) as *const [u8; 8] + )); + } + + m = input[68..68 + 16 * size_of::()].try_into().unwrap(); + t = [ + u64::from_le_bytes(core::ptr::read_unaligned(ptr.add(196) as *const [u8; 8])), + u64::from_le_bytes(core::ptr::read_unaligned(ptr.add(204) as *const [u8; 8])), + ]; + } algo::compress(rounds, &mut h, m, t, f); let mut out = [0u8; 64]; @@ -80,18 +91,25 @@ pub mod algo { 0x5be0cd19137e2179, ]; - #[inline] + #[inline(always)] #[allow(clippy::many_single_char_names)] /// G function: pub fn g(v: &mut [u64], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) { - v[a] = v[a].wrapping_add(v[b]).wrapping_add(x); - v[d] = (v[d] ^ v[a]).rotate_right(32); + v[a] = v[a].wrapping_add(v[b]); + v[a] = v[a].wrapping_add(x); + v[d] ^= v[a]; + v[d] = v[d].rotate_right(32); v[c] = v[c].wrapping_add(v[d]); - v[b] = (v[b] ^ v[c]).rotate_right(24); - v[a] = v[a].wrapping_add(v[b]).wrapping_add(y); - v[d] = (v[d] ^ v[a]).rotate_right(16); + v[b] ^= v[c]; + v[b] = v[b].rotate_right(24); + + v[a] = v[a].wrapping_add(v[b]); + v[a] = v[a].wrapping_add(y); + v[d] ^= v[a]; + v[d] = v[d].rotate_right(16); v[c] = v[c].wrapping_add(v[d]); - v[b] = (v[b] ^ v[c]).rotate_right(63); + v[b] ^= v[c]; + v[b] = v[b].rotate_right(63); } /// Compression function F takes as an argument the state vector "h", @@ -101,7 +119,44 @@ pub mod algo { /// returns a new state vector. The number of rounds, "r", is 12 for /// BLAKE2b and 10 for BLAKE2s. Rounds are numbered from 0 to r - 1. #[allow(clippy::many_single_char_names)] - pub fn compress(rounds: usize, h: &mut [u64; 8], m: [u64; 16], t: [u64; 2], f: bool) { + pub fn compress( + rounds: usize, + h: &mut [u64; 8], + m_slice: &[u8; 16 * size_of::()], + t: [u64; 2], + f: bool, + ) { + assert!(m_slice.len() == 16 * size_of::()); + + #[cfg(all(target_feature = "avx2", feature = "std"))] + { + // only if it is compiled with avx2 flag and it is std, we can use avx2. + if std::is_x86_feature_detected!("avx2") { + // avx2 is 1.8x more performant than portable implementation. + unsafe { + super::avx2::compress_block( + rounds, + m_slice, + h, + ((t[1] as u128) << 64) | (t[0] as u128), + if f { !0 } else { 0 }, + 0, + ); + } + return; + } + } + + // if avx2 is not available, use the fallback portable implementation + + // Read m values + let mut m = [0u64; 16]; + for (i, item) in m.iter_mut().enumerate() { + *item = u64::from_le_bytes(unsafe { + core::ptr::read_unaligned(m_slice.as_ptr().add(i * 8) as *const [u8; 8]) + }); + } + let mut v = [0u64; 16]; v[..h.len()].copy_from_slice(h); // First half from state. v[h.len()..].copy_from_slice(&IV); // Second half from IV. @@ -113,21 +168,491 @@ pub mod algo { v[14] = !v[14] // Invert all bits if the last-block-flag is set. } for i in 0..rounds { - // Message word selection permutation for this round. - let s = &SIGMA[i % 10]; - g(&mut v, 0, 4, 8, 12, m[s[0]], m[s[1]]); - g(&mut v, 1, 5, 9, 13, m[s[2]], m[s[3]]); - g(&mut v, 2, 6, 10, 14, m[s[4]], m[s[5]]); - g(&mut v, 3, 7, 11, 15, m[s[6]], m[s[7]]); - - g(&mut v, 0, 5, 10, 15, m[s[8]], m[s[9]]); - g(&mut v, 1, 6, 11, 12, m[s[10]], m[s[11]]); - g(&mut v, 2, 7, 8, 13, m[s[12]], m[s[13]]); - g(&mut v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + round(&mut v, &m, i); } for i in 0..8 { h[i] ^= v[i] ^ v[i + 8]; } } + + #[inline(always)] + fn round(v: &mut [u64; 16], m: &[u64; 16], r: usize) { + // Message word selection permutation for this round. + let s = &SIGMA[r % 10]; + // g1 + g(v, 0, 4, 8, 12, m[s[0]], m[s[1]]); + g(v, 1, 5, 9, 13, m[s[2]], m[s[3]]); + g(v, 2, 6, 10, 14, m[s[4]], m[s[5]]); + g(v, 3, 7, 11, 15, m[s[6]], m[s[7]]); + + // g2 + g(v, 0, 5, 10, 15, m[s[8]], m[s[9]]); + g(v, 1, 6, 11, 12, m[s[10]], m[s[11]]); + g(v, 2, 7, 8, 13, m[s[12]], m[s[13]]); + g(v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } +} + +// Adapted from https://github.com/rust-lang-nursery/stdsimd/pull/479. +macro_rules! _MM_SHUFFLE { + ($z:expr, $y:expr, $x:expr, $w:expr) => { + ($z << 6) | ($y << 4) | ($x << 2) | $w + }; +} + +/// Code adapted from https://github.com/oconnor663/blake2_simd/blob/82b3e2aee4d2384aabbeb146058301ff0dbd453f/blake2b/src/avx2.rs +#[cfg(all(target_feature = "avx2", feature = "std"))] +mod avx2 { + #[cfg(target_arch = "x86")] + use core::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::*; + + use super::algo::IV; + use arrayref::{array_refs, mut_array_refs}; + + type Word = u64; + type Count = u128; + /// The number input bytes passed to each call to the compression function. Small benchmarks need + /// to use an even multiple of `BLOCKBYTES`, or else their apparent throughput will be low. + const BLOCKBYTES: usize = 16 * size_of::(); + + const DEGREE: usize = 4; + + /// Compress a block of data using the BLAKE2 algorithm. + #[inline(always)] + pub(crate) unsafe fn compress_block( + mut rounds: usize, + block: &[u8; BLOCKBYTES], + words: &mut [Word; 8], + count: Count, + last_block: Word, + last_node: Word, + ) { + let (words_low, words_high) = mut_array_refs!(words, DEGREE, DEGREE); + let (iv_low, iv_high) = array_refs!(&IV, DEGREE, DEGREE); + let mut a = loadu(words_low); + let mut b = loadu(words_high); + let mut c = loadu(iv_low); + let flags = set4(count_low(count), count_high(count), last_block, last_node); + let mut d = xor(loadu(iv_high), flags); + + let msg_chunks = array_refs!(block, 16, 16, 16, 16, 16, 16, 16, 16); + let m0 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.0)); + let m1 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.1)); + let m2 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.2)); + let m3 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.3)); + let m4 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.4)); + let m5 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.5)); + let m6 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.6)); + let m7 = _mm256_broadcastsi128_si256(loadu_128(msg_chunks.7)); + + let iv0 = a; + let iv1 = b; + let mut t0; + let mut t1; + let mut b0; + + loop { + if rounds == 0 { + break; + } + rounds -= 1; + + // round 1 + t0 = _mm256_unpacklo_epi64(m0, m1); + t1 = _mm256_unpacklo_epi64(m2, m3); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m0, m1); + t1 = _mm256_unpackhi_epi64(m2, m3); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_unpacklo_epi64(m7, m4); + t1 = _mm256_unpacklo_epi64(m5, m6); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m7, m4); + t1 = _mm256_unpackhi_epi64(m5, m6); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 2 + t0 = _mm256_unpacklo_epi64(m7, m2); + t1 = _mm256_unpackhi_epi64(m4, m6); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m5, m4); + t1 = _mm256_alignr_epi8(m3, m7, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_unpackhi_epi64(m2, m0); + t1 = _mm256_blend_epi32(m5, m0, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_alignr_epi8(m6, m1, 8); + t1 = _mm256_blend_epi32(m3, m1, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 3 + t0 = _mm256_alignr_epi8(m6, m5, 8); + t1 = _mm256_unpackhi_epi64(m2, m7); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m4, m0); + t1 = _mm256_blend_epi32(m6, m1, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_alignr_epi8(m5, m4, 8); + t1 = _mm256_unpackhi_epi64(m1, m3); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m2, m7); + t1 = _mm256_blend_epi32(m0, m3, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 4 + t0 = _mm256_unpackhi_epi64(m3, m1); + t1 = _mm256_unpackhi_epi64(m6, m5); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m4, m0); + t1 = _mm256_unpacklo_epi64(m6, m7); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_alignr_epi8(m1, m7, 8); + t1 = _mm256_shuffle_epi32(m2, _MM_SHUFFLE!(1, 0, 3, 2)); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m4, m3); + t1 = _mm256_unpacklo_epi64(m5, m0); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 5 + t0 = _mm256_unpackhi_epi64(m4, m2); + t1 = _mm256_unpacklo_epi64(m1, m5); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_blend_epi32(m3, m0, 0x33); + t1 = _mm256_blend_epi32(m7, m2, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_alignr_epi8(m7, m1, 8); + t1 = _mm256_alignr_epi8(m3, m5, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m6, m0); + t1 = _mm256_unpacklo_epi64(m6, m4); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 6 + t0 = _mm256_unpacklo_epi64(m1, m3); + t1 = _mm256_unpacklo_epi64(m0, m4); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m6, m5); + t1 = _mm256_unpackhi_epi64(m5, m1); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_alignr_epi8(m2, m0, 8); + t1 = _mm256_unpackhi_epi64(m3, m7); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m4, m6); + t1 = _mm256_alignr_epi8(m7, m2, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 7 + t0 = _mm256_blend_epi32(m0, m6, 0x33); + t1 = _mm256_unpacklo_epi64(m7, m2); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m2, m7); + t1 = _mm256_alignr_epi8(m5, m6, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_unpacklo_epi64(m4, m0); + t1 = _mm256_blend_epi32(m4, m3, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m5, m3); + t1 = _mm256_shuffle_epi32(m1, _MM_SHUFFLE!(1, 0, 3, 2)); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + // round 8 + t0 = _mm256_unpackhi_epi64(m6, m3); + t1 = _mm256_blend_epi32(m1, m6, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_alignr_epi8(m7, m5, 8); + t1 = _mm256_unpackhi_epi64(m0, m4); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_blend_epi32(m2, m1, 0x33); + t1 = _mm256_alignr_epi8(m4, m7, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m5, m0); + t1 = _mm256_unpacklo_epi64(m2, m3); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 9 + t0 = _mm256_unpacklo_epi64(m3, m7); + t1 = _mm256_alignr_epi8(m0, m5, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpackhi_epi64(m7, m4); + t1 = _mm256_alignr_epi8(m4, m1, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_unpacklo_epi64(m5, m6); + t1 = _mm256_unpackhi_epi64(m6, m0); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_alignr_epi8(m1, m2, 8); + t1 = _mm256_alignr_epi8(m2, m3, 8); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + if rounds == 0 { + break; + } + rounds -= 1; + + // round 10 + t0 = _mm256_unpacklo_epi64(m5, m4); + t1 = _mm256_unpackhi_epi64(m3, m0); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_unpacklo_epi64(m1, m2); + t1 = _mm256_blend_epi32(m2, m3, 0x33); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + diagonalize(&mut a, &mut b, &mut c, &mut d); + t0 = _mm256_unpackhi_epi64(m6, m7); + t1 = _mm256_unpackhi_epi64(m4, m1); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g1(&mut a, &mut b, &mut c, &mut d, &mut b0); + t0 = _mm256_blend_epi32(m5, m0, 0x33); + t1 = _mm256_unpacklo_epi64(m7, m6); + b0 = _mm256_blend_epi32(t0, t1, 0xF0); + g2(&mut a, &mut b, &mut c, &mut d, &mut b0); + undiagonalize(&mut a, &mut b, &mut c, &mut d); + + // last two rounds are removed + } + a = xor(a, c); + b = xor(b, d); + a = xor(a, iv0); + b = xor(b, iv1); + + storeu(a, words_low); + storeu(b, words_high); + } + + #[inline(always)] + pub(crate) fn count_low(count: Count) -> Word { + count as Word + } + + #[inline(always)] + pub(crate) fn count_high(count: Count) -> Word { + (count >> 8 * size_of::()) as Word + } + + #[inline(always)] + unsafe fn loadu(src: *const [Word; DEGREE]) -> __m256i { + // This is an unaligned load, so the pointer cast is allowed. + _mm256_loadu_si256(src as *const __m256i) + } + + #[inline(always)] + unsafe fn storeu(src: __m256i, dest: *mut [Word; DEGREE]) { + // This is an unaligned store, so the pointer cast is allowed. + _mm256_storeu_si256(dest as *mut __m256i, src) + } + + #[inline(always)] + unsafe fn loadu_128(mem_addr: &[u8; 16]) -> __m128i { + _mm_loadu_si128(mem_addr.as_ptr() as *const __m128i) + } + + #[inline(always)] + unsafe fn add(a: __m256i, b: __m256i) -> __m256i { + _mm256_add_epi64(a, b) + } + + #[inline(always)] + unsafe fn xor(a: __m256i, b: __m256i) -> __m256i { + _mm256_xor_si256(a, b) + } + + #[inline(always)] + unsafe fn set4(a: u64, b: u64, c: u64, d: u64) -> __m256i { + _mm256_setr_epi64x(a as i64, b as i64, c as i64, d as i64) + } + + // These rotations are the "simple version". For the "complicated version", see + // https://github.com/sneves/blake2-avx2/blob/b3723921f668df09ece52dcd225a36d4a4eea1d9/blake2b-common.h#L43-L46. + // For a discussion of the tradeoffs, see + // https://github.com/sneves/blake2-avx2/pull/5. In short: + // - Due to an LLVM bug (https://bugs.llvm.org/show_bug.cgi?id=44379), this + // version performs better on recent x86 chips. + // - LLVM is able to optimize this version to AVX-512 rotation instructions + // when those are enabled. + #[inline(always)] + unsafe fn rot32(x: __m256i) -> __m256i { + _mm256_or_si256(_mm256_srli_epi64(x, 32), _mm256_slli_epi64(x, 64 - 32)) + } + + #[inline(always)] + unsafe fn rot24(x: __m256i) -> __m256i { + _mm256_or_si256(_mm256_srli_epi64(x, 24), _mm256_slli_epi64(x, 64 - 24)) + } + + #[inline(always)] + unsafe fn rot16(x: __m256i) -> __m256i { + _mm256_or_si256(_mm256_srli_epi64(x, 16), _mm256_slli_epi64(x, 64 - 16)) + } + + #[inline(always)] + unsafe fn rot63(x: __m256i) -> __m256i { + _mm256_or_si256(_mm256_srli_epi64(x, 63), _mm256_slli_epi64(x, 64 - 63)) + } + + #[inline(always)] + unsafe fn g1( + a: &mut __m256i, + b: &mut __m256i, + c: &mut __m256i, + d: &mut __m256i, + m: &mut __m256i, + ) { + *a = add(*a, *m); + *a = add(*a, *b); + *d = xor(*d, *a); + *d = rot32(*d); + *c = add(*c, *d); + *b = xor(*b, *c); + *b = rot24(*b); + } + + #[inline(always)] + unsafe fn g2( + a: &mut __m256i, + b: &mut __m256i, + c: &mut __m256i, + d: &mut __m256i, + m: &mut __m256i, + ) { + *a = add(*a, *m); + *a = add(*a, *b); + *d = xor(*d, *a); + *d = rot16(*d); + *c = add(*c, *d); + *b = xor(*b, *c); + *b = rot63(*b); + } + + // Note the optimization here of leaving b as the unrotated row, rather than a. + // All the message loads below are adjusted to compensate for this. See + // discussion at https://github.com/sneves/blake2-avx2/pull/4 + #[inline(always)] + unsafe fn diagonalize(a: &mut __m256i, _b: &mut __m256i, c: &mut __m256i, d: &mut __m256i) { + *a = _mm256_permute4x64_epi64(*a, _MM_SHUFFLE!(2, 1, 0, 3)); + *d = _mm256_permute4x64_epi64(*d, _MM_SHUFFLE!(1, 0, 3, 2)); + *c = _mm256_permute4x64_epi64(*c, _MM_SHUFFLE!(0, 3, 2, 1)); + } + + #[inline(always)] + unsafe fn undiagonalize(a: &mut __m256i, _b: &mut __m256i, c: &mut __m256i, d: &mut __m256i) { + *a = _mm256_permute4x64_epi64(*a, _MM_SHUFFLE!(0, 3, 2, 1)); + *d = _mm256_permute4x64_epi64(*d, _MM_SHUFFLE!(1, 0, 3, 2)); + *c = _mm256_permute4x64_epi64(*c, _MM_SHUFFLE!(2, 1, 0, 3)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use primitives::hex; + use std::time::Instant; + + #[test] + fn perfblake2() { + let input = [hex!("0000040048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b616162636465666768696a6b6c6d6e6f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001") + ,hex!("0000020048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001") + ,hex!("0000004048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001")]; + + let time = Instant::now(); + for i in 0..3000 { + let _ = run(&input[i % 3], u64::MAX).unwrap(); + } + println!("duration: {:?}", time.elapsed()); + } } diff --git a/crates/precompile/src/bls12_381/g1_msm.rs b/crates/precompile/src/bls12_381/g1_msm.rs index 9469b2db01..d440f12018 100644 --- a/crates/precompile/src/bls12_381/g1_msm.rs +++ b/crates/precompile/src/bls12_381/g1_msm.rs @@ -25,7 +25,7 @@ pub fn g1_msm(input: &[u8], gas_limit: u64) -> PrecompileResult { let input_len = input.len(); if input_len == 0 || input_len % G1_MSM_INPUT_LENGTH != 0 { return Err(PrecompileError::Other(format!( - "G1MSM input length should be multiple of {G1_MSM_INPUT_LENGTH}, was {input_len}" + "G1MSM input length should be multiple of {G1_MSM_INPUT_LENGTH}, was {input_len}", ))); } diff --git a/crates/precompile/src/bls12_381/g2_msm.rs b/crates/precompile/src/bls12_381/g2_msm.rs index 932916bd9b..dd8b40373e 100644 --- a/crates/precompile/src/bls12_381/g2_msm.rs +++ b/crates/precompile/src/bls12_381/g2_msm.rs @@ -24,7 +24,7 @@ pub fn g2_msm(input: &[u8], gas_limit: u64) -> PrecompileResult { let input_len = input.len(); if input_len == 0 || input_len % G2_MSM_INPUT_LENGTH != 0 { return Err(PrecompileError::Other(format!( - "G2MSM input length should be multiple of {G2_MSM_INPUT_LENGTH}, was {input_len}" + "G2MSM input length should be multiple of {G2_MSM_INPUT_LENGTH}, was {input_len}", ))); } diff --git a/crates/precompile/src/kzg_point_evaluation.rs b/crates/precompile/src/kzg_point_evaluation.rs index 023c49c957..8c285ecb15 100644 --- a/crates/precompile/src/kzg_point_evaluation.rs +++ b/crates/precompile/src/kzg_point_evaluation.rs @@ -80,7 +80,7 @@ pub fn kzg_to_versioned_hash(commitment: &[u8]) -> [u8; 32] { pub fn verify_kzg_proof(commitment: &Bytes48, z: &Bytes32, y: &Bytes32, proof: &Bytes48) -> bool { cfg_if::cfg_if! { if #[cfg(feature = "c-kzg")] { - let kzg_settings = c_kzg::ethereum_kzg_settings(0); + let kzg_settings = c_kzg::ethereum_kzg_settings(8); kzg_settings.verify_kzg_proof(commitment, z, y, proof).unwrap_or(false) } else if #[cfg(feature = "kzg-rs")] { let env = kzg_rs::EnvKzgSettings::default(); diff --git a/crates/precompile/src/lib.rs b/crates/precompile/src/lib.rs index ddf05cdce5..7cf739fba5 100644 --- a/crates/precompile/src/lib.rs +++ b/crates/precompile/src/lib.rs @@ -35,6 +35,9 @@ cfg_if::cfg_if! { } } +#[cfg(not(target_feature = "avx2"))] +use arrayref as _; + #[cfg(all(feature = "c-kzg", feature = "kzg-rs"))] // silence kzg-rs lint as c-kzg will be used as default if both are enabled. use kzg_rs as _; diff --git a/crates/revm/CHANGELOG.md b/crates/revm/CHANGELOG.md index bcd0bc28e3..8238be0391 100644 --- a/crates/revm/CHANGELOG.md +++ b/crates/revm/CHANGELOG.md @@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [27.0.3](https://github.com/bluealloy/revm/compare/revm-v27.0.2...revm-v27.0.3) - 2025-07-14 + +### Other + +- updated the following local packages: revm-context, revm-interpreter, revm-precompile, revm-handler, revm-inspector + +## [27.0.2](https://github.com/bluealloy/revm/compare/revm-v27.0.1...revm-v27.0.2) - 2025-07-03 + +### Other + +- updated the following local packages: revm-bytecode, revm-handler, revm-inspector, revm-state, revm-database-interface, revm-context-interface, revm-context, revm-database, revm-interpreter + +## [27.0.1](https://github.com/bluealloy/revm/compare/revm-v26.0.1...revm-v27.0.1) - 2025-06-30 + +### Added + +- optional_eip3541 ([#2661](https://github.com/bluealloy/revm/pull/2661)) + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- inline documentation of revm top modules ([#2666](https://github.com/bluealloy/revm/pull/2666)) +- use TxEnv::builder ([#2652](https://github.com/bluealloy/revm/pull/2652)) + ## [26.0.1](https://github.com/bluealloy/revm/compare/revm-v26.0.0...revm-v26.0.1) - 2025-06-20 ### Fixed diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 9f4fcd04bd..89fc154a89 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm" description = "Revm - Rust Ethereum Virtual Machine" -version = "26.0.1" +version = "27.0.3" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -79,12 +79,14 @@ dev = [ "memory_limit", "optional_balance_check", "optional_block_gas_limit", + "optional_eip3541", "optional_eip3607", "optional_no_base_fee", ] memory_limit = ["context/memory_limit", "interpreter/memory_limit"] optional_balance_check = ["context/optional_balance_check"] optional_block_gas_limit = ["context/optional_block_gas_limit"] +optional_eip3541 = ["context/optional_eip3541"] optional_eip3607 = ["context/optional_eip3607"] optional_no_base_fee = ["context/optional_no_base_fee"] enable_eip7702 = ["context/enable_eip7702"] diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index a4eedb309c..091f6807e7 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -3,16 +3,27 @@ #![cfg_attr(not(feature = "std"), no_std)] // reexport dependencies +#[doc(inline)] pub use bytecode; +#[doc(inline)] pub use context; +#[doc(inline)] pub use context_interface; +#[doc(inline)] pub use database; +#[doc(inline)] pub use database_interface; +#[doc(inline)] pub use handler; +#[doc(inline)] pub use inspector; +#[doc(inline)] pub use interpreter; +#[doc(inline)] pub use precompile; +#[doc(inline)] pub use primitives; +#[doc(inline)] pub use state; // Export items. diff --git a/crates/revm/tests/testdata/test_frame_stack_index.json b/crates/revm/tests/testdata/test_frame_stack_index.json new file mode 100644 index 0000000000..c5981e4b93 --- /dev/null +++ b/crates/revm/tests/testdata/test_frame_stack_index.json @@ -0,0 +1,11 @@ +{ + "Success": { + "reason": "Stop", + "gas_used": 21000, + "gas_refunded": 0, + "logs": [], + "output": { + "Call": "0x" + } + } +} \ No newline at end of file diff --git a/crates/state/CHANGELOG.md b/crates/state/CHANGELOG.md index 699175f2c2..4705cc41dd 100644 --- a/crates/state/CHANGELOG.md +++ b/crates/state/CHANGELOG.md @@ -12,6 +12,18 @@ Dependency bump ## [Unreleased] +## [7.0.1](https://github.com/bluealloy/revm/compare/revm-state-v7.0.0...revm-state-v7.0.1) - 2025-07-03 + +### Other + +- updated the following local packages: revm-bytecode + +## [6.0.1](https://github.com/bluealloy/revm/compare/revm-state-v6.0.0...revm-state-v6.0.1) - 2025-06-30 + +### Other + +- fix copy-pasted inner doc comments ([#2663](https://github.com/bluealloy/revm/pull/2663)) + ## [5.1.0](https://github.com/bluealloy/revm/compare/revm-state-v5.0.0...revm-state-v5.1.0) - 2025-06-19 ### Added diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 3dac275120..f5e06ac4dd 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-state" description = "Revm state types" -version = "6.0.0" +version = "7.0.1" authors.workspace = true edition.workspace = true keywords.workspace = true diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 6a97401848..d10af7eac3 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! Account and storage state. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/crates/statetest-types/CHANGELOG.md b/crates/statetest-types/CHANGELOG.md index 148990528a..83afd39605 100644 --- a/crates/statetest-types/CHANGELOG.md +++ b/crates/statetest-types/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.4](https://github.com/bluealloy/revm/compare/revm-statetest-types-v8.0.3...revm-statetest-types-v8.0.4) - 2025-07-14 + +### Other + +- updated the following local packages: revm + +## [8.0.3](https://github.com/bluealloy/revm/compare/revm-statetest-types-v8.0.2...revm-statetest-types-v8.0.3) - 2025-07-03 + +### Other + +- updated the following local packages: revm + +## [8.0.2](https://github.com/bluealloy/revm/compare/revm-statetest-types-v8.0.1...revm-statetest-types-v8.0.2) - 2025-06-30 + +### Other + +- cargo clippy --fix --all ([#2671](https://github.com/bluealloy/revm/pull/2671)) +- statetest runner cleanup ([#2665](https://github.com/bluealloy/revm/pull/2665)) + ## [8.0.1](https://github.com/bluealloy/revm/compare/revm-statetest-types-v8.0.0...revm-statetest-types-v8.0.1) - 2025-06-20 ### Other diff --git a/crates/statetest-types/Cargo.toml b/crates/statetest-types/Cargo.toml index 8d12806b22..19b7475971 100644 --- a/crates/statetest-types/Cargo.toml +++ b/crates/statetest-types/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "revm-statetest-types" description = "Statetest types for revme" -version = "8.0.1" +version = "8.0.4" authors.workspace = true edition.workspace = true keywords.workspace = true @@ -21,3 +21,5 @@ workspace = true revm = { workspace = true, features = ["std", "serde"] } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true, features = ["preserve_order"] } +k256 = { workspace = true } +thiserror = { workspace = true } diff --git a/crates/statetest-types/src/error.rs b/crates/statetest-types/src/error.rs new file mode 100644 index 0000000000..3146a2f37a --- /dev/null +++ b/crates/statetest-types/src/error.rs @@ -0,0 +1,21 @@ +use revm::primitives::B256; +use thiserror::Error; + +/// Errors that can occur during test setup and execution +#[derive(Debug, Error)] +pub enum TestError { + /// Unknown private key. + #[error("unknown private key: {0:?}")] + UnknownPrivateKey(B256), + /// Invalid transaction type. + #[error("invalid transaction type")] + InvalidTransactionType, + /// Unexpected exception. + #[error("unexpected exception: got {got_exception:?}, expected {expected_exception:?}")] + UnexpectedException { + /// Expected exception. + expected_exception: Option, + /// Got exception. + got_exception: Option, + }, +} diff --git a/crates/statetest-types/src/lib.rs b/crates/statetest-types/src/lib.rs index 578df9555c..e407782de8 100644 --- a/crates/statetest-types/src/lib.rs +++ b/crates/statetest-types/src/lib.rs @@ -9,19 +9,23 @@ mod account_info; mod deserializer; mod env; +mod error; mod spec; mod test; mod test_authorization; mod test_suite; mod test_unit; mod transaction; +mod utils; pub use account_info::*; pub use deserializer::*; pub use env::*; +pub use error::*; pub use spec::*; pub use test::*; pub use test_authorization::*; pub use test_suite::*; pub use test_unit::*; pub use transaction::*; +pub use utils::*; diff --git a/crates/statetest-types/src/test.rs b/crates/statetest-types/src/test.rs index 89552a3bc3..8bc7cb9c42 100644 --- a/crates/statetest-types/src/test.rs +++ b/crates/statetest-types/src/test.rs @@ -1,7 +1,12 @@ -use revm::primitives::{Address, Bytes, HashMap, B256}; +use revm::{ + context::tx::TxEnv, + primitives::{Address, Bytes, HashMap, TxKind, B256}, +}; use serde::Deserialize; -use crate::{transaction::TxPartIndices, AccountInfo}; +use crate::{ + error::TestError, transaction::TxPartIndices, utils::recover_address, AccountInfo, TestUnit, +}; /// State test indexed state result deserialization. #[derive(Debug, PartialEq, Eq, Deserialize)] @@ -34,3 +39,97 @@ pub struct Test { /// Tx bytes pub txbytes: Option, } + +impl Test { + /// Create a transaction environment from this test and the test unit. + /// + /// This function sets up the transaction environment using the test's + /// indices to select the appropriate transaction parameters from the + /// test unit. + /// + /// # Arguments + /// + /// * `unit` - The test unit containing transaction parts + /// + /// # Returns + /// + /// A configured [`TxEnv`] ready for execution, or an error if setup fails + /// + /// # Errors + /// + /// Returns an error if: + /// - The private key cannot be used to recover the sender address + /// - The transaction type is invalid and no exception is expected + pub fn tx_env(&self, unit: &TestUnit) -> Result { + // Setup sender + let caller = if let Some(address) = unit.transaction.sender { + address + } else { + recover_address(unit.transaction.secret_key.as_slice()) + .ok_or(TestError::UnknownPrivateKey(unit.transaction.secret_key))? + }; + + // Transaction specific fields + let tx_type = unit.transaction.tx_type(self.indexes.data).ok_or_else(|| { + if self.expect_exception.is_some() { + TestError::UnexpectedException { + expected_exception: self.expect_exception.clone(), + got_exception: Some("Invalid transaction type".to_string()), + } + } else { + TestError::InvalidTransactionType + } + })?; + + let tx = TxEnv { + caller, + gas_price: unit + .transaction + .gas_price + .or(unit.transaction.max_fee_per_gas) + .unwrap_or_default() + .try_into() + .unwrap_or(u128::MAX), + gas_priority_fee: unit + .transaction + .max_priority_fee_per_gas + .map(|b| u128::try_from(b).expect("max priority fee less than u128::MAX")), + blob_hashes: unit.transaction.blob_versioned_hashes.clone(), + max_fee_per_blob_gas: unit + .transaction + .max_fee_per_blob_gas + .map(|b| u128::try_from(b).expect("max fee less than u128::MAX")) + .unwrap_or(u128::MAX), + tx_type: tx_type as u8, + gas_limit: unit.transaction.gas_limit[self.indexes.gas].saturating_to(), + data: unit.transaction.data[self.indexes.data].clone(), + nonce: u64::try_from(unit.transaction.nonce).unwrap(), + value: unit.transaction.value[self.indexes.value], + access_list: unit + .transaction + .access_lists + .get(self.indexes.data) + .cloned() + .flatten() + .unwrap_or_default(), + authorization_list: unit + .transaction + .authorization_list + .clone() + .map(|auth_list| { + auth_list + .into_iter() + .map(|i| revm::context::either::Either::Left(i.into())) + .collect::>() + }) + .unwrap_or_default(), + kind: match unit.transaction.to { + Some(add) => TxKind::Call(add), + None => TxKind::Create, + }, + ..TxEnv::default() + }; + + Ok(tx) + } +} diff --git a/crates/statetest-types/src/test_unit.rs b/crates/statetest-types/src/test_unit.rs index 23058830ee..f92caf4fe6 100644 --- a/crates/statetest-types/src/test_unit.rs +++ b/crates/statetest-types/src/test_unit.rs @@ -2,7 +2,16 @@ use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use crate::{AccountInfo, Env, SpecName, Test, TransactionParts}; -use revm::primitives::{Address, Bytes}; +use revm::{ + context::{block::BlockEnv, cfg::CfgEnv}, + context_interface::block::calc_excess_blob_gas, + database::CacheState, + primitives::{ + eip4844::TARGET_BLOB_GAS_PER_BLOCK_CANCUN, hardfork::SpecId, keccak256, Address, Bytes, + B256, + }, + state::Bytecode, +}; /// Single test unit struct #[derive(Debug, PartialEq, Eq, Deserialize)] @@ -51,3 +60,90 @@ pub struct TestUnit { pub out: Option, //pub config } + +impl TestUnit { + /// Prepare the state from the test unit. + /// + /// This function uses [`TestUnit::pre`] to prepare the pre-state from the test unit. + /// It creates a new cache state and inserts the accounts from the test unit. + /// + /// # Returns + /// + /// A [`CacheState`] object containing the pre-state accounts and storages. + pub fn state(&self) -> CacheState { + let mut cache_state = CacheState::new(false); + for (address, info) in &self.pre { + let code_hash = keccak256(&info.code); + let bytecode = Bytecode::new_raw_checked(info.code.clone()) + .unwrap_or(Bytecode::new_legacy(info.code.clone())); + let acc_info = revm::state::AccountInfo { + balance: info.balance, + code_hash, + code: Some(bytecode), + nonce: info.nonce, + }; + cache_state.insert_account_with_storage(*address, acc_info, info.storage.clone()); + } + cache_state + } + + /// Create a block environment from the test unit. + /// + /// This function sets up the block environment using the current test unit's + /// environment settings and the provided configuration. + /// + /// # Arguments + /// + /// * `cfg` - The configuration environment containing spec and blob settings + /// + /// # Returns + /// + /// A configured [`BlockEnv`] ready for execution + pub fn block_env(&self, cfg: &CfgEnv) -> BlockEnv { + let mut block = BlockEnv { + number: self.env.current_number, + beneficiary: self.env.current_coinbase, + timestamp: self.env.current_timestamp, + gas_limit: self.env.current_gas_limit.try_into().unwrap_or(u64::MAX), + basefee: self + .env + .current_base_fee + .unwrap_or_default() + .try_into() + .unwrap_or(u64::MAX), + difficulty: self.env.current_difficulty, + prevrandao: self.env.current_random, + ..BlockEnv::default() + }; + + // Handle EIP-4844 blob gas + if let Some(current_excess_blob_gas) = self.env.current_excess_blob_gas { + block.set_blob_excess_gas_and_price( + current_excess_blob_gas.to(), + revm::primitives::eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, + ); + } else if let (Some(parent_blob_gas_used), Some(parent_excess_blob_gas)) = ( + self.env.parent_blob_gas_used, + self.env.parent_excess_blob_gas, + ) { + block.set_blob_excess_gas_and_price( + calc_excess_blob_gas( + parent_blob_gas_used.to(), + parent_excess_blob_gas.to(), + self.env + .parent_target_blobs_per_block + .map(|i| i.to()) + .unwrap_or(TARGET_BLOB_GAS_PER_BLOCK_CANCUN), + ), + revm::primitives::eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, + ); + } + + // Set default prevrandao for merge + if cfg.spec.is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() { + block.prevrandao = Some(B256::default()); + } + + block + } +} diff --git a/crates/statetest-types/src/transaction.rs b/crates/statetest-types/src/transaction.rs index b11f1b3e97..81a98a30a7 100644 --- a/crates/statetest-types/src/transaction.rs +++ b/crates/statetest-types/src/transaction.rs @@ -90,14 +90,6 @@ impl TransactionParts { return Some(TransactionType::Eip7702); } - // TODO(EOF) - // // And if it has initcodes it is EIP-7873 tx - // if self.initcodes.is_some() { - // // Target need to be present for EIP-7873 tx - // self.to?; - // return Some(TransactionType::Eip7873); - // } - Some(tx_type) } } diff --git a/crates/statetest-types/src/utils.rs b/crates/statetest-types/src/utils.rs new file mode 100644 index 0000000000..3ecc89594a --- /dev/null +++ b/crates/statetest-types/src/utils.rs @@ -0,0 +1,25 @@ +use k256::ecdsa::SigningKey; +use revm::primitives::Address; + +/// Recover the address from a private key ([SigningKey]). +pub fn recover_address(private_key: &[u8]) -> Option
{ + let key = SigningKey::from_slice(private_key).ok()?; + let public_key = key.verifying_key().to_encoded_point(false); + Some(Address::from_raw_public_key(&public_key.as_bytes()[1..])) +} + +#[cfg(test)] +mod tests { + use super::*; + use revm::primitives::{address, hex}; + + #[test] + fn sanity_test() { + assert_eq!( + Some(address!("a94f5374fce5edbc8e2a8697c15331677e6ebf0b")), + recover_address(&hex!( + "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + )) + ) + } +} diff --git a/examples/block_traces/src/main.rs b/examples/block_traces/src/main.rs index df63be3186..af1b3f95e9 100644 --- a/examples/block_traces/src/main.rs +++ b/examples/block_traces/src/main.rs @@ -1,4 +1,6 @@ -//! Optimism-specific constants, types, and helpers. +//! Example that show how to replay a block and trace the execution of each transaction. +//! +//! The EIP3155 trace of each transaction is saved into file `traces/{tx_number}.json`. #![cfg_attr(not(test), warn(unused_crate_dependencies))] use alloy_consensus::Transaction; @@ -119,22 +121,22 @@ async fn main() -> anyhow::Result<()> { // Construct the file writer to write the trace to let tx_number = tx.transaction_index.unwrap_or_default(); - let tx = TxEnv { - caller: tx.inner.signer(), - gas_limit: tx.gas_limit(), - gas_price: tx.gas_price().unwrap_or(tx.inner.max_fee_per_gas()), - value: tx.value(), - data: tx.input().to_owned(), - gas_priority_fee: tx.max_priority_fee_per_gas(), - chain_id: Some(chain_id), - nonce: tx.nonce(), - access_list: tx.access_list().cloned().unwrap_or_default(), - kind: match tx.to() { + let tx = TxEnv::builder() + .caller(tx.inner.signer()) + .gas_limit(tx.gas_limit()) + .gas_price(tx.gas_price().unwrap_or(tx.inner.max_fee_per_gas())) + .value(tx.value()) + .data(tx.input().to_owned()) + .gas_priority_fee(tx.max_priority_fee_per_gas()) + .chain_id(Some(chain_id)) + .nonce(tx.nonce()) + .access_list(tx.access_list().cloned().unwrap_or_default()) + .kind(match tx.to() { Some(to_address) => TxKind::Call(to_address), None => TxKind::Create, - }, - ..Default::default() - }; + }) + .build() + .unwrap(); let file_name = format!("traces/{tx_number}.json"); let write = OpenOptions::new() diff --git a/examples/contract_deployment/src/main.rs b/examples/contract_deployment/src/main.rs index c36ef7bfca..6e900b8cd9 100644 --- a/examples/contract_deployment/src/main.rs +++ b/examples/contract_deployment/src/main.rs @@ -1,4 +1,4 @@ -//! Optimism-specific constants, types, and helpers. +//! Example that deploys a contract by forging and executing a contract creation transaction. #![cfg_attr(not(test), warn(unused_crate_dependencies))] use anyhow::{anyhow, bail}; @@ -52,11 +52,13 @@ fn main() -> anyhow::Result<()> { let mut evm = ctx.build_mainnet(); println!("bytecode: {}", hex::encode(&bytecode)); - let ref_tx = evm.transact_commit(TxEnv { - kind: TxKind::Create, - data: bytecode.clone(), - ..Default::default() - })?; + let ref_tx = evm.transact_commit( + TxEnv::builder() + .kind(TxKind::Create) + .data(bytecode.clone()) + .build() + .unwrap(), + )?; let ExecutionResult::Success { output: Output::Create(_, Some(address)), .. @@ -66,12 +68,14 @@ fn main() -> anyhow::Result<()> { }; println!("Created contract at {address}"); - let output = evm.transact(TxEnv { - kind: TxKind::Call(address), - data: Default::default(), - nonce: 1, - ..Default::default() - })?; + let output = evm.transact( + TxEnv::builder() + .kind(TxKind::Call(address)) + .data(Default::default()) + .nonce(1) + .build() + .unwrap(), + )?; let Some(storage0) = output .state .get(&address) diff --git a/examples/custom_opcodes/src/main.rs b/examples/custom_opcodes/src/main.rs index d679fc1cf5..c08305de5f 100644 --- a/examples/custom_opcodes/src/main.rs +++ b/examples/custom_opcodes/src/main.rs @@ -52,10 +52,12 @@ pub fn main() { .with_inspector(TracerEip3155::new_stdout().without_summary()); // inspect the transaction. - let _ = evm.inspect_one_tx(TxEnv { - kind: TxKind::Call(BENCH_TARGET), - ..Default::default() - }); + let _ = evm.inspect_one_tx( + TxEnv::builder() + .kind(TxKind::Call(BENCH_TARGET)) + .build() + .unwrap(), + ); // Expected output where we can see that JUMPDEST is called. /* diff --git a/examples/custom_precompile_journal/CHANGELOG.md b/examples/custom_precompile_journal/CHANGELOG.md new file mode 100644 index 0000000000..30d68f8f4e --- /dev/null +++ b/examples/custom_precompile_journal/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0](https://github.com/bluealloy/revm/releases/tag/custom_precompile_journal-v0.1.0) - 2025-07-03 + +### Added + +- add custom precompile with journal access example ([#2677](https://github.com/bluealloy/revm/pull/2677)) diff --git a/examples/custom_precompile_journal/Cargo.toml b/examples/custom_precompile_journal/Cargo.toml new file mode 100644 index 0000000000..cc46f6f2f5 --- /dev/null +++ b/examples/custom_precompile_journal/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "custom_precompile_journal" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "custom_precompile_journal" +path = "src/main.rs" + +[dependencies] +revm = { path = "../../crates/revm", features = ["optional_eip3607"] } +anyhow = "1.0" diff --git a/examples/custom_precompile_journal/README.md b/examples/custom_precompile_journal/README.md new file mode 100644 index 0000000000..f4d11ac00c --- /dev/null +++ b/examples/custom_precompile_journal/README.md @@ -0,0 +1,124 @@ +# Custom Precompile with Journal Access Example + +This example demonstrates how to create a custom precompile for REVM that can access and modify the journal (state), integrated into a custom EVM implementation similar to MyEvm. + +## Overview + +The example shows: +1. How to create a custom precompile provider that extends the standard Ethereum precompiles +2. How to implement a precompile that can read from and write to the journaled state +3. How to modify account balances and storage from within a precompile +4. How to integrate custom precompiles into a custom EVM implementation +5. How to create handlers for transaction execution + +## Architecture + +### CustomPrecompileProvider + +A custom implementation of the `PrecompileProvider` trait that: +- Extends the standard Ethereum precompiles (`EthPrecompiles`) +- Adds a custom precompile at address `0x0000000000000000000000000000000000000100` +- Delegates to standard precompiles for all other addresses +- Implements journal access for storage and balance operations + +### CustomEvm + +A custom EVM implementation that: +- Wraps the standard REVM `Evm` struct with `CustomPrecompileProvider` +- Follows the same pattern as the MyEvm example +- Maintains full compatibility with REVM's execution model +- Supports both regular and inspector-based execution + +### CustomHandler + +A handler implementation that: +- Implements the `Handler` trait for transaction execution +- Supports both `Handler` and `InspectorHandler` traits +- Can be used with `handler.run(&mut evm)` for full transaction execution + +## Custom Precompile Functionality + +The precompile at `0x0100` supports two operations: + +1. **Read Storage** (empty input data): + - Reads a value from storage slot 0 + - Returns the value as output + - Gas cost: 2,100 + +2. **Write Storage** (32 bytes input): + - Stores the input value to storage slot 0 + - Transfers 1 wei from the precompile to the caller as a reward + - Gas cost: 41,000 (21,000 base + 20,000 for SSTORE) + +## Journal Access Patterns + +The example demonstrates how to access the journal from within a precompile: + +```rust +// Reading storage +let value = context + .journal_mut() + .sload(address, key) + .map_err(|e| PrecompileError::Other(format!("Storage read failed: {:?}", e)))? + .data; + +// Writing storage +context + .journal_mut() + .sstore(address, key, value) + .map_err(|e| PrecompileError::Other(format!("Storage write failed: {:?}", e)))?; + +// Transferring balance +context + .journal_mut() + .transfer(from, to, amount) + .map_err(|e| PrecompileError::Other(format!("Transfer failed: {:?}", e)))?; + +// Incrementing balance +context + .journal_mut() + .balance_incr(address, amount) + .map_err(|e| PrecompileError::Other(format!("Balance increment failed: {:?}", e)))?; +``` + +## Usage + +To use this custom EVM in your application: + +```rust +use custom_precompile_journal::{CustomEvm, CustomHandler}; +use revm::{context::Context, inspector::NoOpInspector, MainContext}; + +// Create the custom EVM +let context = Context::mainnet().with_db(db); +let mut evm = CustomEvm::new(context, NoOpInspector); + +// Create the handler +let handler = CustomHandler::>::default(); + +// Execute transactions +let result = handler.run(&mut evm); +``` + +## Safety Features + +- **Static call protection**: Prevents state modification in view calls +- **Gas accounting**: Proper gas cost calculation and out-of-gas protection +- **Error handling**: Comprehensive error types and result handling +- **Type safety**: Full Rust type safety with generic constraints + +## Running the Example + +```bash +cargo run -p custom_precompile_journal +``` + +The example will demonstrate the custom EVM architecture and show how the various components work together to provide journal access functionality within precompiles. + +## Integration with Existing Code + +This example extends the op-revm pattern and demonstrates how to: +- Create custom precompile providers that can access the journal +- Integrate custom precompiles into REVM's execution model +- Maintain compatibility with existing REVM patterns and interfaces +- Build custom EVM variants similar to MyEvm but with enhanced precompile capabilities \ No newline at end of file diff --git a/examples/custom_precompile_journal/src/custom_evm.rs b/examples/custom_precompile_journal/src/custom_evm.rs new file mode 100644 index 0000000000..02222c8fd2 --- /dev/null +++ b/examples/custom_precompile_journal/src/custom_evm.rs @@ -0,0 +1,151 @@ +//! Custom EVM implementation with journal-accessing precompiles. + +use crate::precompile_provider::CustomPrecompileProvider; +use revm::{ + context::{ContextError, ContextSetters, ContextTr, Evm, FrameStack}, + handler::{ + evm::FrameTr, instructions::EthInstructions, EthFrame, EvmTr, FrameInitOrResult, + ItemOrResult, + }, + inspector::{InspectorEvmTr, JournalExt}, + interpreter::interpreter::EthInterpreter, + primitives::hardfork::SpecId, + Database, Inspector, +}; + +/// Custom EVM variant with journal-accessing precompiles. +/// +/// This EVM extends the standard behavior by using a custom precompile provider +/// that includes journal access functionality. It follows the same pattern as MyEvm +/// but uses CustomPrecompileProvider instead of EthPrecompiles. +#[derive(Debug)] +pub struct CustomEvm( + pub Evm< + CTX, + INSP, + EthInstructions, + CustomPrecompileProvider, + EthFrame, + >, +); + +impl CustomEvm +where + CTX: ContextTr>, +{ + /// Creates a new instance of CustomEvm with the provided context and inspector. + /// + /// # Arguments + /// + /// * `ctx` - The execution context that manages state, environment, and journaling + /// * `inspector` - The inspector for debugging and tracing execution + /// + /// # Returns + /// + /// A new CustomEvm instance configured with: + /// - The provided context and inspector + /// - Mainnet instruction set + /// - Custom precompiles with journal access + /// - A fresh frame stack for execution + pub fn new(ctx: CTX, inspector: INSP) -> Self { + Self(Evm { + ctx, + inspector, + instruction: EthInstructions::new_mainnet(), + precompiles: CustomPrecompileProvider::new_with_spec(SpecId::CANCUN), + frame_stack: FrameStack::new(), + }) + } +} + +impl EvmTr for CustomEvm +where + CTX: ContextTr>, +{ + type Context = CTX; + type Instructions = EthInstructions; + type Precompiles = CustomPrecompileProvider; + type Frame = EthFrame; + + fn ctx(&mut self) -> &mut Self::Context { + &mut self.0.ctx + } + + fn ctx_ref(&self) -> &Self::Context { + self.0.ctx_ref() + } + + fn ctx_instructions(&mut self) -> (&mut Self::Context, &mut Self::Instructions) { + self.0.ctx_instructions() + } + + fn ctx_precompiles(&mut self) -> (&mut Self::Context, &mut Self::Precompiles) { + self.0.ctx_precompiles() + } + + fn frame_stack(&mut self) -> &mut FrameStack { + self.0.frame_stack() + } + + fn frame_init( + &mut self, + frame_input: ::FrameInit, + ) -> Result< + ItemOrResult<&mut Self::Frame, ::FrameResult>, + ContextError<<::Db as Database>::Error>, + > { + self.0.frame_init(frame_input) + } + + fn frame_run( + &mut self, + ) -> Result< + FrameInitOrResult, + ContextError<<::Db as Database>::Error>, + > { + self.0.frame_run() + } + + fn frame_return_result( + &mut self, + frame_result: ::FrameResult, + ) -> Result< + Option<::FrameResult>, + ContextError<<::Db as Database>::Error>, + > { + self.0.frame_return_result(frame_result) + } +} + +impl InspectorEvmTr for CustomEvm +where + CTX: ContextSetters, Journal: JournalExt>, + INSP: Inspector, +{ + type Inspector = INSP; + + fn inspector(&mut self) -> &mut Self::Inspector { + self.0.inspector() + } + + fn ctx_inspector(&mut self) -> (&mut Self::Context, &mut Self::Inspector) { + self.0.ctx_inspector() + } + + fn ctx_inspector_frame( + &mut self, + ) -> (&mut Self::Context, &mut Self::Inspector, &mut Self::Frame) { + self.0.ctx_inspector_frame() + } + + fn ctx_inspector_frame_instructions( + &mut self, + ) -> ( + &mut Self::Context, + &mut Self::Inspector, + &mut Self::Frame, + &mut Self::Instructions, + ) { + self.0.ctx_inspector_frame_instructions() + } +} diff --git a/examples/custom_precompile_journal/src/lib.rs b/examples/custom_precompile_journal/src/lib.rs new file mode 100644 index 0000000000..91e98b838f --- /dev/null +++ b/examples/custom_precompile_journal/src/lib.rs @@ -0,0 +1,7 @@ +//! Custom EVM implementation with journal-accessing precompiles. + +pub mod custom_evm; +pub mod precompile_provider; + +pub use custom_evm::CustomEvm; +pub use precompile_provider::CustomPrecompileProvider; diff --git a/examples/custom_precompile_journal/src/main.rs b/examples/custom_precompile_journal/src/main.rs new file mode 100644 index 0000000000..2aaf791a62 --- /dev/null +++ b/examples/custom_precompile_journal/src/main.rs @@ -0,0 +1,198 @@ +//! Example of a custom precompile that can access and modify the journal. +//! +//! This example demonstrates: +//! 1. Creating a custom precompile provider that extends the standard Ethereum precompiles +//! 2. Implementing a precompile that can read from and write to the journaled state +//! 3. Modifying account balances and storage from within a precompile +//! 4. Integrating the custom precompile into a custom EVM implementation + +use custom_precompile_journal::{precompile_provider::CUSTOM_PRECOMPILE_ADDRESS, CustomEvm}; +use revm::{ + context::{result::InvalidTransaction, Context, ContextSetters, ContextTr, TxEnv}, + context_interface::result::EVMError, + database::InMemoryDB, + handler::{Handler, MainnetHandler}, + inspector::NoOpInspector, + primitives::{address, TxKind, U256}, + state::AccountInfo, + Database, MainContext, +}; + +// Type alias for the error type +type MyError = EVMError; + +fn main() -> anyhow::Result<()> { + println!("=== Custom EVM with Journal-Accessing Precompiles ===\n"); + + // Setup initial accounts + let user_address = address!("0000000000000000000000000000000000000001"); + let mut db = InMemoryDB::default(); + + // Give the user some ETH for gas + let user_balance = U256::from(10).pow(U256::from(18)); // 1 ETH + db.insert_account_info( + user_address, + AccountInfo { + balance: user_balance, + nonce: 0, + code_hash: revm::primitives::KECCAK_EMPTY, + code: None, + }, + ); + + // Give the precompile some initial balance for transfers + db.insert_account_info( + CUSTOM_PRECOMPILE_ADDRESS, + AccountInfo { + balance: U256::from(1000), // 1000 wei + nonce: 0, + code_hash: revm::primitives::KECCAK_EMPTY, + code: None, + }, + ); + + println!("✅ Custom EVM with journal-accessing precompiles created successfully!"); + println!("🔧 Precompile available at address: {CUSTOM_PRECOMPILE_ADDRESS}"); + println!("📝 Precompile supports:"); + println!(" - Read storage (empty input): Returns value from storage slot 0"); + println!(" - Write storage (32-byte input): Stores value and transfers 1 wei to caller"); + + // Create our custom EVM with mainnet handler + let context = Context::mainnet().with_db(db); + let mut evm = CustomEvm::new(context, NoOpInspector); + println!("\n=== Testing Custom Precompile ==="); + + // Test 1: Read initial storage value (should be 0) + println!("1. Reading initial storage value from custom precompile..."); + evm.0.ctx.set_tx( + TxEnv::builder() + .caller(user_address) + .kind(TxKind::Call(CUSTOM_PRECOMPILE_ADDRESS)) + .data(revm::primitives::Bytes::new()) // Empty data for read operation + .gas_limit(100_000) + .build() + .unwrap(), + ); + let read_result: Result<_, MyError> = MainnetHandler::default().run(&mut evm); + + match read_result { + Ok(revm::context::result::ExecutionResult::Success { + output, gas_used, .. + }) => { + println!(" ✓ Success! Gas used: {gas_used}"); + let data = output.data(); + let value = U256::from_be_slice(data); + println!(" 📖 Initial storage value: {value}"); + } + Ok(revm::context::result::ExecutionResult::Revert { output, gas_used }) => { + println!(" ❌ Reverted! Gas used: {gas_used}, Output: {output:?}"); + } + Ok(revm::context::result::ExecutionResult::Halt { reason, gas_used }) => { + println!(" 🛑 Halted! Reason: {reason:?}, Gas used: {gas_used}"); + } + Err(e) => { + println!(" ❌ Error: {e:?}"); + } + } + + // Test 2: Write value 42 to storage + println!("\n2. Writing value 42 to storage via custom precompile..."); + let storage_value = U256::from(42); + evm.0.ctx.set_tx( + TxEnv::builder() + .caller(user_address) + .kind(TxKind::Call(CUSTOM_PRECOMPILE_ADDRESS)) + .data(storage_value.to_be_bytes_vec().into()) + .gas_limit(100_000) + .nonce(1) + .build() + .unwrap(), + ); + let write_result: Result<_, MyError> = MainnetHandler::default().run(&mut evm); + + match write_result { + Ok(revm::context::result::ExecutionResult::Success { gas_used, .. }) => { + println!(" ✓ Success! Gas used: {gas_used}"); + println!(" 📝 Value 42 written to storage"); + println!(" 💰 1 wei transferred from precompile to caller as reward"); + } + Ok(revm::context::result::ExecutionResult::Revert { output, gas_used }) => { + println!(" ❌ Reverted! Gas used: {gas_used}, Output: {output:?}"); + } + Ok(revm::context::result::ExecutionResult::Halt { reason, gas_used }) => { + println!(" 🛑 Halted! Reason: {reason:?}, Gas used: {gas_used}"); + } + Err(e) => { + println!(" ❌ Error: {e:?}"); + } + } + + // Test 3: Read storage value again to verify the write + println!("\n3. Reading storage value again to verify the write..."); + evm.0.ctx.set_tx( + TxEnv::builder() + .caller(user_address) + .kind(TxKind::Call(CUSTOM_PRECOMPILE_ADDRESS)) + .data(revm::primitives::Bytes::new()) // Empty data for read operation + .gas_limit(100_000) + .nonce(2) + .build() + .unwrap(), + ); + let verify_result: Result<_, MyError> = MainnetHandler::default().run(&mut evm); + + match verify_result { + Ok(revm::context::result::ExecutionResult::Success { + output, gas_used, .. + }) => { + println!(" ✓ Success! Gas used: {gas_used}"); + let data = output.data(); + let value = U256::from_be_slice(data); + println!(" 📖 Final storage value: {value}"); + if value == U256::from(42) { + println!(" 🎉 Storage write was successful!"); + } else { + println!(" ⚠️ Unexpected value in storage"); + } + } + Ok(revm::context::result::ExecutionResult::Revert { output, gas_used }) => { + println!(" ❌ Reverted! Gas used: {gas_used}, Output: {output:?}"); + } + Ok(revm::context::result::ExecutionResult::Halt { reason, gas_used }) => { + println!(" 🛑 Halted! Reason: {reason:?}, Gas used: {gas_used}"); + } + Err(e) => { + println!(" ❌ Error: {e:?}"); + } + } + + // Check final account states + println!("\n=== Final Account States ==="); + let final_context_mut = &mut evm.0.ctx; + + let user_info = final_context_mut.db_mut().basic(user_address).unwrap(); + if let Some(user_account) = user_info { + println!("👤 User balance: {} wei", user_account.balance); + println!(" Received 1 wei reward from precompile!"); + } + + let precompile_info = final_context_mut + .db_mut() + .basic(CUSTOM_PRECOMPILE_ADDRESS) + .unwrap(); + if let Some(precompile_account) = precompile_info { + println!("🔧 Precompile balance: {} wei", precompile_account.balance); + } + + // Check storage directly from the journal using the storage API + println!("📦 Note: Storage state has been modified via journal operations"); + + println!("\n=== Summary ==="); + println!("✅ Custom EVM with journal-accessing precompiles working correctly!"); + println!("📝 Precompile successfully read and wrote storage"); + println!("💸 Balance transfer from precompile to caller executed"); + println!("🔍 All operations properly recorded in the journal"); + println!("🎯 Used default mainnet handler for transaction execution"); + + Ok(()) +} diff --git a/examples/custom_precompile_journal/src/precompile_provider.rs b/examples/custom_precompile_journal/src/precompile_provider.rs new file mode 100644 index 0000000000..79750ef3f3 --- /dev/null +++ b/examples/custom_precompile_journal/src/precompile_provider.rs @@ -0,0 +1,212 @@ +//! Custom precompile provider implementation. + +use revm::{ + context::Cfg, + context_interface::{ContextTr, JournalTr, LocalContextTr, Transaction}, + handler::{EthPrecompiles, PrecompileProvider}, + interpreter::{Gas, InputsImpl, InstructionResult, InterpreterResult}, + precompile::{PrecompileError, PrecompileOutput, PrecompileResult}, + primitives::{address, hardfork::SpecId, Address, Bytes, U256}, +}; +use std::boxed::Box; +use std::string::String; + +// Define our custom precompile address +pub const CUSTOM_PRECOMPILE_ADDRESS: Address = address!("0000000000000000000000000000000000000100"); + +// Custom storage key for our example +const STORAGE_KEY: U256 = U256::ZERO; + +/// Custom precompile provider that includes journal access functionality +#[derive(Debug, Clone)] +pub struct CustomPrecompileProvider { + inner: EthPrecompiles, + spec: SpecId, +} + +impl CustomPrecompileProvider { + pub fn new_with_spec(spec: SpecId) -> Self { + Self { + inner: EthPrecompiles::default(), + spec, + } + } +} + +impl PrecompileProvider for CustomPrecompileProvider +where + CTX: ContextTr>, +{ + type Output = InterpreterResult; + + fn set_spec(&mut self, spec: ::Spec) -> bool { + if spec == self.spec { + return false; + } + self.spec = spec; + // Create a new inner provider with the new spec + self.inner = EthPrecompiles::default(); + true + } + + fn run( + &mut self, + context: &mut CTX, + address: &Address, + inputs: &InputsImpl, + is_static: bool, + gas_limit: u64, + ) -> Result, String> { + // Check if this is our custom precompile + if *address == CUSTOM_PRECOMPILE_ADDRESS { + return Ok(Some(run_custom_precompile( + context, inputs, is_static, gas_limit, + )?)); + } + + // Otherwise, delegate to standard Ethereum precompiles + self.inner + .run(context, address, inputs, is_static, gas_limit) + } + + fn warm_addresses(&self) -> Box> { + // Include our custom precompile address along with standard ones + let mut addresses = vec![CUSTOM_PRECOMPILE_ADDRESS]; + addresses.extend(self.inner.warm_addresses()); + Box::new(addresses.into_iter()) + } + + fn contains(&self, address: &Address) -> bool { + *address == CUSTOM_PRECOMPILE_ADDRESS || self.inner.contains(address) + } +} + +/// Runs our custom precompile +fn run_custom_precompile( + context: &mut CTX, + inputs: &InputsImpl, + is_static: bool, + gas_limit: u64, +) -> Result { + let input_bytes = match &inputs.input { + revm::interpreter::CallInput::SharedBuffer(range) => { + if let Some(slice) = context.local().shared_memory_buffer_slice(range.clone()) { + slice.to_vec() + } else { + vec![] + } + } + revm::interpreter::CallInput::Bytes(bytes) => bytes.0.to_vec(), + }; + + // For this example, we'll implement a simple precompile that: + // - If called with empty data: reads a storage value + // - If called with 32 bytes: writes that value to storage and transfers 1 wei to the caller + + let result = if input_bytes.is_empty() { + // Read storage operation + handle_read_storage(context, gas_limit) + } else if input_bytes.len() == 32 { + if is_static { + return Err("Cannot modify state in static context".to_string()); + } + // Write storage operation + handle_write_storage(context, &input_bytes, gas_limit) + } else { + Err(PrecompileError::Other("Invalid input length".to_string())) + }; + + match result { + Ok(output) => { + let mut interpreter_result = InterpreterResult { + result: InstructionResult::Return, + gas: Gas::new(gas_limit), + output: output.bytes, + }; + let underflow = interpreter_result.gas.record_cost(output.gas_used); + if !underflow { + interpreter_result.result = InstructionResult::PrecompileOOG; + } + Ok(interpreter_result) + } + Err(e) => Ok(InterpreterResult { + result: if e.is_oog() { + InstructionResult::PrecompileOOG + } else { + InstructionResult::PrecompileError + }, + gas: Gas::new(gas_limit), + output: Bytes::new(), + }), + } +} + +/// Handles reading from storage +fn handle_read_storage(context: &mut CTX, gas_limit: u64) -> PrecompileResult { + // Base gas cost for reading storage + const BASE_GAS: u64 = 2_100; + + if gas_limit < BASE_GAS { + return Err(PrecompileError::OutOfGas); + } + + // Read from storage using the journal + let value = context + .journal_mut() + .sload(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY) + .map_err(|e| PrecompileError::Other(format!("Storage read failed: {e:?}")))? + .data; + + // Return the value as output + Ok(PrecompileOutput::new( + BASE_GAS, + value.to_be_bytes_vec().into(), + )) +} + +/// Handles writing to storage and transferring balance +fn handle_write_storage( + context: &mut CTX, + input: &[u8], + gas_limit: u64, +) -> PrecompileResult { + // Base gas cost for the operation + const BASE_GAS: u64 = 21_000; + const SSTORE_GAS: u64 = 20_000; + + if gas_limit < BASE_GAS + SSTORE_GAS { + return Err(PrecompileError::OutOfGas); + } + + // Parse the input as a U256 value + let value = U256::from_be_slice(input); + + // Store the value in the precompile's storage + context + .journal_mut() + .sstore(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY, value) + .map_err(|e| PrecompileError::Other(format!("Storage write failed: {e:?}")))?; + + // Get the caller address + let caller = context.tx().caller(); + + // Transfer 1 wei from the precompile to the caller as a reward + // First, ensure the precompile has balance + context + .journal_mut() + .balance_incr(CUSTOM_PRECOMPILE_ADDRESS, U256::from(1)) + .map_err(|e| PrecompileError::Other(format!("Balance increment failed: {e:?}")))?; + + // Then transfer to caller + let transfer_result = context + .journal_mut() + .transfer(CUSTOM_PRECOMPILE_ADDRESS, caller, U256::from(1)) + .map_err(|e| PrecompileError::Other(format!("Transfer failed: {e:?}")))?; + + if let Some(error) = transfer_result { + return Err(PrecompileError::Other(format!("Transfer error: {error:?}"))); + } + + // Return success with empty output + Ok(PrecompileOutput::new(BASE_GAS + SSTORE_GAS, Bytes::new())) +} diff --git a/examples/erc20_gas/src/handler.rs b/examples/erc20_gas/src/handler.rs index 9d70fa0ea0..c747319899 100644 --- a/examples/erc20_gas/src/handler.rs +++ b/examples/erc20_gas/src/handler.rs @@ -164,8 +164,7 @@ where effective_gas_price }; - let reward = - coinbase_gas_price.saturating_mul((gas.spent() - gas.refunded() as u64) as u128); + let reward = coinbase_gas_price.saturating_mul(gas.used() as u128); token_operation::(context, TREASURY, beneficiary, U256::from(reward))?; Ok(()) diff --git a/examples/erc20_gas/src/main.rs b/examples/erc20_gas/src/main.rs index 98ad133a58..58e2187e7c 100644 --- a/examples/erc20_gas/src/main.rs +++ b/examples/erc20_gas/src/main.rs @@ -9,6 +9,7 @@ use alloy_sol_types::SolValue; use anyhow::Result; use exec::transact_erc20evm_commit; use revm::{ + context::TxEnv, context_interface::{ result::{InvalidHeader, InvalidTransaction}, ContextTr, JournalTr, @@ -134,12 +135,15 @@ fn transfer(from: Address, to: Address, amount: U256, cache_db: &mut AlloyCacheD .modify_cfg_chained(|cfg| { cfg.spec = SpecId::CANCUN; }) - .modify_tx_chained(|tx| { - tx.caller = from; - tx.kind = TxKind::Call(to); - tx.value = amount; - tx.gas_price = 2; - }) + .with_tx( + TxEnv::builder() + .caller(from) + .kind(TxKind::Call(to)) + .value(amount) + .gas_price(2) + .build() + .unwrap(), + ) .modify_block_chained(|b| { b.basefee = 1; }) diff --git a/examples/uniswap_get_reserves/src/main.rs b/examples/uniswap_get_reserves/src/main.rs index 245c64b5be..4b4f9f2a4c 100644 --- a/examples/uniswap_get_reserves/src/main.rs +++ b/examples/uniswap_get_reserves/src/main.rs @@ -70,18 +70,20 @@ async fn main() -> anyhow::Result<()> { // Execute transaction without writing to the DB let result = evm - .transact_one(TxEnv { - // fill in missing bits of env struct - // change that to whatever caller you want to be - caller: address!("0000000000000000000000000000000000000000"), - // account you want to transact with - kind: TxKind::Call(pool_address), - // calldata formed via abigen - data: encoded.into(), - // transaction value in wei - value: U256::from(0), - ..Default::default() - }) + .transact_one( + TxEnv::builder() + // fill in missing bits of env struct + // change that to whatever caller you want to be + .caller(address!("0000000000000000000000000000000000000000")) + // account you want to transact with + .kind(TxKind::Call(pool_address)) + // calldata formed via abigen + .data(encoded.into()) + // transaction value in wei + .value(U256::from(0)) + .build() + .unwrap(), + ) .unwrap(); // Unpack output call enum into raw bytes diff --git a/examples/uniswap_v2_usdc_swap/src/main.rs b/examples/uniswap_v2_usdc_swap/src/main.rs index 817a9bb88f..4b374ab537 100644 --- a/examples/uniswap_v2_usdc_swap/src/main.rs +++ b/examples/uniswap_v2_usdc_swap/src/main.rs @@ -95,14 +95,15 @@ fn balance_of(token: Address, address: Address, alloy_db: &mut AlloyCacheDB) -> let mut evm = Context::mainnet().with_db(alloy_db).build_mainnet(); let result = evm - .transact_one(TxEnv { - // 0x1 because calling USDC proxy from zero address fails - caller: address!("0000000000000000000000000000000000000001"), - kind: TxKind::Call(token), - data: encoded.into(), - value: U256::from(0), - ..Default::default() - }) + .transact_one( + TxEnv::builder() + .caller(address!("0000000000000000000000000000000000000001")) + .kind(TxKind::Call(token)) + .data(encoded.into()) + .value(U256::from(0)) + .build() + .unwrap(), + ) .unwrap(); let value = match result { @@ -139,13 +140,15 @@ async fn get_amount_out( let mut evm = Context::mainnet().with_db(cache_db).build_mainnet(); let result = evm - .transact_one(TxEnv { - caller: address!("0000000000000000000000000000000000000000"), - kind: TxKind::Call(uniswap_v2_router), - data: encoded.into(), - value: U256::from(0), - ..Default::default() - }) + .transact_one( + TxEnv::builder() + .caller(address!("0000000000000000000000000000000000000000")) + .kind(TxKind::Call(uniswap_v2_router)) + .data(encoded.into()) + .value(U256::from(0)) + .build() + .unwrap(), + ) .unwrap(); let value = match result { @@ -171,13 +174,15 @@ fn get_reserves(pair_address: Address, cache_db: &mut AlloyCacheDB) -> Result<(U let mut evm = Context::mainnet().with_db(cache_db).build_mainnet(); let result = evm - .transact_one(TxEnv { - caller: address!("0000000000000000000000000000000000000000"), - kind: TxKind::Call(pair_address), - data: encoded.into(), - value: U256::from(0), - ..Default::default() - }) + .transact_one( + TxEnv::builder() + .caller(address!("0000000000000000000000000000000000000000")) + .kind(TxKind::Call(pair_address)) + .data(encoded.into()) + .value(U256::from(0)) + .build() + .unwrap(), + ) .unwrap(); let value = match result { @@ -218,14 +223,14 @@ fn swap( let mut evm = Context::mainnet().with_db(cache_db).build_mainnet(); - let tx = TxEnv { - caller: from, - kind: TxKind::Call(pool_address), - data: encoded.into(), - value: U256::from(0), - nonce: 1, - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(from) + .kind(TxKind::Call(pool_address)) + .data(encoded.into()) + .value(U256::from(0)) + .nonce(1) + .build() + .unwrap(); let ref_tx = evm.transact_commit(tx).unwrap(); @@ -252,13 +257,13 @@ fn transfer( let mut evm = Context::mainnet().with_db(cache_db).build_mainnet(); - let tx = TxEnv { - caller: from, - kind: TxKind::Call(token), - data: encoded.into(), - value: U256::from(0), - ..Default::default() - }; + let tx = TxEnv::builder() + .caller(from) + .kind(TxKind::Call(token)) + .data(encoded.into()) + .value(U256::from(0)) + .build() + .unwrap(); let ref_tx = evm.transact_commit(tx).unwrap(); let success: bool = match ref_tx {