diff --git a/Cargo.lock b/Cargo.lock index 026e63bff..f4ac55d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2481,7 +2481,7 @@ dependencies = [ [[package]] name = "eth_trie" version = "0.4.0" -source = "git+https://github.com/ethereum/eth-trie.rs?tag=v0.1.0-alpha.1#dc8b95d40ded01396ba53bff10e1ea364bd5ed54" +source = "git+https://github.com/ethereum/eth-trie.rs?tag=v0.1.0-alpha.2#46da867d8a7eace0a9e912271b236b2007e4cd41" dependencies = [ "alloy-primitives", "alloy-rlp", diff --git a/Cargo.toml b/Cargo.toml index 8dab64227..663ec9274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ directories = "3.0" discv5 = { version = "0.4.1", features = ["serde"] } e2store = { path = "e2store" } env_logger = "0.9.0" -eth_trie = { tag = "v0.1.0-alpha.1", git = "https://github.com/ethereum/eth-trie.rs" } +eth_trie = { tag = "v0.1.0-alpha.2", git = "https://github.com/ethereum/eth-trie.rs" } ethereum_ssz = "0.7.1" ethereum_ssz_derive = "0.7.1" ethportal-api = { path = "ethportal-api" } diff --git a/e2store/src/era2.rs b/e2store/src/era2.rs index 3f99ce1e5..77f82afdd 100644 --- a/e2store/src/era2.rs +++ b/e2store/src/era2.rs @@ -1,4 +1,5 @@ use std::{ + fs, io::{ErrorKind, Read, Write}, ops::Deref, path::{Path, PathBuf}, @@ -18,6 +19,8 @@ use crate::{ utils::underlying_io_error_kind, }; +pub const MAX_STORAGE_ITEMS: usize = 10_000_000; + // --.era2 // // era2 := Version | CompressedHeader | account* @@ -63,6 +66,7 @@ impl Era2 { } pub fn create(path: PathBuf, header: Header) -> anyhow::Result { + fs::create_dir_all(&path)?; ensure!(path.is_dir(), "era2 path is not a directory: {:?}", path); let path = path.join(format!( "mainnet-{:010}-{}.era2", @@ -108,11 +112,11 @@ impl Era2 { match self.pending_storage_entries { 0 => bail!("Invalid append entry state: expected an account entry, got a storage entry. No storage entries left to append for the account"), 1 => ensure!( - storage.len() <= 10_000_000, + storage.len() <= MAX_STORAGE_ITEMS, "Storage entry can't have more than 10 million items", ), _ => ensure!( - storage.len() == 10_000_000, + storage.len() == MAX_STORAGE_ITEMS, "Only last storage entry can have less than 10 million items", ), } @@ -221,7 +225,7 @@ impl TryFrom for Entry { } #[derive(Clone, Eq, PartialEq, Debug)] -pub struct StorageEntry(Vec); +pub struct StorageEntry(pub Vec); impl Deref for StorageEntry { type Target = Vec; diff --git a/trin-execution/src/cli.rs b/trin-execution/src/cli.rs index 0166aba42..8a1aa329f 100644 --- a/trin-execution/src/cli.rs +++ b/trin-execution/src/cli.rs @@ -1,6 +1,6 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; -use clap::Parser; +use clap::{Args, Parser, Subcommand}; use crate::types::block_to_trace::BlockToTrace; @@ -26,4 +26,34 @@ pub struct TrinExecutionConfig { help = "Enable prometheus metrics reporting (provide local IP/Port from which your Prometheus server is configured to fetch metrics)" )] pub enable_metrics_with_url: Option, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug, Clone, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub enum TrinExecutionSubCommands { + /// Import a era2 state snapshot from a file, useful for bootstrapping a new node quickly + ImportState(ImportStateConfig), + /// Export the current state of the node to a era2 file + ExportState(ExportStateConfig), +} + +#[derive(Args, Debug, Default, Clone, PartialEq)] +pub struct ImportStateConfig { + #[arg( + long = "path-to-era2", + help = "path to where the era2 state snapshot is located" + )] + pub path_to_era2: PathBuf, +} + +#[derive(Args, Debug, Default, Clone, PartialEq)] +pub struct ExportStateConfig { + #[arg( + long = "path-to-era2", + help = "path to where the era2 state snapshot is located" + )] + pub path_to_era2: PathBuf, } diff --git a/trin-execution/src/evm/block_executor.rs b/trin-execution/src/evm/block_executor.rs index 4211fefbe..b6896c123 100644 --- a/trin-execution/src/evm/block_executor.rs +++ b/trin-execution/src/evm/block_executor.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{bail, ensure}; use eth_trie::{RootWithTrieDiff, Trie}; -use ethportal_api::{types::state_trie::account_state::AccountState as AccountStateInfo, Header}; +use ethportal_api::{types::state_trie::account_state::AccountState, Header}; use revm::{ db::{states::bundle_state::BundleRetention, State}, inspectors::TracerEip3155, @@ -23,7 +23,7 @@ use crate::{ set_int_gauge_vec, start_timer_vec, stop_timer, BLOCK_HEIGHT, BLOCK_PROCESSING_TIMES, TRANSACTION_PROCESSING_TIMES, }, - storage::{account::Account as RocksAccount, evm_db::EvmDB}, + storage::evm_db::EvmDB, types::block_to_trace::BlockToTrace, }; @@ -33,7 +33,7 @@ use super::{ tx_env_modifier::TxEnvModifier, }; -const BLOCKHASH_SERVE_WINDOW: u64 = 256; +pub const BLOCKHASH_SERVE_WINDOW: u64 = 256; const GENESIS_STATE_FILE: &str = "trin-execution/resources/genesis/mainnet.json"; const TEST_GENESIS_STATE_FILE: &str = "resources/genesis/mainnet.json"; @@ -146,12 +146,14 @@ impl<'a> BlockExecutor<'a> { for (address, alloc_balance) in genesis.alloc { let address_hash = keccak256(address); - let mut account = RocksAccount::default(); + let mut account = AccountState::default(); account.balance += alloc_balance.balance; - self.evm.db().database.trie.lock().insert( - address_hash.as_ref(), - &alloy_rlp::encode(AccountStateInfo::from(&account)), - )?; + self.evm + .db() + .database + .trie + .lock() + .insert(address_hash.as_ref(), &alloy_rlp::encode(&account))?; self.evm .db() .database diff --git a/trin-execution/src/execution.rs b/trin-execution/src/execution.rs index 1f36c0dac..9255a6f3a 100644 --- a/trin-execution/src/execution.rs +++ b/trin-execution/src/execution.rs @@ -2,7 +2,7 @@ use alloy_primitives::{keccak256, Address, Bytes, B256}; use alloy_rlp::Decodable; use anyhow::{anyhow, bail, ensure}; use eth_trie::{RootWithTrieDiff, Trie}; -use ethportal_api::{types::state_trie::account_state::AccountState as AccountStateInfo, Header}; +use ethportal_api::{types::state_trie::account_state::AccountState, Header}; use std::{ collections::BTreeSet, path::PathBuf, @@ -17,7 +17,6 @@ use crate::{ evm::block_executor::BlockExecutor, metrics::{start_timer_vec, stop_timer, BLOCK_PROCESSING_TIMES}, storage::{ - account::Account, evm_db::EvmDB, execution_position::ExecutionPosition, utils::{get_default_data_dir, setup_rocksdb}, @@ -29,7 +28,7 @@ use super::{config::StateConfig, types::trie_proof::TrieProof, utils::address_to pub struct TrinExecution { pub database: EvmDB, pub config: StateConfig, - execution_position: ExecutionPosition, + pub execution_position: ExecutionPosition, pub era_manager: Arc>, pub node_data_directory: PathBuf, } @@ -141,14 +140,14 @@ impl TrinExecution { Ok(self.database.trie.lock().root_hash_with_changed_nodes()?) } - pub fn get_account_state(&self, account: &Address) -> anyhow::Result { + pub fn get_account_state(&self, account: &Address) -> anyhow::Result { let account_state = self.database.db.get(keccak256(account))?; match account_state { Some(account) => { - let account: Account = Decodable::decode(&mut account.as_slice())?; - Ok(AccountStateInfo::from(&account)) + let account = AccountState::decode(&mut account.as_slice())?; + Ok(account) } - None => Ok(AccountStateInfo::default()), + None => Ok(AccountState::default()), } } diff --git a/trin-execution/src/lib.rs b/trin-execution/src/lib.rs index 01f1568d8..da4b4a8d6 100644 --- a/trin-execution/src/lib.rs +++ b/trin-execution/src/lib.rs @@ -6,6 +6,7 @@ pub mod evm; pub mod execution; pub mod metrics; pub mod storage; +pub mod subcommands; pub mod trie_walker; pub mod types; pub mod utils; diff --git a/trin-execution/src/main.rs b/trin-execution/src/main.rs index 2360d329d..47283d1dd 100644 --- a/trin-execution/src/main.rs +++ b/trin-execution/src/main.rs @@ -1,15 +1,18 @@ use clap::Parser; - use revm_primitives::SpecId; use tracing::info; use trin_execution::{ - cli::TrinExecutionConfig, evm::spec_id::get_spec_block_number, execution::TrinExecution, + cli::{TrinExecutionConfig, TrinExecutionSubCommands}, + era::manager::EraManager, + evm::spec_id::get_spec_block_number, + execution::TrinExecution, storage::utils::setup_temp_dir, + subcommands::era2::{export::StateExporter, import::StateImporter}, }; use trin_utils::log::init_tracing_logger; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> anyhow::Result<()> { init_tracing_logger(); let trin_execution_config = TrinExecutionConfig::parse(); @@ -26,7 +29,7 @@ async fn main() -> Result<(), Box> { let mut trin_execution = TrinExecution::new( directory.map(|temp_directory| temp_directory.path().to_path_buf()), - trin_execution_config.into(), + trin_execution_config.clone().into(), ) .await?; @@ -38,6 +41,31 @@ async fn main() -> Result<(), Box> { .expect("signal ctrl_c should never fail"); }); + if let Some(command) = trin_execution_config.command { + match command { + TrinExecutionSubCommands::ImportState(import_state) => { + let mut state_importer = StateImporter::new(trin_execution, import_state); + state_importer.import_state()?; + state_importer.import_last_256_block_hashes().await?; + + info!( + "Imported state from era2: {} {}", + state_importer.trin_execution.next_block_number() - 1, + state_importer.trin_execution.get_root()? + ); + return Ok(()); + } + TrinExecutionSubCommands::ExportState(export_state) => { + let mut era_manager = + EraManager::new(trin_execution.next_block_number() - 1).await?; + let header = era_manager.get_next_block().await?.clone(); + let mut state_exporter = StateExporter::new(trin_execution, export_state); + state_exporter.export_state(header.header)?; + return Ok(()); + } + } + } + let mut block_number = trin_execution.next_block_number(); let end_block = get_spec_block_number(SpecId::MERGE); diff --git a/trin-execution/src/storage/account.rs b/trin-execution/src/storage/account.rs deleted file mode 100644 index e90a1bd74..000000000 --- a/trin-execution/src/storage/account.rs +++ /dev/null @@ -1,47 +0,0 @@ -use alloy_consensus::EMPTY_ROOT_HASH; -use alloy_primitives::{B256, U256}; -use alloy_rlp::{RlpDecodable, RlpEncodable}; -use ethportal_api::types::state_trie::account_state::AccountState as AccountStateInfo; -use revm_primitives::{AccountInfo, KECCAK_EMPTY}; - -/// The Account State stored in the state trie. -#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)] -pub struct Account { - pub nonce: u64, - pub balance: U256, - pub storage_root: B256, - pub code_hash: B256, -} - -impl Default for Account { - fn default() -> Self { - Self { - nonce: 0, - balance: U256::ZERO, - storage_root: EMPTY_ROOT_HASH, - code_hash: KECCAK_EMPTY, - } - } -} - -impl From<&Account> for AccountStateInfo { - fn from(account: &Account) -> Self { - Self { - nonce: account.nonce, - balance: account.balance, - storage_root: account.storage_root, - code_hash: account.code_hash, - } - } -} - -impl Account { - pub fn from_account_info(account: AccountInfo, storage_root: B256) -> Self { - Self { - nonce: account.nonce, - balance: account.balance, - storage_root, - code_hash: account.code_hash, - } - } -} diff --git a/trin-execution/src/storage/evm_db.rs b/trin-execution/src/storage/evm_db.rs index 3d0776fdc..033e2b1ac 100644 --- a/trin-execution/src/storage/evm_db.rs +++ b/trin-execution/src/storage/evm_db.rs @@ -11,7 +11,7 @@ use alloy_consensus::EMPTY_ROOT_HASH; use alloy_primitives::{Address, B256, U256}; use alloy_rlp::Decodable; use eth_trie::{EthTrie, RootWithTrieDiff, Trie}; -use ethportal_api::types::state_trie::account_state::AccountState as AccountStateInfo; +use ethportal_api::types::state_trie::account_state::AccountState; use hashbrown::{HashMap as BrownHashMap, HashSet}; use parking_lot::Mutex; use prometheus_exporter::prometheus::HistogramTimer; @@ -23,10 +23,7 @@ use revm_primitives::{keccak256, AccountInfo, Bytecode, HashMap, KECCAK_EMPTY}; use rocksdb::DB as RocksDB; use tracing::info; -use super::{ - account::Account as RocksAccount, account_db::AccountDB, execution_position::ExecutionPosition, - trie_db::TrieRocksDB, -}; +use super::{account_db::AccountDB, execution_position::ExecutionPosition, trie_db::TrieRocksDB}; fn start_commit_timer(name: &str) -> HistogramTimer { start_timer_vec(&BUNDLE_COMMIT_PROCESSING_TIMES, &[name]) @@ -110,25 +107,35 @@ impl EvmDB { let plain_state_some_account_timer = start_commit_timer("account:plain_state_some_account"); let timer = start_commit_timer("account:fetch_account_from_db"); - let existing_rocks_account = self.fetch_account(address_hash)?; + let existing_account_state = self.fetch_account(address_hash)?; stop_timer(timer); - let rocks_account = if let Some(existing_rocks_account) = existing_rocks_account { - RocksAccount::from_account_info(account_info, existing_rocks_account.storage_root) + let account_state = if let Some(existing_account_state) = existing_account_state { + AccountState { + balance: account_info.balance, + nonce: account_info.nonce, + code_hash: account_info.code_hash, + storage_root: existing_account_state.storage_root, + } } else { - RocksAccount::from_account_info(account_info, EMPTY_ROOT_HASH) + AccountState { + balance: account_info.balance, + nonce: account_info.nonce, + code_hash: account_info.code_hash, + storage_root: EMPTY_ROOT_HASH, + } }; let timer = start_commit_timer("account:insert_into_trie"); - let _ = self.trie.lock().insert( - address_hash.as_ref(), - &alloy_rlp::encode(AccountStateInfo::from(&rocks_account)), - ); + let _ = self + .trie + .lock() + .insert(address_hash.as_ref(), &alloy_rlp::encode(&account_state)); stop_timer(timer); let timer = start_commit_timer("account:put_account_into_db"); self.db - .put(address_hash, alloy_rlp::encode(rocks_account))?; + .put(address_hash, alloy_rlp::encode(account_state))?; stop_timer(timer); stop_timer(plain_state_some_account_timer); @@ -142,17 +149,17 @@ impl EvmDB { timer_label: &str, ) -> anyhow::Result<()> { // load account from db - let Some(mut rocks_account) = self.fetch_account(address_hash)? else { + let Some(mut account_state) = self.fetch_account(address_hash)? else { return Ok(()); }; let timer = start_commit_timer(timer_label); // wipe storage trie and db - if rocks_account.storage_root != EMPTY_ROOT_HASH { + if account_state.storage_root != EMPTY_ROOT_HASH { let account_db = AccountDB::new(address_hash, self.db.clone()); - let mut trie = EthTrie::from(Arc::new(account_db), rocks_account.storage_root)?; + let mut trie = EthTrie::from(Arc::new(account_db), account_state.storage_root)?; trie.clear_trie_from_db()?; - rocks_account.storage_root = EMPTY_ROOT_HASH; + account_state.storage_root = EMPTY_ROOT_HASH; } // update account trie and db @@ -161,11 +168,11 @@ impl EvmDB { let _ = self.trie.lock().remove(address_hash.as_ref()); } else { self.db - .put(address_hash, alloy_rlp::encode(&rocks_account))?; - let _ = self.trie.lock().insert( - address_hash.as_ref(), - &alloy_rlp::encode(AccountStateInfo::from(&rocks_account)), - ); + .put(address_hash, alloy_rlp::encode(&account_state))?; + let _ = self + .trie + .lock() + .insert(address_hash.as_ref(), &alloy_rlp::encode(&account_state)); } stop_timer(timer); @@ -195,12 +202,12 @@ impl EvmDB { let timer = start_commit_timer("storage:apply_updates"); let account_db = AccountDB::new(address_hash, self.db.clone()); - let mut rocks_account = self.fetch_account(address_hash)?.unwrap_or_default(); + let mut account_state = self.fetch_account(address_hash)?.unwrap_or_default(); - let mut trie = if rocks_account.storage_root == EMPTY_ROOT_HASH { + let mut trie = if account_state.storage_root == EMPTY_ROOT_HASH { EthTrie::new(Arc::new(account_db)) } else { - EthTrie::from(Arc::new(account_db), rocks_account.storage_root)? + EthTrie::from(Arc::new(account_db), account_state.storage_root)? }; for (key, value) in storage { @@ -225,15 +232,15 @@ impl EvmDB { } } - rocks_account.storage_root = storage_root; + account_state.storage_root = storage_root; - let _ = self.trie.lock().insert( - address_hash.as_ref(), - &alloy_rlp::encode(AccountStateInfo::from(&rocks_account)), - ); + let _ = self + .trie + .lock() + .insert(address_hash.as_ref(), &alloy_rlp::encode(&account_state)); self.db - .put(address_hash, alloy_rlp::encode(rocks_account))?; + .put(address_hash, alloy_rlp::encode(account_state))?; stop_timer(timer); Ok(()) } @@ -300,9 +307,9 @@ impl EvmDB { Ok(()) } - fn fetch_account(&self, address_hash: B256) -> anyhow::Result> { + fn fetch_account(&self, address_hash: B256) -> anyhow::Result> { match self.db.get(address_hash)? { - Some(raw_account) => Ok(Some(RocksAccount::decode(&mut raw_account.as_slice())?)), + Some(raw_account) => Ok(Some(AccountState::decode(&mut raw_account.as_slice())?)), None => Ok(None), } } @@ -359,7 +366,7 @@ impl DatabaseRef for EvmDB { fn storage_ref(&self, address: Address, index: U256) -> Result { let timer = start_processing_timer("database_get_storage"); let address_hash = keccak256(address); - let account: RocksAccount = match self.fetch_account(address_hash)? { + let account: AccountState = match self.fetch_account(address_hash)? { Some(account) => account, None => return Err(Self::Error::NotFound("storage".to_string())), }; diff --git a/trin-execution/src/storage/mod.rs b/trin-execution/src/storage/mod.rs index 44dffa95c..767236a50 100644 --- a/trin-execution/src/storage/mod.rs +++ b/trin-execution/src/storage/mod.rs @@ -1,4 +1,3 @@ -pub mod account; pub mod account_db; pub mod error; pub mod evm_db; diff --git a/trin-execution/src/subcommands/era2/export.rs b/trin-execution/src/subcommands/era2/export.rs new file mode 100644 index 000000000..9a7b6f098 --- /dev/null +++ b/trin-execution/src/subcommands/era2/export.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use alloy_consensus::EMPTY_ROOT_HASH; +use alloy_rlp::Decodable; +use anyhow::ensure; +use e2store::era2::{ + AccountEntry, AccountOrStorageEntry, Era2, StorageEntry, StorageItem, MAX_STORAGE_ITEMS, +}; +use eth_trie::EthTrie; +use ethportal_api::{types::state_trie::account_state::AccountState, Header}; +use parking_lot::Mutex; +use revm_primitives::{B256, KECCAK_EMPTY, U256}; +use tracing::info; + +use crate::{cli::ExportStateConfig, execution::TrinExecution, storage::account_db::AccountDB}; + +pub struct StateExporter { + pub trin_execution: TrinExecution, + exporter_config: ExportStateConfig, +} + +impl StateExporter { + pub fn new(trin_execution: TrinExecution, exporter_config: ExportStateConfig) -> Self { + Self { + trin_execution, + exporter_config, + } + } + + pub fn export_state(&mut self, header: Header) -> anyhow::Result<()> { + ensure!( + header.state_root == self.trin_execution.get_root()?, + "State root mismatch fro block header we are trying to export" + ); + info!( + "Exporting state from block number: {} with state root: {}", + header.number, header.state_root + ); + let mut era2 = Era2::create(self.exporter_config.path_to_era2.clone(), header)?; + info!("Era2 initiated"); + info!("Trie leaf iterator initiated"); + let mut accounts_exported = 0; + for key_hash_and_leaf_value in self.trin_execution.database.trie.lock().iter() { + let (raw_account_hash, account_state) = key_hash_and_leaf_value?; + let account_hash = B256::from_slice(&raw_account_hash); + + let account_state = AccountState::decode(&mut account_state.as_slice())?; + let bytecode = if account_state.code_hash != KECCAK_EMPTY { + self.trin_execution + .database + .db + .get(account_state.code_hash)? + .expect("If code hash is not empty, code must be present") + } else { + vec![] + }; + + let mut storage: Vec = vec![]; + if account_state.storage_root != EMPTY_ROOT_HASH { + let account_db = + AccountDB::new(account_hash, self.trin_execution.database.db.clone()); + let account_trie = Arc::new(Mutex::new(EthTrie::from( + Arc::new(account_db), + account_state.storage_root, + )?)); + for key_hash_and_leaf_value in account_trie.lock().iter() { + let (raw_storage_index_hash, storage_value) = key_hash_and_leaf_value?; + let storage_index_hash = B256::from_slice(&raw_storage_index_hash); + let storage_slot_value: U256 = Decodable::decode(&mut storage_value.as_slice()) + .expect("Failed to decode storage slot value"); + storage.push(StorageItem { + storage_index_hash, + value: storage_slot_value, + }); + } + } + + // Get the rounded up storage count + let storage_count = storage.len().div_ceil(MAX_STORAGE_ITEMS); + + era2.append_entry(&AccountOrStorageEntry::Account(AccountEntry { + address_hash: account_hash, + account_state, + bytecode, + storage_count: storage_count as u32, + }))?; + + for storage_chunk in storage.chunks(MAX_STORAGE_ITEMS) { + era2.append_entry(&AccountOrStorageEntry::Storage(StorageEntry( + storage_chunk.to_vec(), + )))?; + } + + accounts_exported += 1; + if accounts_exported % 10000 == 0 { + info!("Processed {} accounts", accounts_exported); + } + } + + info!("Era2 snapshot exported"); + + Ok(()) + } +} diff --git a/trin-execution/src/subcommands/era2/import.rs b/trin-execution/src/subcommands/era2/import.rs new file mode 100644 index 000000000..f300bebcc --- /dev/null +++ b/trin-execution/src/subcommands/era2/import.rs @@ -0,0 +1,140 @@ +use std::sync::Arc; + +use anyhow::{ensure, Error}; +use e2store::era2::{AccountEntry, AccountOrStorageEntry, Era2, StorageItem}; +use eth_trie::{EthTrie, Trie}; +use revm_primitives::{keccak256, B256, U256}; +use tracing::info; + +use crate::{ + cli::ImportStateConfig, era::manager::EraManager, evm::block_executor::BLOCKHASH_SERVE_WINDOW, + execution::TrinExecution, storage::account_db::AccountDB, +}; + +pub struct StateImporter { + pub trin_execution: TrinExecution, + importer_config: ImportStateConfig, +} + +impl StateImporter { + pub fn new(trin_execution: TrinExecution, importer_config: ImportStateConfig) -> Self { + Self { + trin_execution, + importer_config, + } + } + + pub fn import_state(&mut self) -> anyhow::Result<()> { + info!("Importing state from .era2 file"); + if self.trin_execution.next_block_number() != 0 { + return Err(Error::msg( + "Cannot import state from .era2, database is not empty", + )); + } + + let mut era2 = Era2::open(self.importer_config.path_to_era2.clone())?; + info!("Era2 reader initiated"); + let mut accounts_imported = 0; + while let Some(account) = era2.next() { + let AccountOrStorageEntry::Account(account) = account else { + return Err(Error::msg("Expected account, got storage entry")); + }; + let AccountEntry { + address_hash, + account_state, + bytecode, + storage_count, + } = account; + + // Build storage trie + let account_db = AccountDB::new(address_hash, self.trin_execution.database.db.clone()); + let mut storage_trie = EthTrie::new(Arc::new(account_db)); + for _ in 0..storage_count { + let Some(AccountOrStorageEntry::Storage(storage_entry)) = era2.next() else { + return Err(Error::msg("Expected storage, got account entry")); + }; + for StorageItem { + storage_index_hash, + value, + } in storage_entry.0 + { + storage_trie + .insert(storage_index_hash.as_slice(), &alloy_rlp::encode(value))?; + } + // Commit storage trie every 10 million storage items, to avoid excessive memory + // usage + storage_trie.root_hash()?; + } + + if storage_trie.root_hash()? != account_state.storage_root { + return Err(Error::msg("Failed importing account storage trie: storage roots don't match expect value, .era2 import failed")); + } + + // Insert contract if available + ensure!( + account_state.code_hash == keccak256(&bytecode), + "Code hash mismatch, .era2 import failed" + ); + if !bytecode.is_empty() { + self.trin_execution + .database + .db + .put(keccak256(&bytecode), bytecode.clone())?; + } + + // Insert account into state trie + self.trin_execution + .database + .trie + .lock() + .insert(address_hash.as_slice(), &alloy_rlp::encode(&account_state))?; + + self.trin_execution + .database + .db + .put(address_hash, alloy_rlp::encode(account_state)) + .expect("Inserting account should never fail"); + + accounts_imported += 1; + if accounts_imported % 1000 == 0 { + info!("Imported {} accounts", accounts_imported); + info!("Committing changes to database"); + self.trin_execution.get_root()?; + info!("Finished committing changes to database"); + } + } + + // Check if the state root matches, if this fails it means either the .era2 is wrong or we + // imported the state wrong + if era2.header.header.state_root != self.trin_execution.get_root()? { + return Err(Error::msg("State root mismatch, .era2 import failed")); + } + + // Save execution position + self.trin_execution + .execution_position + .update_position(self.trin_execution.database.db.clone(), era2.header.header)?; + + info!("Done importing State from .era2 file"); + + Ok(()) + } + + /// insert the last 256 block hashes into the database + pub async fn import_last_256_block_hashes(&mut self) -> anyhow::Result<()> { + let first_block_hash_to_add = self + .trin_execution + .next_block_number() + .saturating_sub(BLOCKHASH_SERVE_WINDOW); + let mut era_manager = EraManager::new(first_block_hash_to_add).await?; + while era_manager.next_block_number() < self.trin_execution.next_block_number() { + let block = era_manager.get_next_block().await?; + self.trin_execution.database.db.put( + keccak256(B256::from(U256::from(block.header.number))), + block.header.hash(), + )? + } + + Ok(()) + } +} diff --git a/trin-execution/src/subcommands/era2/mod.rs b/trin-execution/src/subcommands/era2/mod.rs new file mode 100644 index 000000000..c687afe56 --- /dev/null +++ b/trin-execution/src/subcommands/era2/mod.rs @@ -0,0 +1,2 @@ +pub mod export; +pub mod import; diff --git a/trin-execution/src/subcommands/mod.rs b/trin-execution/src/subcommands/mod.rs new file mode 100644 index 000000000..a4ee2c858 --- /dev/null +++ b/trin-execution/src/subcommands/mod.rs @@ -0,0 +1 @@ +pub mod era2; diff --git a/trin-execution/src/utils.rs b/trin-execution/src/utils.rs index 932e7a2ca..671e129a6 100644 --- a/trin-execution/src/utils.rs +++ b/trin-execution/src/utils.rs @@ -2,7 +2,10 @@ use alloy_primitives::{keccak256, Address, B256}; pub fn full_nibble_path_to_address_hash(key_path: &[u8]) -> B256 { if key_path.len() != 64 { - panic!("Key path should always be 64 bytes long.") + panic!( + "Key path should always be 64 bytes long: {}", + key_path.len() + ); } let mut raw_address_hash = vec![];