From 504717be0196b7e2f3270d38f03b49d66617f9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 5 Nov 2026 17:19:23 +0100 Subject: [PATCH 1/4] Follow Osaka rules for legacy EVM transactions Allow unprotected legacy transactions while retaining the chain-ID check for transactions that provide one. Arachnid's presigned deployment then reaches the normal base-fee rejection without changing validation order. Remove the executor's duplicate chain-ID checks and preserve the original optional chain ID in revm. Add specific binary-port rejection codes so clients can distinguish fee shortfalls and missing required chain IDs. --- binary_port/src/error_code.rs | 40 +++++++++++++ executor/evm/src/error.rs | 11 ---- executor/evm/src/executor.rs | 22 -------- executor/evm/src/tx.rs | 2 +- executor/evm/tests/executor.rs | 54 +++++++++++++----- .../components/contract_runtime/operations.rs | 4 -- .../src/types/transaction/meta_transaction.rs | 56 ++++++++++--------- .../transaction/meta_transaction/meta_evm.rs | 12 ++-- 8 files changed, 117 insertions(+), 84 deletions(-) diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index 4cf649b42a..6e4f1e8b19 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -397,6 +397,12 @@ pub enum ErrorCode { /// EVM transaction nonce is further ahead of the account nonce than the chainspec allows. #[error("the EVM transaction nonce is too far ahead of the account nonce")] InvalidTransactionEvmNonceTooFarAhead = 126, + /// EVM transaction gas price is below the configured base fee. + #[error("the EVM transaction gas price is below the base fee")] + InvalidTransactionEvmGasPriceBelowBaseFee = 127, + /// EVM transaction does not include a chain ID. + #[error("the EVM transaction is missing a chain ID")] + InvalidTransactionEvmMissingChainId = 128, } impl TryFrom for ErrorCode { @@ -430,6 +436,12 @@ impl From for ErrorCode { InvalidTransaction::Evm(EvmTransactionError::NonceTooFarAhead { .. }) => { ErrorCode::InvalidTransactionEvmNonceTooFarAhead } + InvalidTransaction::Evm(EvmTransactionError::GasPriceBelowBaseFee { .. }) => { + ErrorCode::InvalidTransactionEvmGasPriceBelowBaseFee + } + InvalidTransaction::Evm(EvmTransactionError::MissingChainId) => { + ErrorCode::InvalidTransactionEvmMissingChainId + } InvalidTransaction::Evm(EvmTransactionError::PositiveEffectivePriorityFeePerGas { .. }) => ErrorCode::InvalidTransactionEvmPositiveEffectivePriorityFeePerGas, @@ -637,6 +649,34 @@ mod tests { }; use strum::IntoEnumIterator; + #[test] + fn evm_missing_chain_id_has_specific_error_code() { + assert_eq!( + ErrorCode::from(InvalidTransaction::Evm(EvmTransactionError::MissingChainId)), + ErrorCode::InvalidTransactionEvmMissingChainId + ); + assert_eq!( + ErrorCode::try_from(128u16).unwrap(), + ErrorCode::InvalidTransactionEvmMissingChainId + ); + } + + #[test] + fn evm_gas_price_below_base_fee_has_specific_error_code() { + let error = InvalidTransaction::Evm(EvmTransactionError::GasPriceBelowBaseFee { + gas_price: 100, + base_fee: 5_000, + }); + assert_eq!( + ErrorCode::from(error), + ErrorCode::InvalidTransactionEvmGasPriceBelowBaseFee + ); + assert_eq!( + ErrorCode::try_from(127u16).unwrap(), + ErrorCode::InvalidTransactionEvmGasPriceBelowBaseFee + ); + } + #[test] fn osaka_transaction_cap_error_code_is_appended() { let error = diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs index c9d49fe34e..40a12f994f 100644 --- a/executor/evm/src/error.rs +++ b/executor/evm/src/error.rs @@ -17,17 +17,6 @@ pub enum Error { /// EVM wei-to-mote conversion ratio is invalid. #[error("EVM wei_per_mote must be greater than zero")] InvalidWeiPerMote, - /// Signed EVM transaction does not include an EIP-155 replay-protection chain id. - #[error("EVM transaction is missing replay-protection chain id")] - MissingChainId, - /// Transaction chain id does not match the executor configuration. - #[error("EVM transaction chain id {actual} does not match configured chain id {expected}")] - ChainIdMismatch { - /// Chain id configured in the chainspec. - expected: u64, - /// Chain id recovered from the signed transaction. - actual: u64, - }, /// Failed to read from the Casper tracking copy as a revm database. #[error(transparent)] Database(#[from] DbError), diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index 77c30615b5..8d45e718fd 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -67,16 +67,6 @@ impl EvmExecutor { return Err(Error::InvalidWeiPerMote); } - let Some(actual) = transaction.chain_id() else { - return Err(Error::MissingChainId); - }; - if actual != self.config.chain_id { - return Err(Error::ChainIdMismatch { - expected: self.config.chain_id, - actual, - }); - } - let kind = ExecuteKind::Transaction(Box::new(transaction.clone())); let tx_env = tx::build_tx_env(&self.config, &kind)?; let block = block_context.to_revm_block(&self.config)?; @@ -127,18 +117,6 @@ impl EvmExecutor { return Err(Error::InvalidWeiPerMote); } - if let ExecuteKind::Transaction(transaction) = &request.kind { - let Some(actual) = transaction.chain_id() else { - return Err(Error::MissingChainId); - }; - if actual != self.config.chain_id { - return Err(Error::ChainIdMismatch { - expected: self.config.chain_id, - actual, - }); - } - } - let tx_env = tx::build_tx_env(&self.config, &request.kind)?; let block = request.block.to_revm_block(&self.config)?; let execution_mode = match &request.kind { diff --git a/executor/evm/src/tx.rs b/executor/evm/src/tx.rs index 973322b100..4e9fc8d42b 100644 --- a/executor/evm/src/tx.rs +++ b/executor/evm/src/tx.rs @@ -22,7 +22,7 @@ pub(crate) fn build_tx_env(config: &EvmConfig, kind: &ExecuteKind) -> Result return Ok(None), Err(EvmExecutorError::Disabled) => CasperEvmTransactionError::Disabled, - Err(EvmExecutorError::MissingChainId) => CasperEvmTransactionError::MissingChainId, - Err(EvmExecutorError::ChainIdMismatch { expected, actual }) => { - CasperEvmTransactionError::ChainIdMismatch { expected, actual } - } Err(EvmExecutorError::InvalidTransaction(error)) => error, Err(EvmExecutorError::Transaction(error)) => CasperEvmTransactionError::Validation(error), Err(error) => { diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 71431ce9cb..a313884b60 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -655,45 +655,47 @@ mod tests { } #[test] - fn evm_config_compliance_rejects_missing_chain_id() { + fn evm_config_compliance_accepts_unprotected_legacy_transaction() { let chainspec = chainspec(); let meta = evm_meta(&chainspec, legacy_transaction(None, BASE_FEE_WEI, 21_000)); - assert!(matches!( - meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), - Err(InvalidTransaction::Evm(EvmTransactionError::MissingChainId)) - )); + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()) + .expect("Osaka permits unprotected legacy transactions"); } #[test] fn evm_config_compliance_rejects_mismatched_chain_id() { let chainspec = chainspec(); - let meta = evm_meta( - &chainspec, - legacy_transaction(Some(CHAIN_ID + 1), BASE_FEE_WEI, 21_000), - ); - assert!(matches!( - meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), - Err(InvalidTransaction::Evm(EvmTransactionError::ChainIdMismatch { - expected: CHAIN_ID, - actual - })) if actual == CHAIN_ID + 1 - )); + for gas_price in [BASE_FEE_WEI, BASE_FEE_WEI - 1] { + let meta = evm_meta( + &chainspec, + legacy_transaction(Some(CHAIN_ID + 1), gas_price, 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(EvmTransactionError::ChainIdMismatch { + expected: CHAIN_ID, + actual + })) if actual == CHAIN_ID + 1 + )); + } } #[test] fn evm_config_compliance_rejects_gas_price_below_base_fee() { let chainspec = chainspec(); - let meta = evm_meta( - &chainspec, - legacy_transaction(Some(CHAIN_ID), BASE_FEE_WEI - 1, 21_000), - ); - assert!(matches!( - meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), - Err(InvalidTransaction::Evm(EvmTransactionError::GasPriceBelowBaseFee { - gas_price, - base_fee - })) if gas_price == BASE_FEE_WEI - 1 && base_fee == BASE_FEE_WEI - )); + for chain_id in [Some(CHAIN_ID), None] { + let meta = evm_meta( + &chainspec, + legacy_transaction(chain_id, BASE_FEE_WEI - 1, 21_000), + ); + assert!(matches!( + meta.is_config_compliant(&chainspec, TimeDiff::from_seconds(0), Timestamp::zero()), + Err(InvalidTransaction::Evm(EvmTransactionError::GasPriceBelowBaseFee { + gas_price, + base_fee + })) if gas_price == BASE_FEE_WEI - 1 && base_fee == BASE_FEE_WEI + )); + } } #[test] diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index ae4f672fb2..f239c9a75c 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -105,11 +105,13 @@ impl MetaEvmTransaction { } let expected = evm_config.chain_id; - let actual = transaction - .chain_id() - .ok_or(EvmTransactionError::MissingChainId)?; - if actual != expected { - return Err(EvmTransactionError::ChainIdMismatch { expected, actual }); + // Follow Osaka's chain-ID semantics: unprotected legacy transactions may omit the ID; + // when present, it must match the network. Typed envelopes require it during verification. + // https://ethereum.github.io/execution-specs/src/ethereum/forks/osaka/fork.py.html#check_transaction + if let Some(actual) = transaction.chain_id() { + if actual != expected { + return Err(EvmTransactionError::ChainIdMismatch { expected, actual }); + } } let gas_limit = transaction.gas_limit(); From 79ac63b570792ead0b7722e92d192b3a638753ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 5 Nov 2026 17:21:16 +0100 Subject: [PATCH 2/4] Share EVM setup for validation and execution Build the transaction environment, database, configuration and precompiles in one place so validation and execution use the same setup. Accept ExecuteRequest in both entry points while keeping their handler calls and state handling separate. --- executor/evm/src/executor.rs | 105 +++++++++--------- executor/evm/src/request.rs | 2 +- executor/evm/tests/executor.rs | 24 ++-- .../components/contract_runtime/operations.rs | 6 +- 4 files changed, 67 insertions(+), 70 deletions(-) diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index 8d45e718fd..731d27c0bd 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -7,18 +7,19 @@ use casper_storage::{ global_state::{error::Error as GlobalStateError, state::StateReader}, TrackingCopy, }; -use casper_types::{EvmConfig, EvmSpec, EvmTransaction, EvmTransactionError, Key, StoredValue}; +use casper_types::{EvmConfig, EvmSpec, EvmTransactionError, Key, StoredValue}; use revm::{ - context::CfgEnv, + context::{CfgEnv, Evm}, context_interface::result::{EVMError, InvalidTransaction as RevmInvalidTransaction}, - handler::{Handler, MainnetHandler}, + handler::{instructions::EthInstructions, EthFrame, Handler, MainnetContext, MainnetHandler}, + interpreter::interpreter::EthInterpreter, primitives::{hardfork::SpecId, Bytes}, Context, ExecuteEvm, MainBuilder, MainContext, SystemCallEvm, }; use crate::{ - db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, BlockContext, DbError, Error, - ExecuteKind, ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest, + db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, DbError, Error, ExecuteKind, + ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -38,6 +39,14 @@ enum EvmExecutionMode { SystemCall, } +type CasperEvm = Evm< + CTX, + (), + EthInstructions, + CasperEvmPrecompiles, + EthFrame, +>; + impl EvmExecutor { /// Creates a new executor from chainspec EVM configuration. pub fn new(config: EvmConfig) -> Self { @@ -49,37 +58,17 @@ impl EvmExecutor { &self.config } - /// Validates an EVM transaction without executing it or applying state changes. + /// Validates an EVM transaction or call without executing it or applying state changes. pub fn validate_transaction( &self, data_access_layer: &DataAccessLayer, tracking_copy: &mut TrackingCopy, - block_context: BlockContext, - transaction: &EvmTransaction, + request: ExecuteRequest, ) -> Result<()> where R: StateReader, { - if !self.config.enabled { - return Err(Error::Disabled); - } - if self.config.wei_per_mote == 0 { - return Err(Error::InvalidWeiPerMote); - } - - let kind = ExecuteKind::Transaction(Box::new(transaction.clone())); - let tx_env = tx::build_tx_env(&self.config, &kind)?; - let block = block_context.to_revm_block(&self.config)?; - let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote); - let mut evm = Context::mainnet() - .with_db(db) - .with_block(block) - .with_tx(tx_env) - .modify_cfg_chained(|cfg| { - configure_evm_cfg(cfg, &self.config, EvmExecutionMode::Checked); - }) - .build_mainnet() - .with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec))); + let mut evm = self.prepare_evm(data_access_layer, tracking_copy, &request)?; // revm's validate() includes the caller state phase. Running that phase // again would bump a call transaction's nonce twice in this journal. @@ -107,6 +96,32 @@ impl EvmExecutor { tracking_copy: &mut TrackingCopy, request: ExecuteRequest, ) -> Result + where + R: StateReader, + { + let result_and_state = { + let mut evm = self.prepare_evm(data_access_layer, tracking_copy, &request)?; + evm.replay().map_err(map_revm_error)? + }; + + let balance_losses = state::apply( + tracking_copy, + result_and_state.state, + self.config.wei_per_mote, + )?; + Ok(ExecutionOutcome::from_revm_result( + &result_and_state.result, + balance_losses, + self.config.wei_per_mote, + )) + } + + fn prepare_evm<'a, R, S>( + &self, + data_access_layer: &'a DataAccessLayer, + tracking_copy: &'a mut TrackingCopy, + request: &ExecuteRequest, + ) -> Result>>> where R: StateReader, { @@ -127,30 +142,16 @@ impl EvmExecutor { ExecuteKind::Call(_) => EvmExecutionMode::Checked, }; - let result_and_state = { - let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote); - let mut evm = Context::mainnet() - .with_db(db) - .with_block(block) - .modify_cfg_chained(|cfg| { - configure_evm_cfg(cfg, &self.config, execution_mode); - }) - .build_mainnet() - .with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec))); - - evm.transact(tx_env).map_err(map_revm_error)? - }; - - let balance_losses = state::apply( - tracking_copy, - result_and_state.state, - self.config.wei_per_mote, - )?; - Ok(ExecutionOutcome::from_revm_result( - &result_and_state.result, - balance_losses, - self.config.wei_per_mote, - )) + let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote); + Ok(Context::mainnet() + .with_db(db) + .with_block(block) + .with_tx(tx_env) + .modify_cfg_chained(|cfg| { + configure_evm_cfg(cfg, &self.config, execution_mode); + }) + .build_mainnet() + .with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec)))) } /// Executes a system call against the supplied tracking copy. diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index 996fb0e3d5..cab69a44f1 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -4,7 +4,7 @@ use casper_types::{evm, EvmConfig, EvmTransaction, U256}; use crate::{tx, Error}; -/// Request passed to [`crate::EvmExecutor::execute`]. +/// Request passed to [`crate::EvmExecutor::validate_transaction`] or [`crate::EvmExecutor::execute`]. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ExecuteRequest { /// Block context available to EVM opcodes and validation. diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 15d142b121..a98410ccf9 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -3424,19 +3424,14 @@ fn signed_transactions_allow_unprotected_legacy_envelopes() { transaction.from(), U512::from(1_000_000u64), ); - executor - .validate_transaction( - &data_access_layer, - &mut tracking_copy, - block(), - &transaction, - ) - .expect("Osaka permits unprotected legacy transactions"); - assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 0); let request = ExecuteRequest { block: block(), kind: ExecuteKind::Transaction(Box::new(transaction.clone())), }; + executor + .validate_transaction(&data_access_layer, &mut tracking_copy, request.clone()) + .expect("Osaka permits unprotected legacy transactions"); + assert_eq!(read_evm_nonce(&mut tracking_copy, transaction.from()), 0); let outcome = executor .execute(&data_access_layer, &mut tracking_copy, request) .expect("unprotected legacy transaction should execute"); @@ -3459,20 +3454,19 @@ fn signed_transactions_reject_mismatched_chain_id() { preinstalls: Default::default(), }); let transaction = legacy_transaction(Some(7)); + let request = ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(Box::new(transaction)), + }; assert!(matches!( wrong_chain_executor.validate_transaction( &data_access_layer, &mut tracking_copy, - block(), - &transaction, + request.clone(), ), Err(Error::InvalidTransaction(EvmTransactionError::Validation(message))) if message == "invalid chain ID" )); - let request = ExecuteRequest { - block: block(), - kind: ExecuteKind::Transaction(Box::new(transaction)), - }; assert!(matches!( wrong_chain_executor.execute(&data_access_layer, &mut tracking_copy, request), Err(Error::Revm(message)) if message.contains("invalid chain ID") diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index c3c8cab576..4e91682423 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -189,8 +189,10 @@ where let result = EvmExecutor::new(chainspec.evm_config.clone()).validate_transaction( data_access_layer, tracking_copy, - block_context, - evm_transaction, + EvmExecuteRequest { + block: block_context, + kind: EvmExecuteKind::Transaction(Box::new(evm_transaction.clone())), + }, ); let invalid_request = match result { Ok(()) => return Ok(None), From 6099ae527b30eafefbeb28925bd1ad5ba3c688aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 5 Nov 2026 17:21:48 +0100 Subject: [PATCH 3/4] Remove unused EVM APIs and error variants Remove the system-call entry point and request type, which have no production callers. Drop their dedicated execution mode and tests, reuse CallValidation for the remaining paths, and remove the unused InconsistentEnvelope error variant. --- executor/evm/src/executor.rs | 86 +++++----------------------------- executor/evm/src/lib.rs | 4 +- executor/evm/src/request.rs | 15 +----- executor/evm/tests/executor.rs | 57 +--------------------- types/src/evm/transaction.rs | 5 -- 5 files changed, 16 insertions(+), 151 deletions(-) diff --git a/executor/evm/src/executor.rs b/executor/evm/src/executor.rs index 731d27c0bd..2af0959dae 100644 --- a/executor/evm/src/executor.rs +++ b/executor/evm/src/executor.rs @@ -13,13 +13,13 @@ use revm::{ context_interface::result::{EVMError, InvalidTransaction as RevmInvalidTransaction}, handler::{instructions::EthInstructions, EthFrame, Handler, MainnetContext, MainnetHandler}, interpreter::interpreter::EthInterpreter, - primitives::{hardfork::SpecId, Bytes}, - Context, ExecuteEvm, MainBuilder, MainContext, SystemCallEvm, + primitives::hardfork::SpecId, + Context, ExecuteEvm, MainBuilder, MainContext, }; use crate::{ - db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, DbError, Error, ExecuteKind, - ExecuteRequest, ExecutionOutcome, Result, SystemCallRequest, + db::CasperDb, precompiles::CasperEvmPrecompiles, state, tx, CallValidation, DbError, Error, + ExecuteKind, ExecuteRequest, ExecutionOutcome, Result, }; /// Executes EVM transactions and calls against a Casper tracking copy. @@ -32,13 +32,6 @@ pub struct EvmExecutor { config: EvmConfig, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum EvmExecutionMode { - Checked, - UncheckedSimulation, - SystemCall, -} - type CasperEvm = Evm< CTX, (), @@ -134,88 +127,33 @@ impl EvmExecutor { let tx_env = tx::build_tx_env(&self.config, &request.kind)?; let block = request.block.to_revm_block(&self.config)?; - let execution_mode = match &request.kind { - ExecuteKind::Transaction(_) => EvmExecutionMode::Checked, - ExecuteKind::Call(call) if call.validation.is_unchecked_simulation() => { - EvmExecutionMode::UncheckedSimulation - } - ExecuteKind::Call(_) => EvmExecutionMode::Checked, + let validation = match &request.kind { + ExecuteKind::Transaction(_) => CallValidation::Checked, + ExecuteKind::Call(call) => call.validation, }; - let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote); Ok(Context::mainnet() .with_db(db) .with_block(block) .with_tx(tx_env) .modify_cfg_chained(|cfg| { - configure_evm_cfg(cfg, &self.config, execution_mode); + configure_evm_cfg(cfg, &self.config, validation); }) .build_mainnet() .with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec)))) } - - /// Executes a system call against the supplied tracking copy. - pub fn execute_system_call( - &self, - data_access_layer: &DataAccessLayer, - tracking_copy: &mut TrackingCopy, - request: SystemCallRequest, - ) -> Result - where - R: StateReader, - { - if !self.config.enabled { - return Err(Error::Disabled); - } - if self.config.wei_per_mote == 0 { - return Err(Error::InvalidWeiPerMote); - } - - let block = request.block.to_revm_block(&self.config)?; - let result_and_state = { - let db = CasperDb::new(data_access_layer, tracking_copy, self.config.wei_per_mote); - let mut evm = Context::mainnet() - .with_db(db) - .with_block(block) - .modify_cfg_chained(|cfg| { - configure_evm_cfg(cfg, &self.config, EvmExecutionMode::SystemCall); - }) - .build_mainnet() - .with_precompiles(CasperEvmPrecompiles::new(spec_id(self.config.spec))); - - evm.system_call( - tx::to_revm_address(request.target), - Bytes::from(request.input), - ) - .map_err(map_revm_error)? - }; - - let balance_losses = state::apply( - tracking_copy, - result_and_state.state, - self.config.wei_per_mote, - )?; - Ok(ExecutionOutcome::from_revm_result( - &result_and_state.result, - balance_losses, - self.config.wei_per_mote, - )) - } } -fn configure_evm_cfg(cfg: &mut CfgEnv, config: &EvmConfig, execution_mode: EvmExecutionMode) { - let skip_validation = matches!( - execution_mode, - EvmExecutionMode::UncheckedSimulation | EvmExecutionMode::SystemCall - ); +fn configure_evm_cfg(cfg: &mut CfgEnv, config: &EvmConfig, validation: CallValidation) { + let skip_validation = validation.is_unchecked_simulation(); cfg.spec = spec_id(config.spec); cfg.chain_id = config.chain_id; - cfg.tx_chain_id_check = matches!(execution_mode, EvmExecutionMode::Checked); + cfg.tx_chain_id_check = !skip_validation; // Read-only simulations may consume the block budget, following eth_call. // All checked execution retains revm's Osaka transaction cap. cfg.tx_gas_limit_cap = skip_validation.then_some(u64::MAX); - cfg.disable_block_gas_limit = matches!(execution_mode, EvmExecutionMode::SystemCall); + cfg.disable_block_gas_limit = false; cfg.disable_base_fee = skip_validation; cfg.disable_balance_check = skip_validation; cfg.disable_nonce_check = skip_validation; diff --git a/executor/evm/src/lib.rs b/executor/evm/src/lib.rs index 43c3dee49b..da5e8e9282 100644 --- a/executor/evm/src/lib.rs +++ b/executor/evm/src/lib.rs @@ -16,9 +16,7 @@ mod tx; pub use error::{DbError, Error, Result}; pub use executor::EvmExecutor; pub use outcome::{ExecutionOutcome, ExecutionStatus}; -pub use request::{ - BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest, SystemCallRequest, -}; +pub use request::{BlockContext, CallRequest, CallValidation, ExecuteKind, ExecuteRequest}; use casper_types::evm; diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index cab69a44f1..cd3bb10147 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -13,17 +13,6 @@ pub struct ExecuteRequest { pub kind: ExecuteKind, } -/// Request passed to [`crate::EvmExecutor::execute_system_call`]. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SystemCallRequest { - /// Block context available to EVM opcodes. - pub block: BlockContext, - /// System contract address. - pub target: evm::Address, - /// System call input bytes. - pub input: Vec, -} - /// EVM work item to execute. #[derive(Clone, Debug, Eq, PartialEq)] pub enum ExecuteKind { @@ -35,8 +24,8 @@ pub enum ExecuteKind { /// Unsigned EVM call request. /// -/// Calls are useful for views, simulations, tests, and controlled system -/// execution. They still write effects into the supplied tracking copy, so +/// Calls are useful for views, simulations, and tests. +/// They still write effects into the supplied tracking copy, so /// callers should pass a fork when they want to discard the result. #[derive(Clone, Debug, Eq, PartialEq)] pub struct CallRequest { diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index a98410ccf9..796402ecec 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -13,7 +13,7 @@ use alloy_eips::{ use alloy_primitives::{keccak256, Address as AlloyAddress, Signature, TxKind, B256, U256}; use casper_executor_evm::{ BlockContext, CallRequest, CallValidation, DbError, Error, EvmExecutor, ExecuteKind, - ExecuteRequest, ExecutionStatus, SystemCallRequest, BLOCK_HASH_HISTORY, EMPTY_CODE_HASH, + ExecuteRequest, ExecutionStatus, BLOCK_HASH_HISTORY, EMPTY_CODE_HASH, }; use casper_storage::{ block_store::{lmdb::LmdbBlockStore, BlockStoreTransaction}, @@ -1199,36 +1199,6 @@ fn missing_bytecode_rejects_delegated_call() { )); } -#[test] -fn missing_bytecode_rejects_system_call() { - let executor = executor(EvmSpec::Osaka); - let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); - let target = evm::Address::new([0xa1; 20]); - seed_evm_code(&mut tracking_copy, target, reverting_runtime()); - let missing_key = Key::Evm(EvmAddr::ByteCode(read_code_hash( - &mut tracking_copy, - target, - ))); - tracking_copy.prune(missing_key); - - let error = executor - .execute_system_call( - &data_access_layer, - &mut tracking_copy, - SystemCallRequest { - block: block(), - target, - input: Vec::new(), - }, - ) - .expect_err("missing system-call bytecode must abort execution"); - - assert!(matches!( - error, - Error::Database(DbError::MissingBytecode { key }) if *key == missing_key - )); -} - #[test] fn osaka_bls12_g1_add_precompile_delegates_to_revm() { let executor = executor(EvmSpec::Osaka); @@ -2843,31 +2813,6 @@ fn invalid_wei_per_mote_is_rejected() { )); } -#[test] -fn system_call_reports_zero_dust_for_whole_mote_state() { - let executor = executor(EvmSpec::Osaka); - let target = evm::Address::new([0x3b; 20]); - let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); - seed_evm_balance(&mut tracking_copy, target, U512::one()); - seed_evm_code(&mut tracking_copy, target, vec![opcode::STOP]); - - let outcome = executor - .execute_system_call( - &data_access_layer, - &mut tracking_copy, - SystemCallRequest { - block: block(), - target, - input: Vec::new(), - }, - ) - .expect("system call should execute"); - - assert_eq!(outcome.status, ExecutionStatus::Success); - assert_eq!(outcome.supply_reduction_motes().unwrap(), U512::zero()); - assert_eq!(read_balance(&mut tracking_copy, target), U512::one()); -} - #[test] fn coinbase_transfer_to_prelinked_beneficiary_credits_proposer_account() { let executor = executor(EvmSpec::Osaka); diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index d710f41f58..1fa2bc13b8 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -628,8 +628,6 @@ pub enum EvmTransactionError { HashMismatch, /// The sender address could not be recovered from the signature. SenderRecovery(String), - /// Reconstructing the signed envelope produced metadata different from this transaction. - InconsistentEnvelope, } impl Display for EvmTransactionError { @@ -780,9 +778,6 @@ impl Display for EvmTransactionError { EvmTransactionError::SenderRecovery(error) => { write!(formatter, "EVM transaction sender recovery error: {error}") } - EvmTransactionError::InconsistentEnvelope => { - formatter.write_str("EVM transaction fields do not match signed envelope") - } } } } From 55a213a9e0eeb8d2698939eca58338cde7f2d729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Thu, 5 Nov 2026 18:19:44 +0100 Subject: [PATCH 4/4] Avoid missing block heights in reactor test waits Accept validators that have completed the target block even after they advance past it. Requiring every node to report exactly the same height can miss the target and time out in the fee distribution test. Add a regression test for waiting on a block the nodes have passed. --- node/src/reactor/main_reactor/tests/fixture.rs | 8 ++++---- .../reactor/main_reactor/tests/network_general.rs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/node/src/reactor/main_reactor/tests/fixture.rs b/node/src/reactor/main_reactor/tests/fixture.rs index dbebfc4fbb..0dc4a364ca 100644 --- a/node/src/reactor/main_reactor/tests/fixture.rs +++ b/node/src/reactor/main_reactor/tests/fixture.rs @@ -616,7 +616,7 @@ impl TestFixture { .await } - /// Runs the network until all nodes reach the given completed block height. + /// Runs the network until all nodes reach at least the given completed block height. /// /// Returns an error if the condition isn't met in time. pub(crate) async fn try_run_until_block_height( @@ -627,13 +627,13 @@ impl TestFixture { self.try_run_until( move |nodes: &Nodes| { nodes.values().all(|runner| { + // One node can pass the target before the others reach it. runner .main_reactor() .storage() .get_highest_complete_block() .expect("should not error reading db") - .map(|block| block.height()) - == Some(block_height) + .is_some_and(|block| block.height() >= block_height) }) }, within, @@ -641,7 +641,7 @@ impl TestFixture { .await } - /// Runs the network until all nodes reach the given completed block height. + /// Runs the network until all nodes reach at least the given completed block height. /// /// Panics if the condition isn't met in time. pub(crate) async fn run_until_block_height(&mut self, block_height: u64, within: Duration) { diff --git a/node/src/reactor/main_reactor/tests/network_general.rs b/node/src/reactor/main_reactor/tests/network_general.rs index b4473da8e4..d89ee96c14 100644 --- a/node/src/reactor/main_reactor/tests/network_general.rs +++ b/node/src/reactor/main_reactor/tests/network_general.rs @@ -51,6 +51,21 @@ async fn run_network() { fixture.run_until_consensus_in_era(ERA_TWO, ONE_MIN).await; } +#[tokio::test] +async fn block_height_wait_accepts_already_completed_blocks() { + let initial_stakes = InitialStakes::AllEqual { + count: 2, + stake: 100, + }; + let mut fixture = TestFixture::new(initial_stakes, None).await; + fixture.run_until_block_height(1, ONE_MIN).await; + + fixture + .try_run_until_block_height(0, Duration::from_millis(100)) + .await + .expect("nodes past block 0 have already completed it"); +} + #[tokio::test] async fn historical_sync_with_era_height_1() { let initial_stakes = InitialStakes::Random { count: 5 };