diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f3499d9..2786d4d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: cargo run --release + - run: cargo test --release diff --git a/Cargo.lock b/Cargo.lock index d53dbde0..59b4e662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -274,6 +274,7 @@ name = "block-stm-revm" version = "0.1.0" dependencies = [ "dashmap", + "rand", "revm", ] diff --git a/Cargo.toml b/Cargo.toml index a1968934..02546e21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] dashmap = "5.5.3" +rand = "0.8.5" # TODO: Let's do our best to port needed changes upstream revm = { git = "https://github.com/risechain/revm", rev = "3c046a6aa734d914dc37bcade8ac5932a9599a23" } diff --git a/src/block_stm.rs b/src/block_stm.rs index dab6b630..5276f03a 100644 --- a/src/block_stm.rs +++ b/src/block_stm.rs @@ -22,22 +22,17 @@ impl BlockSTM { /// Run a list of REVM transactions through Block-STM. /// TODO: Better concurrency control pub fn run( - storage: Arc, + storage: Storage, block_env: BlockEnv, - txs: Arc>, + txs: Vec, concurrency_level: NonZeroUsize, ) -> Vec { let block_size = txs.len(); let scheduler = Scheduler::new(block_size); let mv_memory = Arc::new(MvMemory::new(block_size)); - let vm = Vm::new( - storage.clone(), - block_env.clone(), - txs.clone(), - mv_memory.clone(), - ); // TODO: Better error handling let mut beneficiary_account_info = storage.basic(block_env.coinbase).unwrap(); + let vm = Vm::new(storage, block_env.clone(), txs, mv_memory.clone()); // TODO: Should we move this to `Vm`? let execution_results = (0..block_size).map(|_| Mutex::new(None)).collect(); // TODO: Better thread handling diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7055e3d3..00000000 --- a/src/main.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! A quick & dirty executable to test run BlockSTM. -//! TODO: Turn this into proper test & bench executables. - -use block_stm_revm::{BlockSTM, Storage}; -use revm::primitives::{ - alloy_primitives::U160, env::TxEnv, AccountInfo, Address, BlockEnv, ResultAndState, TransactTo, - U256, -}; -use revm::{DatabaseCommit, Evm, InMemoryDB}; -use std::num::NonZeroUsize; -use std::sync::Arc; -use std::thread; -use std::time::SystemTime; - -// The source-of-truth execution result that BlockSTM must match. -fn execute_sequential(mut db: InMemoryDB, txs: Arc>) -> Vec { - txs.iter() - .map(|tx| { - let result_and_state = Evm::builder() - .with_ref_db(&mut db) - .with_tx_env(tx.clone()) - .build() - .transact() - // TODO: Proper error handling - .unwrap(); - db.commit(result_and_state.state.clone()); - result_and_state - }) - .collect() -} - -fn main() { - // TODO: Populate real-er transactions please! - let block_size = 500_000; // number of transactions - - // Mock the beneficiary account (`Address:ZERO`) and the next `block_size` user accounts. - let mut sequential_db = InMemoryDB::default(); - let mut block_stm_storage = Storage::default(); - // Half full to have enough tokens for tests without the corner case of not going beyond MAX. - let mock_account = AccountInfo::from_balance(U256::MAX.div_ceil(U256::from(2))); - for i in 0..=block_size { - let address = Address::from(U160::from(i)); - sequential_db.insert_account_info(address, mock_account.clone()); - block_stm_storage.insert_account_info(address, mock_account.clone()); - } - - // Mock `block_size` transactions sending some tokens to itself. - let txs: Arc> = Arc::new( - (1..=block_size) - .map(|i| { - let address = Address::from(U160::from(i)); - TxEnv { - // The beneficiary account spending mid-block to test - // beneficiary handling. - caller: if i == 100 { Address::ZERO } else { address }, - transact_to: TransactTo::Call(address), - value: U256::from(1), - gas_price: U256::from(1), - ..TxEnv::default() - } - }) - .collect(), - ); - - let start_time = SystemTime::now(); - let result_sequential = execute_sequential(sequential_db, txs.clone()); - println!("Executed sequentially in {:?}", start_time.elapsed()); - - let start_time = SystemTime::now(); - let result_block_stm = BlockSTM::run( - Arc::new(block_stm_storage), - BlockEnv::default(), - txs, - thread::available_parallelism().unwrap_or(NonZeroUsize::MIN), - ); - println!("Executed Block-STM in {:?}", start_time.elapsed()); - - // TODO: Only print the differences for easier debugging - assert_eq!( - result_sequential, result_block_stm, - "Block-STM's execution result doesn't match Sequential's" - ); -} diff --git a/src/vm.rs b/src/vm.rs index 54cfd6c3..7de9fead 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -190,7 +190,8 @@ impl Database for VmDb { } // The VM describes how to read values to execute transactions. Also, it -// captures the read & write sets of each execution. +// captures the read & write sets of each execution. Note that a single +// `Vm` can be shared among threads. pub(crate) struct Vm { storage: Arc, block_env: BlockEnv, @@ -200,15 +201,16 @@ pub(crate) struct Vm { impl Vm { pub(crate) fn new( - storage: Arc, + storage: Storage, block_env: BlockEnv, - txs: Arc>, + txs: Vec, + // TODO: Make `Vm` own `MvMemory` away from `BlockSTM::run`? mv_memory: Arc, ) -> Self { Self { - storage, + storage: Arc::new(storage), block_env, - txs, + txs: Arc::new(txs), mv_memory, } } diff --git a/tests/beneficiary.rs b/tests/beneficiary.rs new file mode 100644 index 00000000..f53686fb --- /dev/null +++ b/tests/beneficiary.rs @@ -0,0 +1,38 @@ +// Tests for the beneficiary account, especially for the lazy update of its balance to avoid +// "implicit" dependency among consecutive transactions. +// Currently, we randomly insert a beneficiary spending in the middle of the block. +// TODO: Add more test scenarios around the beneficiary account's activities in the block. + +use rand::random; +use revm::primitives::{alloy_primitives::U160, env::TxEnv, Address, BlockEnv, TransactTo, U256}; + +mod common; + +#[test] +fn beneficiary() { + let block_size = 100_000; // number of transactions + let block_env = BlockEnv::default(); + + common::test_txs( + block_env, + // Mock `block_size` transactions sending some tokens to itself. + // Skipping `Address::ZERO` as the beneficiary account. + (1..=block_size) + .map(|i| { + // Randomly insert a beneficiary spending every ~256 txs + let address = if random::() == 0 { + Address::from(U160::from(0)) + } else { + Address::from(U160::from(i)) + }; + TxEnv { + caller: address, + transact_to: TransactTo::Call(address), + value: U256::from(1), + gas_price: U256::from(1), + ..TxEnv::default() + } + }) + .collect(), + ); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..eedbf50d --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,70 @@ +use std::{num::NonZeroUsize, thread}; + +use block_stm_revm::{BlockSTM, Storage}; +use revm::{ + primitives::{ + alloy_primitives::U160, AccountInfo, Address, BlockEnv, ResultAndState, TxEnv, U256, + }, + DatabaseCommit, Evm, InMemoryDB, +}; + +// Return an `InMemoryDB` for sequential usage and `Storage` for BlockSTM usage. +// Both represent a "standard" mock state with prefilled accounts. +// TODO: Mock pre-deployed contracts. +fn mock_dbs(num_prefilled_accounts: usize) -> (InMemoryDB, Storage) { + let mut sequential_db = InMemoryDB::default(); + let mut block_stm_storage = Storage::default(); + + // Mock the beneficiary account (`Address:ZERO`) and the next `block_size` user accounts. + // Filling half full accounts to have enough tokens for tests without worrying about the + // corner case of balance not going beyond `U256::MAX`. + let mock_account = AccountInfo::from_balance(U256::MAX.div_ceil(U256::from(2))); + for i in 0..=num_prefilled_accounts { + let address = Address::from(U160::from(i)); + sequential_db.insert_account_info(address, mock_account.clone()); + block_stm_storage.insert_account_info(address, mock_account.clone()); + } + + (sequential_db, block_stm_storage) +} + +// The source-of-truth sequential execution result that BlockSTM must match. +fn execute_sequential( + mut db: InMemoryDB, + block_env: BlockEnv, + txs: &[TxEnv], +) -> Vec { + txs.iter() + .map(|tx| { + let result_and_state = Evm::builder() + .with_ref_db(&mut db) + .with_block_env(block_env.clone()) + .with_tx_env(tx.clone()) + .build() + .transact() + // TODO: Proper error handling + .unwrap(); + db.commit(result_and_state.state.clone()); + result_and_state + }) + .collect() +} + +// Execute a list of transactions sequentially & with BlockSTM and assert that +// the execution results match. +pub(crate) fn test_txs(block_env: BlockEnv, txs: Vec) { + // TODO: Decouple the (number of) prefilled accounts with the number of transactions. + let (sequential_db, block_stm_storage) = mock_dbs(txs.len()); + let result_sequential = execute_sequential(sequential_db, block_env.clone(), &txs); + let result_block_stm = BlockSTM::run( + block_stm_storage, + block_env, + txs, + thread::available_parallelism().unwrap_or(NonZeroUsize::MIN), + ); + + assert_eq!( + result_sequential, result_block_stm, + "Block-STM's execution result doesn't match Sequential's" + ); +} diff --git a/tests/raw_transfers.rs b/tests/raw_transfers.rs new file mode 100644 index 00000000..bc39f8cd --- /dev/null +++ b/tests/raw_transfers.rs @@ -0,0 +1,31 @@ +// Test raw transfers -- only send some ETH from one account to another without extra data. +// Currently, we only have a no-state-conflict test of user account sending to themselves. +// TODO: Add more tests of accounts cross-transferring to create state depdendencies. + +use revm::primitives::{alloy_primitives::U160, env::TxEnv, Address, BlockEnv, TransactTo, U256}; + +mod common; + +#[test] +fn raw_transfers() { + let block_size = 100_000; // number of transactions + let block_env = BlockEnv::default(); + + common::test_txs( + block_env, + // Mock `block_size` transactions sending some tokens to itself. + // Skipping `Address::ZERO` as the beneficiary account. + (1..=block_size) + .map(|i| { + let address = Address::from(U160::from(i)); + TxEnv { + caller: address, + transact_to: TransactTo::Call(address), + value: U256::from(1), + gas_price: U256::from(1), + ..TxEnv::default() + } + }) + .collect(), + ); +}