Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions crates/context/interface/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,54 @@ pub enum OutOfGasError {
ReentrancySentry,
}

/// Error that includes transaction index for batch transaction processing.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransactionIndexedError<Error> {
/// The original error that occurred.
pub error: Error,
/// The index of the transaction that failed.
pub transaction_index: usize,
}

impl<Error> TransactionIndexedError<Error> {
/// Create a new `TransactionIndexedError` with the given error and transaction index.
#[must_use]
pub fn new(error: Error, transaction_index: usize) -> Self {
Self {
error,
transaction_index,
}
}

/// Get a reference to the underlying error.
pub fn error(&self) -> &Error {
&self.error
}

/// Convert into the underlying error.
#[must_use]
pub fn into_error(self) -> Error {
self.error
}
}

impl<Error: fmt::Display> fmt::Display for TransactionIndexedError<Error> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"transaction {} failed: {}",
self.transaction_index, self.error
)
}
}

impl<Error: core::error::Error + 'static> core::error::Error for TransactionIndexedError<Error> {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
Some(&self.error)
}
}

impl From<&'static str> for InvalidTransaction {
fn from(s: &'static str) -> Self {
Self::Str(Cow::Borrowed(s))
Expand Down
29 changes: 18 additions & 11 deletions crates/handler/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
use context::{
result::{
EVMError, ExecResultAndState, ExecutionResult, HaltReason, InvalidTransaction,
ResultAndState, ResultVecAndState,
ResultAndState, ResultVecAndState, TransactionIndexedError,
},
Block, ContextSetters, ContextTr, Database, Evm, JournalTr, Transaction,
};
Expand All @@ -13,6 +13,10 @@ use interpreter::{interpreter::EthInterpreter, InterpreterResult};
use state::EvmState;
use std::vec::Vec;

/// Type alias for the result of transact_many_finalize to reduce type complexity.
type TransactManyFinalizeResult<ExecutionResult, State, Error> =
Result<ResultVecAndState<ExecutionResult, State>, TransactionIndexedError<Error>>;

/// Execute EVM transactions. Main trait for transaction execution.
pub trait ExecuteEvm {
/// Output of transaction execution.
Expand Down Expand Up @@ -79,19 +83,22 @@ pub trait ExecuteEvm {
///
/// # Outcome of Error
///
/// If any transaction fails, the journal is finalized and the last error is returned.
///
/// TODO add tx index to the error.
/// If any transaction fails, the journal is finalized and the error is returned with the
/// transaction index that failed.
#[inline]
fn transact_many(
&mut self,
txs: impl Iterator<Item = Self::Tx>,
) -> Result<Vec<Self::ExecutionResult>, Self::Error> {
) -> Result<Vec<Self::ExecutionResult>, TransactionIndexedError<Self::Error>> {
let mut outputs = Vec::new();
for tx in txs {
outputs.push(self.transact_one(tx).inspect_err(|_| {
let _ = self.finalize();
})?);
for (index, tx) in txs.enumerate() {
outputs.push(
self.transact_one(tx)
.inspect_err(|_| {
let _ = self.finalize();
})
.map_err(|error| TransactionIndexedError::new(error, index))?,
);
}
Ok(outputs)
}
Expand All @@ -103,7 +110,7 @@ pub trait ExecuteEvm {
fn transact_many_finalize(
&mut self,
txs: impl Iterator<Item = Self::Tx>,
) -> Result<ResultVecAndState<Self::ExecutionResult, Self::State>, Self::Error> {
) -> TransactManyFinalizeResult<Self::ExecutionResult, Self::State, Self::Error> {
// on error transact_multi will clear the journal
let result = self.transact_many(txs)?;
let state = self.finalize();
Expand Down Expand Up @@ -145,7 +152,7 @@ pub trait ExecuteCommitEvm: ExecuteEvm {
fn transact_many_commit(
&mut self,
txs: impl Iterator<Item = Self::Tx>,
) -> Result<Vec<Self::ExecutionResult>, Self::Error> {
) -> Result<Vec<Self::ExecutionResult>, TransactionIndexedError<Self::Error>> {
let outputs = self.transact_many(txs)?;
self.commit_inner();
Ok(outputs)
Expand Down
2 changes: 1 addition & 1 deletion crates/handler/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ pub trait Handler {
match core::mem::replace(evm.ctx().error(), Ok(())) {
Err(ContextError::Db(e)) => return Err(e.into()),
Err(ContextError::Custom(e)) => return Err(Self::Error::from_string(e)),
Ok(_) => (),
Ok(()) => (),
}

let exec_result = post_execution::output(evm.ctx(), result);
Expand Down
141 changes: 138 additions & 3 deletions crates/handler/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,15 @@ pub fn validate_initial_tx_gas(

#[cfg(test)]
mod tests {
use crate::{ExecuteCommitEvm, MainBuilder, MainContext};
use crate::{api::ExecuteEvm, ExecuteCommitEvm, MainBuilder, MainContext};
use bytecode::opcode;
use context::{
result::{EVMError, ExecutionResult, HaltReason, InvalidTransaction, Output},
Context, TxEnv,
Context, ContextTr, TxEnv,
};
use database::{CacheDB, EmptyDB};
use primitives::{address, eip3860, eip7907, hardfork::SpecId, Bytes, TxKind};
use primitives::{address, eip3860, eip7907, hardfork::SpecId, Bytes, TxKind, B256};
use state::{AccountInfo, Bytecode};

fn deploy_contract(
bytecode: Bytes,
Expand Down Expand Up @@ -554,4 +555,138 @@ mod tests {
_ => panic!("execution result is not Success"),
}
}

#[test]
fn test_transact_many_with_transaction_index_error() {
use context::result::TransactionIndexedError;

let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
let mut evm = ctx.build_mainnet();

// Create a transaction that will fail (invalid gas limit)
let invalid_tx = TxEnv::builder()
.gas_limit(0) // This will cause a validation error
.build()
.unwrap();

// Create a valid transaction
let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();

// Test that the first transaction fails with index 0
let result = evm.transact_many([invalid_tx.clone()].into_iter());
assert!(matches!(
result,
Err(TransactionIndexedError {
transaction_index: 0,
..
})
));

// Test that the second transaction fails with index 1
let result = evm.transact_many([valid_tx, invalid_tx].into_iter());
assert!(matches!(
result,
Err(TransactionIndexedError {
transaction_index: 1,
..
})
));
}

#[test]
fn test_transact_many_success() {
use primitives::{address, U256};

let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
let mut evm = ctx.build_mainnet();

// Add balance to the caller account
let caller = address!("0x0000000000000000000000000000000000000001");
evm.db_mut().insert_account_info(
caller,
AccountInfo::new(
U256::from(1000000000000000000u64),
0,
B256::ZERO,
Bytecode::new(),
),
);

// Create valid transactions with proper data
let tx1 = TxEnv::builder()
.caller(caller)
.gas_limit(100000)
.gas_price(20_000_000_000u128)
.nonce(0)
.build()
.unwrap();

let tx2 = TxEnv::builder()
.caller(caller)
.gas_limit(100000)
.gas_price(20_000_000_000u128)
.nonce(1)
.build()
.unwrap();

// Test that all transactions succeed
let result = evm.transact_many([tx1, tx2].into_iter());
if let Err(e) = &result {
println!("Error: {:?}", e);
}
let outputs = result.expect("All transactions should succeed");
assert_eq!(outputs.len(), 2);
}

#[test]
fn test_transact_many_finalize_with_error() {
use context::result::TransactionIndexedError;

let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
let mut evm = ctx.build_mainnet();

// Create transactions where the second one fails
let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();

let invalid_tx = TxEnv::builder()
.gas_limit(0) // This will cause a validation error
.build()
.unwrap();

// Test that transact_many_finalize returns the error with correct index
let result = evm.transact_many_finalize([valid_tx, invalid_tx].into_iter());
assert!(matches!(
result,
Err(TransactionIndexedError {
transaction_index: 1,
..
})
));
}

#[test]
fn test_transact_many_commit_with_error() {
use context::result::TransactionIndexedError;

let ctx = Context::mainnet().with_db(CacheDB::<EmptyDB>::default());
let mut evm = ctx.build_mainnet();

// Create transactions where the first one fails
let invalid_tx = TxEnv::builder()
.gas_limit(0) // This will cause a validation error
.build()
.unwrap();

let valid_tx = TxEnv::builder().gas_limit(100000).build().unwrap();

// Test that transact_many_commit returns the error with correct index
let result = evm.transact_many_commit([invalid_tx, valid_tx].into_iter());
assert!(matches!(
result,
Err(TransactionIndexedError {
transaction_index: 0,
..
})
));
}
}
Loading