From fa8edcd2177a49907cd7f2d9d8ac9a68614d76bd Mon Sep 17 00:00:00 2001 From: Lukasz Rozmej Date: Thu, 14 Sep 2023 10:26:34 +0200 Subject: [PATCH] EVM: Storage improvements , Blocks, Transactions, Receipts (#830) * Properly create receipts for successful transactions * move to reth domain types for storage + add our packing types where feasible * Added block building * create receipts for failed transactions * Add finalize_slot_hook * separating state and storage better, fixes * fix transaction mapping from hash to index * fix * fix * improve tests * test & fix state vector extensions * fix test * add failed transaction call test --------- Co-authored-by: bkolad --- examples/demo-rollup/tests/evm/mod.rs | 1 + examples/demo-stf/src/hooks_impl.rs | 17 +- .../src/chain_state/helpers.rs | 10 +- .../sov-chain-state/src/hooks.rs | 8 +- .../sov-evm/src/call.rs | 104 ++++++----- .../sov-evm/src/evm/conversions.rs | 64 ++----- .../sov-evm/src/evm/executor.rs | 4 +- .../sov-evm/src/evm/mod.rs | 11 -- .../sov-evm/src/evm/tests.rs | 27 ++- .../sov-evm/src/evm/transaction.rs | 79 ++++++++- .../sov-evm/src/genesis.rs | 28 +-- .../sov-evm/src/hooks.rs | 158 +++++++++++++---- .../module-implementations/sov-evm/src/lib.rs | 45 ++--- .../sov-evm/src/query.rs | 129 +++++++++++++- .../sov-evm/src/tests/call_tests.rs | 139 +++++++++++---- .../sov-evm/src/tests/dev_signer.rs | 3 +- .../sov-evm/src/tests/genesis_tests.rs | 83 ++++++--- .../sov-evm/src/tests/hooks_tests.rs | 163 +++++++++++++++++- module-system/module-schemas/build.rs | 1 - module-system/sov-modules-api/src/hooks.rs | 8 +- .../sov-modules-stf-template/src/lib.rs | 13 +- .../sov-state/src/containers/accessory_vec.rs | 39 +++++ module-system/sov-state/src/containers/vec.rs | 39 +++++ 23 files changed, 899 insertions(+), 274 deletions(-) diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs index d80cf1f297..02bebf88d0 100644 --- a/examples/demo-rollup/tests/evm/mod.rs +++ b/examples/demo-rollup/tests/evm/mod.rs @@ -203,6 +203,7 @@ async fn send_tx_test_to_eth(rpc_address: SocketAddr) -> Result<(), Box Result<(), anyhow::Error> { let (port_tx, port_rx) = tokio::sync::oneshot::channel(); diff --git a/examples/demo-stf/src/hooks_impl.rs b/examples/demo-stf/src/hooks_impl.rs index 5c80199a8d..b0d1224039 100644 --- a/examples/demo-stf/src/hooks_impl.rs +++ b/examples/demo-stf/src/hooks_impl.rs @@ -6,7 +6,7 @@ use sov_modules_stf_template::SequencerOutcome; use sov_rollup_interface::da::BlockHeaderTrait; use sov_rollup_interface::da::{BlobReaderTrait, DaSpec}; use sov_sequencer_registry::SequencerRegistry; -use sov_state::WorkingSet; +use sov_state::{AccessoryWorkingSet, WorkingSet}; use tracing::info; use crate::runtime::Runtime; @@ -94,12 +94,23 @@ impl SlotHooks for Runtime { fn end_slot_hook( &self, - #[allow(unused_variables)] root_hash: [u8; 32], #[allow(unused_variables)] working_set: &mut sov_state::WorkingSet< ::Storage, >, ) { #[cfg(feature = "experimental")] - self.evm.end_slot_hook(root_hash, working_set); + self.evm.end_slot_hook(working_set); + } + + fn finalize_slot_hook( + &self, + #[allow(unused_variables)] root_hash: [u8; 32], + #[allow(unused_variables)] accesorry_working_set: &mut AccessoryWorkingSet< + ::Storage, + >, + ) { + #[cfg(feature = "experimental")] + self.evm + .finalize_slot_hook(root_hash, accesorry_working_set); } } diff --git a/module-system/module-implementations/integration-tests/src/chain_state/helpers.rs b/module-system/module-implementations/integration-tests/src/chain_state/helpers.rs index 5ba90787bc..e6ce7f3da0 100644 --- a/module-system/module-implementations/integration-tests/src/chain_state/helpers.rs +++ b/module-system/module-implementations/integration-tests/src/chain_state/helpers.rs @@ -74,10 +74,18 @@ impl SlotHooks for TestRuntime { fn end_slot_hook( &self, - _root_hash: [u8; 32], _working_set: &mut sov_state::WorkingSet<::Storage>, ) { } + + fn finalize_slot_hook( + &self, + _root_hash: [u8; 32], + _accesorry_working_set: &mut sov_state::AccessoryWorkingSet< + ::Storage, + >, + ) { + } } impl BlobSelector for TestRuntime diff --git a/module-system/module-implementations/sov-chain-state/src/hooks.rs b/module-system/module-implementations/sov-chain-state/src/hooks.rs index a934ab1a60..32aa01561c 100644 --- a/module-system/module-implementations/sov-chain-state/src/hooks.rs +++ b/module-system/module-implementations/sov-chain-state/src/hooks.rs @@ -1,7 +1,7 @@ use sov_modules_api::hooks::SlotHooks; use sov_modules_api::{Context, Spec}; use sov_rollup_interface::da::BlockHeaderTrait; -use sov_state::{Storage, WorkingSet}; +use sov_state::{AccessoryWorkingSet, Storage, WorkingSet}; use super::ChainState; use crate::{StateTransitionId, TransitionInProgress}; @@ -62,10 +62,12 @@ impl SlotHooks for ChainState::Storage>) {} + + fn finalize_slot_hook( &self, _root_hash: [u8; 32], - _working_set: &mut WorkingSet<::Storage>, + _accesorry_working_set: &mut AccessoryWorkingSet<::Storage>, ) { } } diff --git a/module-system/module-implementations/sov-evm/src/call.rs b/module-system/module-implementations/sov-evm/src/call.rs index 3b7b42e00d..97cb9d00f8 100644 --- a/module-system/module-implementations/sov-evm/src/call.rs +++ b/module-system/module-implementations/sov-evm/src/call.rs @@ -1,20 +1,21 @@ use anyhow::Result; use reth_primitives::TransactionSignedEcRecovered; -use revm::primitives::{CfgEnv, SpecId}; +use reth_revm::into_reth_log; +use revm::primitives::{CfgEnv, EVMError, SpecId}; use sov_modules_api::CallResponse; use sov_state::WorkingSet; use crate::evm::db::EvmDb; use crate::evm::executor::{self}; -use crate::evm::transaction::BlockEnv; -use crate::evm::{contract_address, EvmChainConfig, RlpEvmTransaction}; +use crate::evm::transaction::{BlockEnv, Receipt, TransactionSignedAndRecovered}; +use crate::evm::{EvmChainConfig, RlpEvmTransaction}; +use crate::experimental::PendingTransaction; use crate::Evm; #[cfg_attr( feature = "native", derive(serde::Serialize), - derive(serde::Deserialize), - derive(schemars::JsonSchema) + derive(serde::Deserialize) )] #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] pub struct CallMessage { @@ -29,53 +30,70 @@ impl Evm { working_set: &mut WorkingSet, ) -> Result { let evm_tx_recovered: TransactionSignedEcRecovered = tx.try_into()?; + let block_env = self + .pending_block + .get(working_set) + .expect("Pending block must be set"); - let block_env = self.pending_block.get(working_set).unwrap_or_default(); - let cfg = self.cfg.get(working_set).unwrap_or_default(); + let cfg = self.cfg.get(working_set).expect("Evm config must be set"); let cfg_env = get_cfg_env(&block_env, cfg, None); - let hash = evm_tx_recovered.hash(); - let evm_db: EvmDb<'_, C> = self.get_db(working_set); + let result = executor::execute_tx(evm_db, &block_env, &evm_tx_recovered, cfg_env); + let previous_transaction = self.pending_transactions.last(working_set); + let previous_transaction_cumulative_gas_used = previous_transaction + .as_ref() + .map_or(0u64, |tx| tx.receipt.receipt.cumulative_gas_used); + let log_index_start = previous_transaction.as_ref().map_or(0u64, |tx| { + tx.receipt.log_index_start + tx.receipt.receipt.logs.len() as u64 + }); - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/505 - let result = executor::execute_tx(evm_db, block_env, &evm_tx_recovered, cfg_env).unwrap(); - - let from = evm_tx_recovered.signer(); - let to = evm_tx_recovered.to(); - let transaction = reth_rpc_types::Transaction::from_recovered(evm_tx_recovered); + let receipt = match result { + Ok(result) => { + let logs: Vec<_> = result.logs().into_iter().map(into_reth_log).collect(); + let gas_used = result.gas_used(); - self.pending_transactions - .push(&transaction, &mut working_set.accessory_state()); + Receipt { + receipt: reth_primitives::Receipt { + tx_type: evm_tx_recovered.tx_type(), + success: result.is_success(), + cumulative_gas_used: previous_transaction_cumulative_gas_used + gas_used, + logs, + }, + gas_used, + log_index_start, + error: None, + } + } + Err(err) => Receipt { + receipt: reth_primitives::Receipt { + tx_type: evm_tx_recovered.tx_type(), + success: false, + cumulative_gas_used: previous_transaction_cumulative_gas_used, + logs: vec![], + }, + // TODO: Do we want failed transactions to use all gas? + gas_used: 0, + log_index_start, + error: Some(match err { + EVMError::Transaction(err) => EVMError::Transaction(err), + EVMError::PrevrandaoNotSet => EVMError::PrevrandaoNotSet, + EVMError::Database(_) => EVMError::Database(0u8), + }), + }, + }; - let receipt = reth_rpc_types::TransactionReceipt { - transaction_hash: hash.into(), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - transaction_index: Some(reth_primitives::U256::from(0)), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - block_hash: Default::default(), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - block_number: Some(reth_primitives::U256::from(0)), - from, - to, - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - gas_used: Default::default(), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - cumulative_gas_used: Default::default(), - contract_address: contract_address(result), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - logs: Default::default(), - state_root: Some(reth_primitives::U256::from(0).into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - logs_bloom: Default::default(), - status_code: Some(1u64.into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/504 - effective_gas_price: Default::default(), - transaction_type: reth_primitives::U8::from(1), + let pending_transaction = PendingTransaction { + transaction: TransactionSignedAndRecovered { + signer: evm_tx_recovered.signer(), + signed_transaction: evm_tx_recovered.into(), + block_number: block_env.number, + }, + receipt, }; - self.receipts - .set(&hash.into(), &receipt, &mut working_set.accessory_state()); + self.pending_transactions + .push(&pending_transaction, working_set); Ok(CallResponse::default()) } diff --git a/module-system/module-implementations/sov-evm/src/evm/conversions.rs b/module-system/module-implementations/sov-evm/src/evm/conversions.rs index 542fa1b057..2b214f1e0b 100644 --- a/module-system/module-implementations/sov-evm/src/evm/conversions.rs +++ b/module-system/module-implementations/sov-evm/src/evm/conversions.rs @@ -1,6 +1,4 @@ use bytes::Bytes; -use ethereum_types::U64; -use ethers_core::types::{Bytes as EthBytes, OtherFields, Transaction}; use reth_primitives::{ Bytes as RethBytes, TransactionSigned, TransactionSignedEcRecovered, TransactionSignedNoHash, }; @@ -12,7 +10,7 @@ use revm::primitives::{ }; use thiserror::Error; -use super::transaction::{BlockEnv, RlpEvmTransaction}; +use super::transaction::{BlockEnv, RlpEvmTransaction, TransactionSignedAndRecovered}; use super::AccountInfo; impl From for ReVmAccountInfo { @@ -37,15 +35,14 @@ impl From for AccountInfo { } } -impl From for ReVmBlockEnv { - fn from(block_env: BlockEnv) -> Self { +impl From<&BlockEnv> for ReVmBlockEnv { + fn from(block_env: &BlockEnv) -> Self { Self { number: U256::from(block_env.number), coinbase: block_env.coinbase, - timestamp: block_env.timestamp, - // TODO: handle difficulty + timestamp: U256::from(block_env.timestamp), difficulty: U256::ZERO, - prevrandao: block_env.prevrandao, + prevrandao: Some(block_env.prevrandao), basefee: U256::from(block_env.basefee), gas_limit: U256::from(block_env.gas_limit), } @@ -73,44 +70,6 @@ pub(crate) fn create_tx_env(tx: &TransactionSignedEcRecovered) -> TxEnv { } } -impl TryFrom for Transaction { - type Error = RawEvmTxConversionError; - fn try_from(evm_tx: RlpEvmTransaction) -> Result { - let tx: TransactionSignedEcRecovered = evm_tx.try_into()?; - - Ok(Self { - hash: tx.hash().into(), - nonce: tx.nonce().into(), - - from: tx.signer().into(), - to: tx.to().map(|addr| addr.into()), - value: tx.value().into(), - gas_price: Some(tx.effective_gas_price(None).into()), - - input: EthBytes::from(tx.input().to_vec()), - v: tx.signature().v(tx.chain_id()).into(), - r: tx.signature().r.into(), - s: tx.signature().s.into(), - transaction_type: Some(U64::from(tx.tx_type() as u8)), - // TODO handle access list - access_list: None, - max_priority_fee_per_gas: tx.max_priority_fee_per_gas().map(From::from), - max_fee_per_gas: Some(tx.max_fee_per_gas().into()), - chain_id: tx.chain_id().map(|id| id.into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/503 - block_hash: Some([0; 32].into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/503 - block_number: Some(1.into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/503 - transaction_index: Some(1.into()), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/503 - gas: Default::default(), - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/503 - other: OtherFields::default(), - }) - } -} - #[derive(Error, Debug)] pub enum RawEvmTxConversionError { #[error("Empty raw transaction data")] @@ -162,6 +121,15 @@ impl TryFrom for TransactionSignedEcRecovered { } } +impl From for TransactionSignedEcRecovered { + fn from(value: TransactionSignedAndRecovered) -> Self { + TransactionSignedEcRecovered::from_signed_transaction( + value.signed_transaction, + value.signer, + ) + } +} + // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/576 // https://github.com/paradigmxyz/reth/blob/d8677b4146f77c7c82d659c59b79b38caca78778/crates/rpc/rpc/src/eth/revm_utils.rs#L201 pub fn prepare_call_env(request: CallRequest) -> TxEnv { @@ -187,7 +155,3 @@ pub fn prepare_call_env(request: CallRequest) -> TxEnv { access_list: Default::default(), } } - -pub fn to_u64(value: U256) -> u64 { - value.try_into().unwrap() -} diff --git a/module-system/module-implementations/sov-evm/src/evm/executor.rs b/module-system/module-implementations/sov-evm/src/evm/executor.rs index 1cb8ae9878..8be4dc46d4 100644 --- a/module-system/module-implementations/sov-evm/src/evm/executor.rs +++ b/module-system/module-implementations/sov-evm/src/evm/executor.rs @@ -10,7 +10,7 @@ use super::transaction::BlockEnv; pub(crate) fn execute_tx + DatabaseCommit>( db: DB, - block_env: BlockEnv, + block_env: &BlockEnv, tx: &TransactionSignedEcRecovered, config_env: CfgEnv, ) -> Result> { @@ -29,7 +29,7 @@ pub(crate) fn execute_tx + DatabaseCommit>( pub(crate) fn inspect + DatabaseCommit>( db: DB, - block_env: BlockEnv, + block_env: &BlockEnv, tx: TxEnv, config_env: CfgEnv, ) -> Result> { diff --git a/module-system/module-implementations/sov-evm/src/evm/mod.rs b/module-system/module-implementations/sov-evm/src/evm/mod.rs index 553d3ef9f9..f9350be8c0 100644 --- a/module-system/module-implementations/sov-evm/src/evm/mod.rs +++ b/module-system/module-implementations/sov-evm/src/evm/mod.rs @@ -1,6 +1,5 @@ use reth_primitives::{Address, H256, U256}; use revm::primitives::specification::SpecId; -use revm::primitives::{ExecutionResult, Output, B160}; use serde::{Deserialize, Serialize}; use sov_state::{Prefix, StateMap}; @@ -59,16 +58,6 @@ impl DbAccount { } } -pub(crate) fn contract_address(result: ExecutionResult) -> Option { - match result { - ExecutionResult::Success { - output: Output::Create(_, Some(addr)), - .. - } => Some(addr), - _ => None, - } -} - /// EVM Chain configuration #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct EvmChainConfig { diff --git a/module-system/module-implementations/sov-evm/src/evm/tests.rs b/module-system/module-implementations/sov-evm/src/evm/tests.rs index 7c1cc7f460..130b9b884d 100644 --- a/module-system/module-implementations/sov-evm/src/evm/tests.rs +++ b/module-system/module-implementations/sov-evm/src/evm/tests.rs @@ -2,6 +2,7 @@ use std::convert::Infallible; use reth_primitives::TransactionKind; use revm::db::CacheDB; +use revm::precompile::B160; use revm::primitives::{CfgEnv, ExecutionResult, Output, KECCAK_EMPTY, U256}; use revm::{Database, DatabaseCommit}; use sov_state::{ProverStorage, WorkingSet}; @@ -10,7 +11,7 @@ use super::db::EvmDb; use super::db_init::InitEvmDb; use super::executor; use crate::evm::transaction::BlockEnv; -use crate::evm::{contract_address, AccountInfo}; +use crate::evm::AccountInfo; use crate::smart_contracts::SimpleStorageContract; use crate::tests::dev_signer::TestSigner; use crate::Evm; @@ -61,7 +62,7 @@ fn simple_contract_execution + DatabaseCommit + let contract = SimpleStorageContract::default(); - let contract_address = { + let contract_address: B160 = { let tx = dev_signer .sign_default_transaction(TransactionKind::Create, contract.byte_code().to_vec(), 1) .unwrap(); @@ -71,8 +72,8 @@ fn simple_contract_execution + DatabaseCommit + gas_limit: reth_primitives::constants::ETHEREUM_BLOCK_GAS_LIMIT, ..Default::default() }; - let result = executor::execute_tx(&mut evm_db, block_env, tx, CfgEnv::default()).unwrap(); - contract_address(result).expect("Expected successful contract creation") + let result = executor::execute_tx(&mut evm_db, &block_env, tx, CfgEnv::default()).unwrap(); + contract_address(&result).expect("Expected successful contract creation") }; let set_arg = 21989; @@ -82,14 +83,14 @@ fn simple_contract_execution + DatabaseCommit + let tx = dev_signer .sign_default_transaction( - TransactionKind::Call(contract_address.as_fixed_bytes().into()), + TransactionKind::Call(contract_address.into()), hex::decode(hex::encode(&call_data)).unwrap(), 2, ) .unwrap(); let tx = &tx.try_into().unwrap(); - executor::execute_tx(&mut evm_db, BlockEnv::default(), tx, CfgEnv::default()).unwrap(); + executor::execute_tx(&mut evm_db, &BlockEnv::default(), tx, CfgEnv::default()).unwrap(); } let get_res = { @@ -97,7 +98,7 @@ fn simple_contract_execution + DatabaseCommit + let tx = dev_signer .sign_default_transaction( - TransactionKind::Call(contract_address.as_fixed_bytes().into()), + TransactionKind::Call(contract_address.into()), hex::decode(hex::encode(&call_data)).unwrap(), 3, ) @@ -105,7 +106,7 @@ fn simple_contract_execution + DatabaseCommit + let tx = &tx.try_into().unwrap(); let result = - executor::execute_tx(&mut evm_db, BlockEnv::default(), tx, CfgEnv::default()).unwrap(); + executor::execute_tx(&mut evm_db, &BlockEnv::default(), tx, CfgEnv::default()).unwrap(); let out = output(result); ethereum_types::U256::from(out.as_ref()) @@ -113,3 +114,13 @@ fn simple_contract_execution + DatabaseCommit + assert_eq!(set_arg, get_res.as_u32()) } + +fn contract_address(result: &ExecutionResult) -> Option { + match result { + ExecutionResult::Success { + output: Output::Create(_, Some(addr)), + .. + } => Some(**addr), + _ => None, + } +} diff --git a/module-system/module-implementations/sov-evm/src/evm/transaction.rs b/module-system/module-implementations/sov-evm/src/evm/transaction.rs index 3c0b6925e2..39b14b13d1 100644 --- a/module-system/module-implementations/sov-evm/src/evm/transaction.rs +++ b/module-system/module-implementations/sov-evm/src/evm/transaction.rs @@ -1,12 +1,15 @@ -use reth_primitives::{Address, H256, U256}; +use std::ops::Range; + +use reth_primitives::{Address, Header, SealedHeader, TransactionSigned, H256}; +use revm::primitives::EVMError; #[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Clone)] pub(crate) struct BlockEnv { pub(crate) number: u64, pub(crate) coinbase: Address, - pub(crate) timestamp: U256, + pub(crate) timestamp: u64, /// Prevrandao is used after Paris (aka TheMerge) instead of the difficulty value. - pub(crate) prevrandao: Option, + pub(crate) prevrandao: H256, /// basefee is added in EIP1559 London upgrade pub(crate) basefee: u64, pub(crate) gas_limit: u64, @@ -18,7 +21,7 @@ impl Default for BlockEnv { number: Default::default(), coinbase: Default::default(), timestamp: Default::default(), - prevrandao: Some(Default::default()), + prevrandao: Default::default(), basefee: Default::default(), gas_limit: reth_primitives::constants::ETHEREUM_BLOCK_GAS_LIMIT, } @@ -29,11 +32,75 @@ impl Default for BlockEnv { #[cfg_attr( feature = "native", derive(serde::Serialize), - derive(serde::Deserialize), - derive(schemars::JsonSchema) + derive(serde::Deserialize) )] #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] pub struct RlpEvmTransaction { /// Rlp data. pub rlp: Vec, } + +#[cfg_attr( + feature = "native", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct TransactionSignedAndRecovered { + /// Signer of the transaction + pub(crate) signer: Address, + /// Signed transaction + pub(crate) signed_transaction: TransactionSigned, + /// Block the transaction was added to + pub(crate) block_number: u64, +} + +#[cfg_attr( + feature = "native", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct Block { + /// Block header. + pub(crate) header: Header, + + /// Transactions in this block. + pub(crate) transactions: Range, +} + +impl Block { + pub(crate) fn seal(self) -> SealedBlock { + SealedBlock { + header: self.header.seal_slow(), + transactions: self.transactions, + } + } +} + +#[cfg_attr( + feature = "native", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct SealedBlock { + /// Block header. + pub(crate) header: SealedHeader, + + /// Transactions in this block. + pub(crate) transactions: Range, +} + +#[cfg_attr( + feature = "native", + derive(serde::Serialize), + derive(serde::Deserialize) +)] +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct Receipt { + pub(crate) receipt: reth_primitives::Receipt, + pub(crate) gas_used: u64, + pub(crate) log_index_start: u64, + pub(crate) error: Option>, +} diff --git a/module-system/module-implementations/sov-evm/src/genesis.rs b/module-system/module-implementations/sov-evm/src/genesis.rs index b63e7c2626..bf1e8c6065 100644 --- a/module-system/module-implementations/sov-evm/src/genesis.rs +++ b/module-system/module-implementations/sov-evm/src/genesis.rs @@ -1,11 +1,11 @@ use anyhow::Result; use reth_primitives::constants::{EMPTY_RECEIPTS, EMPTY_TRANSACTIONS}; use reth_primitives::{Bloom, Bytes, EMPTY_OMMER_ROOT, H256, KECCAK_EMPTY, U256}; -use reth_rpc_types::{Block, BlockTransactions, Header}; use revm::primitives::SpecId; use sov_state::WorkingSet; use crate::evm::db_init::InitEvmDb; +use crate::evm::transaction::Block; use crate::evm::{AccountInfo, EvmChainConfig}; use crate::Evm; @@ -55,7 +55,6 @@ impl Evm { self.cfg.set(&chain_cfg, working_set); let genesis_block_number = 0u64; - self.head_number.set(&genesis_block_number, working_set); let header = reth_primitives::Header { parent_hash: H256::default(), @@ -77,20 +76,23 @@ impl Evm { extra_data: Bytes::default(), }; - let header = header.seal_slow(); + let block = Block { + header, + transactions: 0u64..0u64, + }; + + self.head.set(&block, working_set); + + let sealead_block = block.seal(); + + let mut accessory_state = working_set.accessory_state(); - self.blocks.set( + self.block_hashes.set( + &sealead_block.header.hash, &genesis_block_number, - &Block { - header: Header::from_primitive_with_hash(header), - total_difficulty: None, - uncles: vec![], - transactions: BlockTransactions::Hashes(vec![]), - size: None, - withdrawals: None, - }, - &mut working_set.accessory_state(), + &mut accessory_state, ); + self.blocks.push(&sealead_block, &mut accessory_state); Ok(()) } diff --git a/module-system/module-implementations/sov-evm/src/hooks.rs b/module-system/module-implementations/sov-evm/src/hooks.rs index 028f12bab6..a60a440904 100644 --- a/module-system/module-implementations/sov-evm/src/hooks.rs +++ b/module-system/module-implementations/sov-evm/src/hooks.rs @@ -1,9 +1,8 @@ -use reth_primitives::U256; -use reth_rpc_types::Transaction; -use sov_state::WorkingSet; +use reth_primitives::{Bloom, Bytes, U256}; +use sov_state::{AccessoryWorkingSet, WorkingSet}; -use crate::evm::conversions::to_u64; -use crate::evm::transaction::BlockEnv; +use crate::evm::transaction::{Block, BlockEnv}; +use crate::experimental::PendingTransaction; use crate::Evm; impl Evm { @@ -12,52 +11,135 @@ impl Evm { da_root_hash: [u8; 32], working_set: &mut WorkingSet, ) { - let block_number: u64 = self.head_number.get(working_set).unwrap(); - let parent_block: reth_rpc_types::Block = self - .blocks - .get(&block_number, &mut working_set.accessory_state()) + let parent_block = self + .head + .get(working_set) .expect("Head block should always be set"); + + // parent_block.header.state_root = root_hash.into(); + // self.head.set(&parent_block, working_set); + let cfg = self.cfg.get(working_set).unwrap_or_default(); let new_pending_block = BlockEnv { - number: block_number + 1, + number: parent_block.header.number + 1, coinbase: cfg.coinbase, - timestamp: parent_block.header.timestamp + U256::from(cfg.block_timestamp_delta), - prevrandao: Some(da_root_hash.into()), - basefee: reth_primitives::basefee::calculate_next_block_base_fee( - to_u64(parent_block.header.gas_used), - cfg.block_gas_limit, - parent_block - .header - .base_fee_per_gas - .map_or(reth_primitives::constants::MIN_PROTOCOL_BASE_FEE, to_u64), - ), + timestamp: parent_block.header.timestamp + cfg.block_timestamp_delta, + prevrandao: da_root_hash.into(), + basefee: parent_block.header.next_block_base_fee().unwrap(), gas_limit: cfg.block_gas_limit, }; self.pending_block.set(&new_pending_block, working_set); } - pub fn end_slot_hook(&self, _root_hash: [u8; 32], working_set: &mut WorkingSet) { - // TODO implement block creation logic. + pub fn end_slot_hook(&self, working_set: &mut WorkingSet) { + let pending_block = self + .pending_block + .get(working_set) + .expect("Pending block should always be sets"); + + let parent_block = self + .head + .get(working_set) + .expect("Head block should always be set") + .seal(); + + let pending_transactions: Vec = + self.pending_transactions.iter(working_set).collect(); + + self.pending_transactions.clear(working_set); + + let start_tx_index = parent_block.transactions.end; + + let gas_used = pending_transactions + .last() + .map_or(0u64, |tx| tx.receipt.receipt.cumulative_gas_used); + + let transactions: Vec<&reth_primitives::TransactionSigned> = pending_transactions + .iter() + .map(|tx| &tx.transaction.signed_transaction) + .collect(); + + let receipts: Vec = pending_transactions + .iter() + .map(|tx| tx.receipt.receipt.clone().with_bloom()) + .collect(); + + let header = reth_primitives::Header { + parent_hash: parent_block.header.hash, + timestamp: pending_block.timestamp, + number: pending_block.number, + ommers_hash: reth_primitives::constants::EMPTY_OMMER_ROOT, + beneficiary: parent_block.header.beneficiary, + // This will be set in finalize_slot_hook or in the next begin_slot_hook + state_root: reth_primitives::constants::KECCAK_EMPTY, + transactions_root: reth_primitives::proofs::calculate_transaction_root( + transactions.as_slice(), + ), + receipts_root: reth_primitives::proofs::calculate_receipt_root(receipts.as_slice()), + withdrawals_root: None, + logs_bloom: receipts + .iter() + .fold(Bloom::zero(), |bloom, r| bloom | r.bloom), + difficulty: U256::ZERO, + gas_limit: pending_block.gas_limit, + gas_used, + mix_hash: pending_block.prevrandao, + nonce: 0, + base_fee_per_gas: parent_block.header.next_block_base_fee(), + extra_data: Bytes::default(), + }; + + let block = Block { + header, + transactions: start_tx_index..start_tx_index + pending_transactions.len() as u64, + }; + + self.head.set(&block, working_set); - let mut transactions: Vec = Vec::default(); + let mut accessory_state = working_set.accessory_state(); + self.pending_head.set(&block, &mut accessory_state); - while let Some(mut tx) = self - .pending_transactions - .pop(&mut working_set.accessory_state()) + let mut tx_index = start_tx_index; + for PendingTransaction { + transaction, + receipt, + } in &pending_transactions { - tx.block_hash = Some(reth_primitives::H256::default()); - tx.block_number = Some(reth_primitives::U256::from(1)); - tx.transaction_index = Some(reth_primitives::U256::from(1)); - - // TODO fill all data that is set by: from_recovered_with_block_context - // tx.gas_price - // tx.max_fee_per_gas - transactions.push(tx); - } + self.transactions.push(transaction, &mut accessory_state); + self.receipts.push(receipt, &mut accessory_state); - for tx in transactions { - self.transactions - .set(&tx.hash, &tx, &mut working_set.accessory_state()); + self.transaction_hashes.set( + &transaction.signed_transaction.hash, + &tx_index, + &mut accessory_state, + ); + + tx_index += 1 } + + self.pending_transactions.clear(working_set); + } + + pub fn finalize_slot_hook( + &self, + root_hash: [u8; 32], + accesorry_working_set: &mut AccessoryWorkingSet, + ) { + let mut block = self + .pending_head + .get(accesorry_working_set) + .expect("Pending head must be set"); + + block.header.state_root = root_hash.into(); + + let sealed_block = block.seal(); + + self.blocks.push(&sealed_block, accesorry_working_set); + self.block_hashes.set( + &sealed_block.header.hash, + &sealed_block.header.number, + accesorry_working_set, + ); + self.pending_head.delete(accesorry_working_set); } } diff --git a/module-system/module-implementations/sov-evm/src/lib.rs b/module-system/module-implementations/sov-evm/src/lib.rs index f0dfaadbff..686c5857b8 100644 --- a/module-system/module-implementations/sov-evm/src/lib.rs +++ b/module-system/module-implementations/sov-evm/src/lib.rs @@ -28,12 +28,13 @@ mod experimental { use reth_primitives::{Address, H256}; use revm::primitives::{SpecId, KECCAK_EMPTY, U256}; use sov_modules_api::{Error, ModuleInfo}; - use sov_state::codec::{BcsCodec, JsonCodec}; + use sov_state::codec::BcsCodec; use sov_state::WorkingSet; use super::evm::db::EvmDb; use super::evm::transaction::BlockEnv; use super::evm::{DbAccount, EvmChainConfig}; + use crate::evm::transaction::{Block, Receipt, SealedBlock, TransactionSignedAndRecovered}; #[derive(Clone, Debug)] pub struct AccountData { pub address: Address, @@ -66,6 +67,12 @@ mod experimental { pub block_timestamp_delta: u64, } + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + pub(crate) struct PendingTransaction { + pub(crate) transaction: TransactionSignedAndRecovered, + pub(crate) receipt: Receipt, + } + impl Default for EvmConfig { fn default() -> Self { Self { @@ -83,7 +90,7 @@ mod experimental { } #[allow(dead_code)] - #[cfg_attr(feature = "native", derive(sov_modules_api::ModuleCallJsonSchema))] + // #[cfg_attr(feature = "native", derive(sov_modules_api::ModuleCallJsonSchema))] #[derive(ModuleInfo, Clone)] pub struct Evm { #[address] @@ -99,36 +106,30 @@ mod experimental { pub(crate) pending_block: sov_state::StateValue, #[state] - pub(crate) head_number: sov_state::StateValue, + pub(crate) pending_transactions: sov_state::StateVec, + + #[state] + pub(crate) head: sov_state::StateValue, + + #[state] + pub(crate) pending_head: sov_state::AccessoryStateValue, - // TODO JsonCodec: This is a workaround for https://github.com/bincode-org/bincode/issues/245 which affects all - // binary serialization formats. - // 1. Implement custom types for Block, Transaction etc.. with borsh derived. - // 2. Remove JsonCodec. #[state] - pub(crate) blocks: sov_state::AccessoryStateMap, + pub(crate) blocks: sov_state::AccessoryStateVec, #[state] - pub(crate) block_hashes: - sov_state::AccessoryStateMap, + pub(crate) block_hashes: sov_state::AccessoryStateMap, #[state] - pub(crate) pending_transactions: - sov_state::AccessoryStateVec, + pub(crate) transactions: + sov_state::AccessoryStateVec, #[state] - pub(crate) transactions: sov_state::AccessoryStateMap< - reth_primitives::H256, - reth_rpc_types::Transaction, - JsonCodec, - >, + pub(crate) transaction_hashes: + sov_state::AccessoryStateMap, #[state] - pub(crate) receipts: sov_state::AccessoryStateMap< - reth_primitives::U256, - reth_rpc_types::TransactionReceipt, - JsonCodec, - >, + pub(crate) receipts: sov_state::AccessoryStateVec, #[state] pub(crate) code: diff --git a/module-system/module-implementations/sov-evm/src/query.rs b/module-system/module-implementations/sov-evm/src/query.rs index 81abf79d2b..3ccadc3251 100644 --- a/module-system/module-implementations/sov-evm/src/query.rs +++ b/module-system/module-implementations/sov-evm/src/query.rs @@ -1,10 +1,15 @@ +use ethereum_types::U64; use jsonrpsee::core::RpcResult; +use reth_primitives::contract::create_address; +use reth_primitives::TransactionKind::{Call, Create}; +use reth_primitives::{TransactionSignedEcRecovered, U128, U256}; use sov_modules_api::macros::rpc_gen; use sov_state::WorkingSet; use tracing::info; use crate::call::get_cfg_env; use crate::evm::db::EvmDb; +use crate::evm::transaction::{Receipt, SealedBlock, TransactionSignedAndRecovered}; use crate::evm::{executor, prepare_call_env}; use crate::Evm; @@ -86,23 +91,69 @@ impl Evm { working_set: &mut WorkingSet, ) -> RpcResult> { info!("evm module: eth_getTransactionByHash"); - //let evm_transaction = self.transactions.get(&hash, working_set); - let evm_transaction = self - .transactions - .get(&hash, &mut working_set.accessory_state()); - Ok(evm_transaction) + let mut accessory_state = working_set.accessory_state(); + + let tx_number = self.transaction_hashes.get(&hash, &mut accessory_state); + + let transaction = tx_number.map(|number| { + let tx = self + .transactions + .get(number as usize, &mut accessory_state) + .unwrap_or_else(|| panic!("Transaction with known hash {} and number {} must be set in all {} transaction", + hash, + number, + self.transactions.len(&mut accessory_state))); + + let block = self + .blocks + .get(tx.block_number as usize, &mut accessory_state) + .unwrap_or_else(|| panic!("Block with number {} for known transaction {} must be set", + tx.block_number, + tx.signed_transaction.hash)); + + reth_rpc_types::Transaction::from_recovered_with_block_context( + tx.into(), + block.header.hash, + block.header.number, + block.header.base_fee_per_gas, + U256::from(tx_number.unwrap() - block.transactions.start), + ) + }); + + Ok(transaction) } // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 #[rpc_method(name = "getTransactionReceipt")] pub fn get_transaction_receipt( &self, - hash: reth_primitives::U256, + hash: reth_primitives::H256, working_set: &mut WorkingSet, ) -> RpcResult> { info!("evm module: eth_getTransactionReceipt"); - let receipt = self.receipts.get(&hash, &mut working_set.accessory_state()); + let mut accessory_state = working_set.accessory_state(); + + let tx_number = self.transaction_hashes.get(&hash, &mut accessory_state); + + let receipt = tx_number.map(|number| { + let tx = self + .transactions + .get(number as usize, &mut accessory_state) + .expect("Transaction with known hash must be set"); + let block = self + .blocks + .get(tx.block_number as usize, &mut accessory_state) + .expect("Block number for known transaction must be set"); + + let receipt = self + .receipts + .get(tx_number.unwrap() as usize, &mut accessory_state) + .expect("Receipt for known transaction must be set"); + + build_rpc_receipt(block, tx, tx_number.unwrap(), receipt) + }); + Ok(receipt) } @@ -145,7 +196,7 @@ impl Evm { let evm_db: EvmDb<'_, C> = self.get_db(working_set); // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/505 - let result = executor::inspect(evm_db, block_env, tx_env, cfg_env).unwrap(); + let result = executor::inspect(evm_db, &block_env, tx_env, cfg_env).unwrap(); let output = match result.result { revm::primitives::ExecutionResult::Success { output, .. } => output, _ => todo!(), @@ -183,3 +234,65 @@ impl Evm { unimplemented!("eth_sendTransaction not implemented") } } + +// modified from: https://github.com/paradigmxyz/reth/blob/cc576bc8690a3e16e6e5bf1cbbbfdd029e85e3d4/crates/rpc/rpc/src/eth/api/transactions.rs#L849 +pub(crate) fn build_rpc_receipt( + block: SealedBlock, + tx: TransactionSignedAndRecovered, + tx_number: u64, + receipt: Receipt, +) -> reth_rpc_types::TransactionReceipt { + let transaction: TransactionSignedEcRecovered = tx.into(); + let transaction_kind = transaction.kind(); + + let transaction_hash = Some(transaction.hash); + let transaction_index = Some(U256::from(tx_number - block.transactions.start)); + let block_hash = Some(block.header.hash); + let block_number = Some(U256::from(block.header.number)); + + reth_rpc_types::TransactionReceipt { + transaction_hash, + transaction_index, + block_hash, + block_number, + from: transaction.signer(), + to: match transaction_kind { + Create => None, + Call(addr) => Some(*addr), + }, + cumulative_gas_used: U256::from(receipt.receipt.cumulative_gas_used), + gas_used: Some(U256::from(receipt.gas_used)), + contract_address: match transaction_kind { + Create => Some(create_address(transaction.signer(), transaction.nonce())), + Call(_) => None, + }, + effective_gas_price: U128::from( + transaction.effective_gas_price(block.header.base_fee_per_gas), + ), + transaction_type: transaction.tx_type().into(), + logs_bloom: receipt.receipt.bloom_slow(), + status_code: if receipt.receipt.success { + Some(U64::from(1)) + } else { + Some(U64::from(0)) + }, + state_root: None, // Pre https://eips.ethereum.org/EIPS/eip-658 (pre-byzantium) and won't be used + logs: receipt + .receipt + .logs + .into_iter() + .enumerate() + .map(|(idx, log)| reth_rpc_types::Log { + address: log.address, + topics: log.topics, + data: log.data, + block_hash, + block_number, + transaction_hash, + transaction_index, + log_index: Some(U256::from(receipt.log_index_start + idx as u64)), + removed: false, + }) + .collect(), + } +} diff --git a/module-system/module-implementations/sov-evm/src/tests/call_tests.rs b/module-system/module-implementations/sov-evm/src/tests/call_tests.rs index d063ed482d..4ab68d00aa 100644 --- a/module-system/module-implementations/sov-evm/src/tests/call_tests.rs +++ b/module-system/module-implementations/sov-evm/src/tests/call_tests.rs @@ -2,30 +2,34 @@ use reth_primitives::{Address, TransactionKind}; use revm::primitives::{SpecId, KECCAK_EMPTY, U256}; use sov_modules_api::default_context::DefaultContext; use sov_modules_api::default_signature::private_key::DefaultPrivateKey; -use sov_modules_api::{Context, Module, PrivateKey, Spec}; -use sov_state::{ProverStorage, WorkingSet}; +use sov_modules_api::{Context, Module, PrivateKey, PublicKey, Spec}; use crate::call::CallMessage; +use crate::evm::transaction::Receipt; use crate::smart_contracts::SimpleStorageContract; use crate::tests::dev_signer::TestSigner; -use crate::{AccountData, Evm, EvmConfig}; +use crate::tests::genesis_tests::get_evm; +use crate::{AccountData, EvmConfig}; type C = DefaultContext; fn create_messages( contract_addr: Address, set_arg: u32, dev_signer: TestSigner, + create_contract: bool, ) -> Vec { let mut transactions = Vec::default(); let contract = SimpleStorageContract::default(); + let mut nonce = 0; // Contract creation. - { + if create_contract { let signed_tx = dev_signer .sign_default_transaction(TransactionKind::Create, contract.byte_code().to_vec(), 0) .unwrap(); transactions.push(CallMessage { tx: signed_tx }); + nonce += 1; } // Update contract state. @@ -34,7 +38,7 @@ fn create_messages( .sign_default_transaction( TransactionKind::Call(contract_addr), hex::decode(hex::encode(&contract.set_call_data(set_arg))).unwrap(), - 1, + nonce, ) .unwrap(); @@ -46,36 +50,22 @@ fn create_messages( #[test] fn evm_test() { - use sov_modules_api::PublicKey; - let tmpdir = tempfile::tempdir().unwrap(); - let working_set = &mut WorkingSet::new(ProverStorage::with_path(tmpdir.path()).unwrap()); - - let priv_key = DefaultPrivateKey::generate(); - - let sender = priv_key.pub_key(); - let sender_addr = sender.to_address::<::Address>(); - let sender_context = C::new(sender_addr); - let dev_signer: TestSigner = TestSigner::new_random(); - let caller = dev_signer.address(); - - let evm = Evm::::default(); - - let data = AccountData { - address: caller, - balance: U256::from(1000000000), - code_hash: KECCAK_EMPTY, - code: vec![], - nonce: 0, - }; let config = EvmConfig { - data: vec![data], + data: vec![AccountData { + address: dev_signer.address(), + balance: U256::from(1000000000), + code_hash: KECCAK_EMPTY, + code: vec![], + nonce: 0, + }], spec: vec![(0, SpecId::LATEST)].into_iter().collect(), ..Default::default() }; - evm.genesis(&config, working_set).unwrap(); + let (evm, mut working_set) = get_evm(&config); + let working_set = &mut working_set; let contract_addr: Address = Address::from_slice( hex::decode("819c5497b157177315e1204f52e588b393771719") @@ -83,14 +73,103 @@ fn evm_test() { .as_slice(), ); + evm.begin_slot_hook([5u8; 32], working_set); + let set_arg = 999; + let sender_context = C::new( + DefaultPrivateKey::generate() + .pub_key() + .to_address::<::Address>(), + ); - for tx in create_messages(contract_addr, set_arg, dev_signer) { + for tx in create_messages(contract_addr, set_arg, dev_signer, true) { evm.call(tx, &sender_context, working_set).unwrap(); } + evm.end_slot_hook(working_set); + let db_account = evm.accounts.get(&contract_addr, working_set).unwrap(); let storage_value = db_account.storage.get(&U256::ZERO, working_set).unwrap(); - assert_eq!(U256::from(set_arg), storage_value) + assert_eq!(U256::from(set_arg), storage_value); + assert_eq!( + evm.receipts + .iter(&mut working_set.accessory_state()) + .collect::>(), + [ + Receipt { + receipt: reth_primitives::Receipt { + tx_type: reth_primitives::TxType::EIP1559, + success: true, + cumulative_gas_used: 132943, + logs: vec![] + }, + gas_used: 132943, + log_index_start: 0, + error: None + }, + Receipt { + receipt: reth_primitives::Receipt { + tx_type: reth_primitives::TxType::EIP1559, + success: true, + cumulative_gas_used: 176673, + logs: vec![] + }, + gas_used: 43730, + log_index_start: 0, + error: None + } + ] + ) +} + +#[test] +fn failed_transaction_test() { + let dev_signer: TestSigner = TestSigner::new_random(); + + let (evm, mut working_set) = get_evm(&EvmConfig::default()); + let working_set = &mut working_set; + + let contract_addr: Address = Address::from_slice( + hex::decode("819c5497b157177315e1204f52e588b393771719") + .unwrap() + .as_slice(), + ); + + evm.begin_slot_hook([5u8; 32], working_set); + + let set_arg = 999; + let sender_context = C::new( + DefaultPrivateKey::generate() + .pub_key() + .to_address::<::Address>(), + ); + + for tx in create_messages(contract_addr, set_arg, dev_signer, false) { + evm.call(tx, &sender_context, working_set).unwrap(); + } + + evm.end_slot_hook(working_set); + + assert_eq!( + evm.receipts + .iter(&mut working_set.accessory_state()) + .collect::>(), + [Receipt { + receipt: reth_primitives::Receipt { + tx_type: reth_primitives::TxType::EIP1559, + success: false, + cumulative_gas_used: 0, + logs: vec![] + }, + gas_used: 0, + log_index_start: 0, + error: Some(revm::primitives::EVMError::Transaction( + revm::primitives::InvalidTransaction::LackOfFundForGasLimit { + gas_limit: U256::from(0xd59f80), + balance: U256::ZERO + } + )) + }] + ) } diff --git a/module-system/module-implementations/sov-evm/src/tests/dev_signer.rs b/module-system/module-implementations/sov-evm/src/tests/dev_signer.rs index 942ba34c74..e7e92e6484 100644 --- a/module-system/module-implementations/sov-evm/src/tests/dev_signer.rs +++ b/module-system/module-implementations/sov-evm/src/tests/dev_signer.rs @@ -51,7 +51,8 @@ impl TestSigner { input: RethBytes::from(data), nonce, chain_id: 1, - gas_limit: reth_primitives::constants::ETHEREUM_BLOCK_GAS_LIMIT / 2, + gas_limit: 1_000_000u64, + max_fee_per_gas: u128::from(reth_primitives::constants::MIN_PROTOCOL_BASE_FEE * 2), ..Default::default() }; diff --git a/module-system/module-implementations/sov-evm/src/tests/genesis_tests.rs b/module-system/module-implementations/sov-evm/src/tests/genesis_tests.rs index 2153226b15..f797a99f6d 100644 --- a/module-system/module-implementations/sov-evm/src/tests/genesis_tests.rs +++ b/module-system/module-implementations/sov-evm/src/tests/genesis_tests.rs @@ -1,14 +1,13 @@ -use ethereum_types::H64; use lazy_static::lazy_static; use reth_primitives::constants::{EMPTY_RECEIPTS, EMPTY_TRANSACTIONS, ETHEREUM_BLOCK_GAS_LIMIT}; use reth_primitives::hex_literal::hex; -use reth_primitives::{Address, Bloom, Bytes, EMPTY_OMMER_ROOT, H256}; -use reth_rpc_types::{Block, BlockTransactions, Header}; +use reth_primitives::{Address, Bloom, Bytes, Header, SealedHeader, EMPTY_OMMER_ROOT, H256}; use revm::primitives::{SpecId, KECCAK_EMPTY, U256}; use sov_modules_api::default_context::DefaultContext; use sov_modules_api::Module; use sov_state::{DefaultStorageSpec, ProverStorage, WorkingSet}; +use crate::evm::transaction::{Block, SealedBlock}; // use crate::evm::db; use crate::{evm::EvmChainConfig, AccountData, Evm, EvmConfig}; type C = DefaultContext; @@ -35,6 +34,14 @@ lazy_static! { }; } +pub(crate) const GENESIS_HASH: H256 = H256(hex!( + "d57423e4375c45bc114cd137146aab671dbd3f6304f05b31bdd416301b4a99f0" +)); + +lazy_static! { + pub(crate) static ref BENEFICIARY: Address = Address::from([3u8; 20]); +} + #[test] fn genesis_data() { get_evm(&TEST_CONFIG); @@ -73,14 +80,57 @@ fn genesis_cfg_missing_specs() { #[test] fn genesis_block() { let (evm, mut working_set) = get_evm(&TEST_CONFIG); + let mut accessory_state = working_set.accessory_state(); + let block_number = evm + .block_hashes + .get(&GENESIS_HASH, &mut accessory_state) + .unwrap(); let block = evm .blocks - .get(&0u64, &mut working_set.accessory_state()) + .get(block_number as usize, &mut accessory_state) .unwrap(); + assert_eq!(block_number, 0); + assert_eq!( block, + SealedBlock { + header: SealedHeader { + header: Header { + parent_hash: H256::default(), + state_root: KECCAK_EMPTY, + transactions_root: EMPTY_TRANSACTIONS, + receipts_root: EMPTY_RECEIPTS, + logs_bloom: Bloom::default(), + difficulty: U256::ZERO, + number: 0, + gas_limit: ETHEREUM_BLOCK_GAS_LIMIT, + gas_used: 0, + timestamp: 50, + extra_data: Bytes::default(), + mix_hash: H256::default(), + nonce: 0, + base_fee_per_gas: Some(70), + ommers_hash: EMPTY_OMMER_ROOT, + beneficiary: *BENEFICIARY, + withdrawals_root: None + }, + hash: GENESIS_HASH + }, + transactions: (0u64..0u64), + } + ); +} + +#[test] +fn genesis_head() { + let (evm, mut working_set) = get_evm(&TEST_CONFIG); + + let head = evm.head.get(&mut working_set).unwrap(); + + assert_eq!( + head, Block { header: Header { parent_hash: H256::default(), @@ -89,26 +139,19 @@ fn genesis_block() { receipts_root: EMPTY_RECEIPTS, logs_bloom: Bloom::default(), difficulty: U256::ZERO, - number: Some(U256::ZERO), - gas_limit: U256::from(ETHEREUM_BLOCK_GAS_LIMIT), - gas_used: U256::ZERO, - timestamp: U256::from(50), + number: 0, + gas_limit: ETHEREUM_BLOCK_GAS_LIMIT, + gas_used: 0, + timestamp: 50, extra_data: Bytes::default(), mix_hash: H256::default(), - nonce: Some(H64::default()), - base_fee_per_gas: Some(U256::from(70)), - hash: Some(H256(hex!( - "d57423e4375c45bc114cd137146aab671dbd3f6304f05b31bdd416301b4a99f0" - ))), - uncles_hash: EMPTY_OMMER_ROOT, - miner: Address::from([3u8; 20]), + nonce: 0, + base_fee_per_gas: Some(70), + ommers_hash: EMPTY_OMMER_ROOT, + beneficiary: *BENEFICIARY, withdrawals_root: None }, - transactions: BlockTransactions::Hashes(vec![]), - total_difficulty: None, - uncles: vec![], - size: None, - withdrawals: None + transactions: (0u64..0u64), } ); } diff --git a/module-system/module-implementations/sov-evm/src/tests/hooks_tests.rs b/module-system/module-implementations/sov-evm/src/tests/hooks_tests.rs index aac7188fe7..5d7676026e 100644 --- a/module-system/module-implementations/sov-evm/src/tests/hooks_tests.rs +++ b/module-system/module-implementations/sov-evm/src/tests/hooks_tests.rs @@ -1,22 +1,169 @@ -use reth_primitives::{Address, H256, U256}; +use lazy_static::lazy_static; +use reth_primitives::hex_literal::hex; +use reth_primitives::{ + Address, Bloom, Bytes, Header, Signature, TransactionSigned, EMPTY_OMMER_ROOT, H256, + KECCAK_EMPTY, U256, +}; use super::genesis_tests::{get_evm, TEST_CONFIG}; -use crate::evm::transaction::BlockEnv; +use crate::evm::transaction::{Block, BlockEnv, Receipt, TransactionSignedAndRecovered}; +use crate::experimental::PendingTransaction; +use crate::tests::genesis_tests::{BENEFICIARY, GENESIS_HASH}; + +lazy_static! { + pub(crate) static ref DA_ROOT_HASH: H256 = H256::from([5u8; 32]); +} #[test] fn begin_slot_hook_creates_pending_block() { let (evm, mut working_set) = get_evm(&TEST_CONFIG); - evm.begin_slot_hook([5u8; 32], &mut working_set); + evm.begin_slot_hook(DA_ROOT_HASH.0, &mut working_set); let pending_block = evm.pending_block.get(&mut working_set).unwrap(); assert_eq!( pending_block, BlockEnv { number: 1, - coinbase: Address::from([3u8; 20]), - timestamp: U256::from(52), - prevrandao: Some(H256::from([5u8; 32])), - basefee: 62, - gas_limit: 30000000, + coinbase: *BENEFICIARY, + timestamp: TEST_CONFIG.genesis_timestamp + TEST_CONFIG.block_timestamp_delta, + prevrandao: *DA_ROOT_HASH, + basefee: 62u64, + gas_limit: TEST_CONFIG.block_gas_limit, } ); } + +#[test] +fn end_slot_hook_sets_head() { + let (evm, mut working_set) = get_evm(&TEST_CONFIG); + evm.begin_slot_hook(DA_ROOT_HASH.0, &mut working_set); + + evm.pending_transactions.push( + &create_pending_transaction(H256::from([1u8; 32]), 1), + &mut working_set, + ); + + evm.pending_transactions.push( + &create_pending_transaction(H256::from([2u8; 32]), 2), + &mut working_set, + ); + + evm.end_slot_hook(&mut working_set); + let head = evm.head.get(&mut working_set).unwrap(); + let pending_head = evm + .pending_head + .get(&mut working_set.accessory_state()) + .unwrap(); + + assert_eq!(head, pending_head); + assert_eq!( + head, + Block { + header: Header { + parent_hash: GENESIS_HASH, + ommers_hash: EMPTY_OMMER_ROOT, + beneficiary: TEST_CONFIG.coinbase, + state_root: KECCAK_EMPTY, + transactions_root: H256(hex!( + "30eb5f6050df7ea18ca34cf3503f4713119315a2d3c11f892c5c8920acf816f4" + )), + receipts_root: H256(hex!( + "27036187b3f5e87d4306b396cf06c806da2cc9a0fef9b07c042e3b4304e01c64" + )), + withdrawals_root: None, + logs_bloom: Bloom::default(), + difficulty: U256::ZERO, + number: 1, + gas_limit: TEST_CONFIG.block_gas_limit, + gas_used: 200u64, + timestamp: TEST_CONFIG.genesis_timestamp + TEST_CONFIG.block_timestamp_delta, + mix_hash: *DA_ROOT_HASH, + nonce: 0, + base_fee_per_gas: Some(62u64), + extra_data: Bytes::default() + }, + transactions: 0..2 + } + ); +} + +#[test] +fn end_slot_hook_moves_transactions_and_receipts() { + let (evm, mut working_set) = get_evm(&TEST_CONFIG); + evm.begin_slot_hook(DA_ROOT_HASH.0, &mut working_set); + + let tx1 = create_pending_transaction(H256::from([1u8; 32]), 1); + evm.pending_transactions.push(&tx1, &mut working_set); + + let tx2 = create_pending_transaction(H256::from([2u8; 32]), 2); + evm.pending_transactions.push(&tx2, &mut working_set); + + evm.end_slot_hook(&mut working_set); + + let tx1_hash = tx1.transaction.signed_transaction.hash; + let tx2_hash = tx2.transaction.signed_transaction.hash; + + assert_eq!( + evm.transactions + .iter(&mut working_set.accessory_state()) + .collect::>(), + [tx1.transaction, tx2.transaction] + ); + + assert_eq!( + evm.receipts + .iter(&mut working_set.accessory_state()) + .collect::>(), + [tx1.receipt, tx2.receipt] + ); + + assert_eq!( + evm.transaction_hashes + .get(&tx1_hash, &mut working_set.accessory_state()) + .unwrap(), + 0 + ); + + assert_eq!( + evm.transaction_hashes + .get(&tx2_hash, &mut working_set.accessory_state()) + .unwrap(), + 1 + ); + + assert_eq!(evm.pending_transactions.len(&mut working_set), 0); +} + +fn create_pending_transaction(hash: H256, index: u64) -> PendingTransaction { + PendingTransaction { + transaction: TransactionSignedAndRecovered { + signer: Address::from([1u8; 20]), + signed_transaction: TransactionSigned { + hash, + signature: Signature::default(), + transaction: reth_primitives::Transaction::Eip1559(reth_primitives::TxEip1559 { + chain_id: 1u64, + nonce: 1u64, + gas_limit: 1000u64, + max_fee_per_gas: 2000u64 as u128, + max_priority_fee_per_gas: 3000u64 as u128, + to: reth_primitives::TransactionKind::Call(Address::from([3u8; 20])), + value: 4000u64 as u128, + access_list: reth_primitives::AccessList::default(), + input: Bytes::from([4u8; 20]), + }), + }, + block_number: 1, + }, + receipt: Receipt { + receipt: reth_primitives::Receipt { + tx_type: reth_primitives::TxType::EIP1559, + success: true, + cumulative_gas_used: 100u64 * index, + logs: vec![], + }, + gas_used: 100u64, + log_index_start: 0, + error: None, + }, + } +} diff --git a/module-system/module-schemas/build.rs b/module-system/module-schemas/build.rs index 5fb771f9a0..6d46632d89 100644 --- a/module-system/module-schemas/build.rs +++ b/module-system/module-schemas/build.rs @@ -8,7 +8,6 @@ use sov_rollup_interface::mocks::MockZkvm; fn main() -> io::Result<()> { store_json_schema::>("sov-bank.json")?; store_json_schema::>("sov-accounts.json")?; - store_json_schema::>("sov-evm.json")?; store_json_schema::>("sov-value-setter.json")?; store_json_schema::>( "sov-prover-incentives.json", diff --git a/module-system/sov-modules-api/src/hooks.rs b/module-system/sov-modules-api/src/hooks.rs index deb51ea3f7..aff67984cf 100644 --- a/module-system/sov-modules-api/src/hooks.rs +++ b/module-system/sov-modules-api/src/hooks.rs @@ -1,5 +1,5 @@ use sov_rollup_interface::da::{BlobReaderTrait, DaSpec}; -use sov_state::WorkingSet; +use sov_state::{AccessoryWorkingSet, WorkingSet}; use crate::transaction::Transaction; use crate::{Context, Spec}; @@ -62,9 +62,11 @@ pub trait SlotHooks { working_set: &mut WorkingSet<::Storage>, ); - fn end_slot_hook( + fn end_slot_hook(&self, working_set: &mut WorkingSet<::Storage>); + + fn finalize_slot_hook( &self, root_hash: [u8; 32], - working_set: &mut WorkingSet<::Storage>, + accesorry_working_set: &mut AccessoryWorkingSet<::Storage>, ); } diff --git a/module-system/sov-modules-stf-template/src/lib.rs b/module-system/sov-modules-stf-template/src/lib.rs index 5a06b555ab..a917baa46f 100644 --- a/module-system/sov-modules-stf-template/src/lib.rs +++ b/module-system/sov-modules-stf-template/src/lib.rs @@ -96,17 +96,24 @@ where #[cfg_attr(all(target_os = "zkvm", feature = "bench"), cycle_tracker)] fn end_slot(&mut self) -> (jmt::RootHash, <::Storage as Storage>::Witness) { - let mut checkpoint = self.checkpoint.take().unwrap(); - let (cache_log, witness) = checkpoint.freeze(); + let checkpoint = self.checkpoint.take().unwrap(); + // Run end end_slot_hook let mut working_set = checkpoint.to_revertable(); + self.runtime.end_slot_hook(&mut working_set); + // Save checkpoint + let mut checkpoint = working_set.checkpoint(); + + let (cache_log, witness) = checkpoint.freeze(); let (root_hash, authenticated_node_batch) = self .current_storage .compute_state_update(cache_log, &witness) .expect("jellyfish merkle tree update must succeed"); - self.runtime.end_slot_hook(root_hash, &mut working_set); + let mut working_set = checkpoint.to_revertable(); + self.runtime + .finalize_slot_hook([0; 32], &mut working_set.accessory_state()); let accessory_log = working_set.checkpoint().freeze_non_provable(); diff --git a/module-system/sov-state/src/containers/accessory_vec.rs b/module-system/sov-state/src/containers/accessory_vec.rs index 3028d83b73..a5a6bc558a 100644 --- a/module-system/sov-state/src/containers/accessory_vec.rs +++ b/module-system/sov-state/src/containers/accessory_vec.rs @@ -179,6 +179,16 @@ where next_i: 0, } } + + pub fn last(&self, working_set: &mut AccessoryWorkingSet) -> Option { + let len = self.len(working_set); + + if len == 0usize { + None + } else { + self.elems.get(&(len - 1), working_set) + } + } } /// An [`Iterator`] over a [`AccessoryStateVec`] @@ -237,6 +247,23 @@ where { } +impl<'a, 'ws, V, Codec, S> DoubleEndedIterator for AccessoryStateVecIter<'a, 'ws, V, Codec, S> +where + Codec: StateCodec + Clone, + Codec::ValueCodec: StateValueCodec + StateValueCodec, + Codec::KeyCodec: StateKeyCodec, + S: Storage, +{ + fn next_back(&mut self) -> Option { + if self.len == 0 { + return None; + } + + self.len -= 1; + self.state_vec.get(self.len, self.ws) + } +} + #[cfg(all(test, feature = "native"))] mod test { use std::fmt::Debug; @@ -247,10 +274,12 @@ mod test { enum TestCaseAction { Push(T), Pop(T), + Last(T), Set(usize, T), SetAll(Vec), CheckLen(usize), CheckContents(Vec), + CheckContentsReverse(Vec), CheckGet(usize, Option), Clear, } @@ -284,6 +313,8 @@ mod test { TestCaseAction::CheckGet(0, None), TestCaseAction::SetAll(vec![1, 2, 3]), TestCaseAction::CheckContents(vec![1, 2, 3]), + TestCaseAction::CheckContentsReverse(vec![3, 2, 1]), + TestCaseAction::Last(3), ] } @@ -343,6 +374,14 @@ mod test { TestCaseAction::Clear => { state_vec.clear(ws); } + TestCaseAction::Last(expected) => { + let actual = state_vec.last(ws); + assert_eq!(actual, Some(expected)); + } + TestCaseAction::CheckContentsReverse(expected) => { + let contents: Vec = state_vec.iter(ws).rev().collect(); + assert_eq!(expected, contents); + } } } } diff --git a/module-system/sov-state/src/containers/vec.rs b/module-system/sov-state/src/containers/vec.rs index 1873064e3e..9973f2ebcd 100644 --- a/module-system/sov-state/src/containers/vec.rs +++ b/module-system/sov-state/src/containers/vec.rs @@ -181,6 +181,16 @@ where next_i: 0, } } + + pub fn last(&self, working_set: &mut WorkingSet) -> Option { + let len = self.len(working_set); + + if len == 0usize { + None + } else { + self.elems.get(&(len - 1), working_set) + } + } } /// An [`Iterator`] over a [`StateVec`] @@ -239,6 +249,23 @@ where { } +impl<'a, 'ws, V, Codec, S> DoubleEndedIterator for StateVecIter<'a, 'ws, V, Codec, S> +where + Codec: StateCodec + Clone, + Codec::ValueCodec: StateValueCodec + StateValueCodec, + Codec::KeyCodec: StateKeyCodec, + S: Storage, +{ + fn next_back(&mut self) -> Option { + if self.len == 0 { + return None; + } + + self.len -= 1; + self.state_vec.get(self.len, self.ws) + } +} + #[cfg(all(test, feature = "native"))] mod test { use std::fmt::Debug; @@ -249,10 +276,12 @@ mod test { enum TestCaseAction { Push(T), Pop(T), + Last(T), Set(usize, T), SetAll(Vec), CheckLen(usize), CheckContents(Vec), + CheckContentsReverse(Vec), CheckGet(usize, Option), Clear, } @@ -286,6 +315,8 @@ mod test { TestCaseAction::CheckGet(0, None), TestCaseAction::SetAll(vec![1, 2, 3]), TestCaseAction::CheckContents(vec![1, 2, 3]), + TestCaseAction::CheckContentsReverse(vec![3, 2, 1]), + TestCaseAction::Last(3), ] } @@ -341,6 +372,14 @@ mod test { TestCaseAction::Clear => { state_vec.clear(ws); } + TestCaseAction::Last(expected) => { + let actual = state_vec.last(ws); + assert_eq!(actual, Some(expected)); + } + TestCaseAction::CheckContentsReverse(expected) => { + let contents: Vec = state_vec.iter(ws).rev().collect(); + assert_eq!(expected, contents); + } } } }