Skip to content

Commit

Permalink
feat: verify receipts in tests/mainnet.rs (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
kien-rise committed Jun 9, 2024
1 parent 5d3324d commit ae18f9e
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 72 deletions.
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ reqwest = "0.12.4"
tokio = { version = "1.38.0", features = ["rt-multi-thread"] }

[dev-dependencies]
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "a4bb5f0be3eec5c8679bdab93c1482df38ba8509" }
alloy-rlp = "0.3.5"
alloy-trie = "0.4.1"
criterion = "0.5.1"
rand = "0.8.5"
rayon = "1.10.0"
Expand Down
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ macro_rules! index_mutex {
}

mod pevm;
pub use pevm::{execute, execute_revm, execute_revm_sequential, PevmError, PevmResult};
pub use pevm::{
execute, execute_revm, execute_revm_sequential, PevmError, PevmResult, PevmTxExecutionResult,
};
mod mv_memory;
mod primitives;
pub use primitives::get_block_spec;
Expand Down
70 changes: 52 additions & 18 deletions src/pevm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ use std::{

use ahash::{AHashMap, AHashSet};
use alloy_primitives::{Address, U256};
use alloy_rpc_types::Block;
use alloy_rpc_types::{Block, Receipt};
use revm::{
db::CacheDB,
primitives::{Account, AccountInfo, BlockEnv, ResultAndState, SpecId, TransactTo, TxEnv},
primitives::{
Account, AccountInfo, BlockEnv, EvmState, ResultAndState, SpecId, TransactTo, TxEnv,
},
DatabaseCommit,
};

Expand Down Expand Up @@ -42,8 +44,19 @@ pub enum PevmError {
UnreachableError,
}

/// Execution result of PEVM.
pub type PevmResult = Result<Vec<ResultAndState>, PevmError>;
/// Execution result of a transaction
#[derive(Debug, Clone, PartialEq)]
pub struct PevmTxExecutionResult {
/// Receipt of execution
// TODO: Consider promoting to `ReceiptEnvelope` if there is high demand
pub receipt: Receipt,
/// State that got updated
// TODO: Use our own type to not leak REVM types to library users.
pub state: EvmState,
}

/// Execution result of a block
pub type PevmResult = Result<Vec<PevmTxExecutionResult>, PevmError>;

/// Execute an Alloy block, which is becoming the "standard" format in Rust.
/// TODO: Better error handling.
Expand Down Expand Up @@ -185,18 +198,21 @@ pub fn execute_revm<S: Storage + Send + Sync>(
// to avoid "implicit" dependency among consecutive transactions that read & write there.
// TODO: Refactor, improve speed & error handling.
let beneficiary_values = mv_memory.consume_beneficiary();
Ok(execution_results
.into_iter()
.zip(beneficiary_values)
.map(|(mutex, value)| {
let mut result_and_state = mutex.into_inner().unwrap().unwrap();
result_and_state.state.insert(
beneficiary_address,
post_process_beneficiary(&mut beneficiary_account_info, value),
);
result_and_state
})
.collect())

let fully_evaluated_results =
execution_results
.into_iter()
.zip(beneficiary_values)
.map(|(mutex, value)| {
let mut result_and_state = mutex.into_inner().unwrap().unwrap();
result_and_state.state.insert(
beneficiary_address,
post_process_beneficiary(&mut beneficiary_account_info, value),
);
result_and_state
});

Ok(transform_output(fully_evaluated_results))
}

/// Execute REVM transactions sequentially.
Expand All @@ -207,7 +223,7 @@ pub fn execute_revm_sequential<S: Storage>(
spec_id: SpecId,
block_env: BlockEnv,
txs: Vec<TxEnv>,
) -> Result<Vec<ResultAndState>, PevmError> {
) -> Result<Vec<PevmTxExecutionResult>, PevmError> {
let mut results = Vec::with_capacity(txs.len());
let mut db = CacheDB::new(StorageWrapper(storage));
for tx in txs {
Expand All @@ -219,7 +235,7 @@ pub fn execute_revm_sequential<S: Storage>(
Err(err) => return Err(PevmError::ExecutionError(format!("{err:?}"))),
}
}
Ok(results)
Ok(transform_output(results))
}

// Return `None` to signal falling back to sequential execution as we detected too many
Expand Down Expand Up @@ -435,3 +451,21 @@ fn post_process_beneficiary(
beneficiary_account.mark_touch();
beneficiary_account
}

fn transform_output(
revm_results: impl IntoIterator<Item = ResultAndState>,
) -> Vec<PevmTxExecutionResult> {
let mut cumulative_gas_used: u128 = 0;
revm_results
.into_iter()
.map(|ResultAndState { result, state }| {
cumulative_gas_used += result.gas_used() as u128;
let receipt = Receipt {
status: result.is_success().into(),
cumulative_gas_used,
logs: result.into_logs(),
};
PevmTxExecutionResult { receipt, state }
})
.collect()
}
108 changes: 86 additions & 22 deletions tests/common/runner.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use alloy_rpc_types::Block;
use pevm::{PevmResult, Storage};
use alloy_consensus::{ReceiptEnvelope, TxType};
use alloy_primitives::{Bloom, B256};
use alloy_provider::network::eip2718::Encodable2718;
use alloy_rpc_types::{Block, BlockTransactions, Transaction};
use pevm::{PevmResult, PevmTxExecutionResult, Storage};
use revm::{
db::PlainAccount,
primitives::{alloy_primitives::U160, AccountInfo, Address, BlockEnv, SpecId, TxEnv, U256},
};
use std::{num::NonZeroUsize, thread};
use std::{collections::BTreeMap, num::NonZeroUsize, thread};

// Mock an account from an integer index that is used as the address.
// Useful for mock iterations.
Expand All @@ -18,16 +21,7 @@ pub fn mock_account(idx: usize) -> (Address, PlainAccount) {
)
}

// TODO: Pass in hashes to checksum, especially for real blocks.
pub fn assert_execution_result(
sequential_result: PevmResult,
parallel_result: PevmResult,
must_succeed: bool,
) {
// We must assert sucess for real blocks, etc.
if must_succeed {
assert!(sequential_result.is_ok() && parallel_result.is_ok());
}
pub fn assert_execution_result(sequential_result: &PevmResult, parallel_result: &PevmResult) {
assert_eq!(sequential_result, parallel_result);
}

Expand All @@ -41,23 +35,93 @@ pub fn test_execute_revm<S: Storage + Clone + Send + Sync>(
) {
let concurrency_level = thread::available_parallelism().unwrap_or(NonZeroUsize::MIN);
assert_execution_result(
pevm::execute_revm_sequential(storage.clone(), spec_id, block_env.clone(), txs.clone()),
pevm::execute_revm(storage, spec_id, block_env, txs, concurrency_level),
false, // TODO: Parameterize this
&pevm::execute_revm_sequential(storage.clone(), spec_id, block_env.clone(), txs.clone()),
&pevm::execute_revm(storage, spec_id, block_env, txs, concurrency_level),
);
}

// Refer to section 4.3.2. Holistic Validity in the Ethereum Yellow Paper.
// https://github.com/ethereum/go-ethereum/blob/master/cmd/era/main.go#L289
fn calculate_receipt_root(
txs: &BlockTransactions<Transaction>,
tx_results: &[PevmTxExecutionResult],
) -> B256 {
// 1. Create an iterator of ReceiptEnvelope
let tx_type_iter = txs
.txns()
.map(|tx| TxType::try_from(tx.transaction_type.unwrap_or_default()).unwrap());

let receipt_iter = tx_results.iter().map(|tx| tx.receipt.clone().with_bloom());

let receipt_envelope_iter =
Iterator::zip(tx_type_iter, receipt_iter).map(|(tx_type, receipt)| match tx_type {
TxType::Legacy => ReceiptEnvelope::Legacy(receipt),
TxType::Eip2930 => ReceiptEnvelope::Eip2930(receipt),
TxType::Eip1559 => ReceiptEnvelope::Eip1559(receipt),
TxType::Eip4844 => ReceiptEnvelope::Eip4844(receipt),
});

// 2. Create a trie then calculate the root hash
// We use BTreeMap because the keys must be sorted in ascending order.
let trie_entries: BTreeMap<_, _> = receipt_envelope_iter
.enumerate()
.map(|(index, receipt)| {
let key_buffer = alloy_rlp::encode_fixed_size(&index);
let mut value_buffer = Vec::new();
receipt.encode_2718(&mut value_buffer);
(key_buffer, value_buffer)
})
.collect();

let mut hash_builder = alloy_trie::HashBuilder::default();
for (k, v) in trie_entries {
hash_builder.add_leaf(alloy_trie::Nibbles::unpack(&k), &v);
}
hash_builder.root()
}

// Execute an Alloy block sequentially & with PEVM and assert that
// the execution results match.
pub fn test_execute_alloy<S: Storage + Clone + Send + Sync>(
storage: S,
block: Block,
must_succeed: bool,
must_match_block_header: bool,
) {
let concurrency_level = thread::available_parallelism().unwrap_or(NonZeroUsize::MIN);
assert_execution_result(
pevm::execute(storage.clone(), block.clone(), concurrency_level, true),
pevm::execute(storage, block, concurrency_level, false),
must_succeed,
);
let sequential_result = pevm::execute(storage.clone(), block.clone(), concurrency_level, true);
let parallel_result = pevm::execute(storage, block.clone(), concurrency_level, false);
assert_execution_result(&sequential_result, &parallel_result);

if must_match_block_header {
let tx_results = sequential_result.unwrap();

// We can only calculate the receipts root from Byzantium.
// Before EIP-658 (https://eips.ethereum.org/EIPS/eip-658), the
// receipt root is calculated with the post transaction state root,
// which we doesn't have in these tests.
if block.header.number.unwrap() >= 4370000 {
assert_eq!(
block.header.receipts_root,
calculate_receipt_root(&block.transactions, &tx_results)
);
}

assert_eq!(
block.header.logs_bloom,
tx_results
.iter()
.map(|tx| tx.receipt.bloom_slow())
.fold(Bloom::default(), |acc, bloom| acc.bit_or(bloom))
);

assert_eq!(
block.header.gas_used,
tx_results
.iter()
.last()
.unwrap()
.receipt
.cumulative_gas_used
);
}
}
39 changes: 11 additions & 28 deletions tests/ethereum/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
// - Help outline the minimal state commitment logic for PEVM.

use ahash::AHashMap;
use pevm::{InMemoryStorage, PevmError};
use pevm::{InMemoryStorage, PevmError, PevmTxExecutionResult};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use revm::db::PlainAccount;
use revm::primitives::ruint::ParseError;
use revm::primitives::{
calc_excess_blob_gas, AccountInfo, BlobExcessGasAndPrice, BlockEnv, Bytecode, Bytes,
ExecutionResult, HaltReason, Output, ResultAndState, SpecId, SuccessReason, TransactTo, TxEnv,
U256,
calc_excess_blob_gas, AccountInfo, BlobExcessGasAndPrice, BlockEnv, Bytecode, SpecId,
TransactTo, TxEnv, U256,
};
use revme::cmd::statetest::models::{
Env, SpecName, TestSuite, TestUnit, TransactionParts, TxPartIndices,
Expand Down Expand Up @@ -143,29 +142,13 @@ fn run_test_unit(path: &Path, unit: &TestUnit) {
// EIP-2681
(Some("TR_NonceHasMaxValue"), Ok(exec_results)) => {
assert!(exec_results.len() == 1);
assert!(match exec_results[0].result.clone() {
ExecutionResult::Success {
output: Output::Create(b, None),
..
} => b == Bytes::new(),
_ => false,
});
}
// Special cases where REVM returns `Ok` instead of `Err` on unsupported features.
// Requiring stopping or halting reasons for now.
(Some("TR_TypeNotSupported"), Ok(exec_results)) => {
assert!(exec_results.len() == 1);
assert!(matches!(
exec_results[0].result,
ExecutionResult::Halt {
reason: HaltReason::NotActivated,
..
} | ExecutionResult::Success {
reason: SuccessReason::Stop,
..
}
));
assert!(exec_results[0].receipt.status.coerce_status());
// This is overly strict as we only need the newly created account's code to be empty.
// Extracting such account is unjustified complexity so let's live with this for now.
assert!(exec_results[0].state.values().all(|account| account.info.is_empty_code_hash()));
}
// Skipping special cases where REVM returns `Ok` on unsupported features.
(Some("TR_TypeNotSupported"), Ok(_)) => {}
// Remaining tests that expect execution to fail -> match error
(Some(exception), Err(PevmError::ExecutionError(error))) => {
// TODO: Cleaner code would be nice..
Expand Down Expand Up @@ -201,9 +184,9 @@ fn run_test_unit(path: &Path, unit: &TestUnit) {
// Tests that exepect execution to succeed -> match post state root
(None, Ok(exec_results)) => {
assert!(exec_results.len() == 1);
let ResultAndState { result, state } = exec_results[0].clone();
let PevmTxExecutionResult {receipt, state} = exec_results[0].clone();

let logs_root = log_rlp_hash(result.logs());
let logs_root = log_rlp_hash(&receipt.logs);
assert_eq!(logs_root, test.logs, "Mismatched logs root for {path:?}");

// This is a good reference for a minimal state/DB commitment logic for
Expand Down
Loading

0 comments on commit ae18f9e

Please sign in to comment.