diff --git a/Cargo.lock b/Cargo.lock index f3a4ce735d4..36c01dea011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4360,6 +4360,7 @@ dependencies = [ "hasher", "hex", "hex-literal 0.4.1", + "lazy_static", "proptest", "rand 0.8.5", "reqwest", diff --git a/crates/networking/p2p/p2p.rs b/crates/networking/p2p/p2p.rs index ff9275a3fd6..8ba8147fd94 100644 --- a/crates/networking/p2p/p2p.rs +++ b/crates/networking/p2p/p2p.rs @@ -74,7 +74,7 @@ pub(crate) mod metrics; pub mod network; pub mod peer_handler; pub mod rlpx; -pub(crate) mod snap; +pub mod snap; pub mod sync; pub mod sync_manager; pub mod tx_broadcaster; diff --git a/crates/networking/p2p/peer_handler.rs b/crates/networking/p2p/peer_handler.rs index b89650944c5..ec2af80136b 100644 --- a/crates/networking/p2p/peer_handler.rs +++ b/crates/networking/p2p/peer_handler.rs @@ -11,57 +11,29 @@ use crate::{ }, message::Message as RLPxMessage, p2p::{Capability, SUPPORTED_ETH_CAPABILITIES}, - snap::{ - AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, - GetStorageRanges, GetTrieNodes, StorageRanges, TrieNodes, - }, - }, - snap::encodable_to_proof, - sync::{AccountStorageRoots, SnapBlockSyncState, block_is_stale, update_pivot}, - utils::{ - AccountsWithStorage, dump_accounts_to_file, dump_storages_to_file, - get_account_state_snapshot_file, get_account_storages_snapshot_file, }, }; -use bytes::Bytes; use ethrex_common::{ - BigEndianHash, H256, U256, - types::{AccountState, BlockBody, BlockHeader, validate_block_body}, + H256, + types::{BlockBody, BlockHeader, validate_block_body}, }; -use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; -use ethrex_storage::Store; -use ethrex_trie::Nibbles; -use ethrex_trie::{Node, verify_range}; use spawned_concurrency::tasks::GenServerHandle; use std::{ - collections::{BTreeMap, HashMap, HashSet, VecDeque}, - io::ErrorKind, - path::{Path, PathBuf}, + collections::{HashSet, VecDeque}, sync::atomic::Ordering, time::{Duration, SystemTime}, }; -use tracing::{debug, error, info, trace, warn}; -pub const PEER_REPLY_TIMEOUT: Duration = Duration::from_secs(15); -pub const PEER_SELECT_RETRY_ATTEMPTS: u32 = 3; -pub const REQUEST_RETRY_ATTEMPTS: u32 = 5; -pub const MAX_RESPONSE_BYTES: u64 = 512 * 1024; -pub const HASH_MAX: H256 = H256([0xFF; 32]); - -pub const MAX_HEADER_CHUNK: u64 = 500_000; - -// How much we store in memory of request_account_range and request_storage_ranges -// before we dump it into the file. This tunes how much memory ethrex uses during -// the first steps of snap sync -pub const RANGE_FILE_CHUNK_SIZE: usize = 1024 * 1024 * 64; // 64MB -pub const SNAP_LIMIT: usize = 128; - -// Request as many as 128 block bodies per request -// this magic number is not part of the protocol and is taken from geth, see: -// https://github.com/ethereum/go-ethereum/blob/2585776aabbd4ae9b00050403b42afb0cee968ec/eth/downloader/downloader.go#L42-L43 -// -// Note: We noticed that while bigger values are supported -// increasing them may be the cause of peers disconnection -pub const MAX_BLOCK_BODIES_TO_REQUEST: usize = 128; +use tracing::{debug, error, trace, warn}; + +// Re-export constants from snap::constants for backward compatibility +pub use crate::snap::constants::{ + HASH_MAX, MAX_BLOCK_BODIES_TO_REQUEST, MAX_HEADER_CHUNK, MAX_RESPONSE_BYTES, + PEER_REPLY_TIMEOUT, PEER_SELECT_RETRY_ATTEMPTS, RANGE_FILE_CHUNK_SIZE, REQUEST_RETRY_ATTEMPTS, + SNAP_LIMIT, +}; + +// Re-export snap client types for backward compatibility +pub use crate::snap::{DumpError, RequestMetadata, RequestStorageTrieNodesError, SnapError}; /// An abstraction over the [Kademlia] containing logic to make requests to peers #[derive(Debug, Clone)] @@ -75,24 +47,6 @@ pub enum BlockRequestOrder { NewToOld, } -#[derive(Clone)] -struct StorageTaskResult { - start_index: usize, - account_storages: Vec>, - peer_id: H256, - remaining_start: usize, - remaining_end: usize, - remaining_hash_range: (H256, Option), -} -#[derive(Debug)] -struct StorageTask { - start_index: usize, - end_index: usize, - start_hash: H256, - // end_hash is None if the task is for the first big storage request - end_hash: Option, -} - async fn ask_peer_head_number( peer_id: H256, connection: &mut PeerConnection, @@ -149,7 +103,7 @@ impl PeerHandler { } } - async fn make_request( + pub(crate) async fn make_request( // TODO: We should receive the PeerHandler (or self) instead, but since it is not yet spawnified it cannot be shared // Fix this to avoid passing the PeerTable as a parameter peer_table: &mut PeerTable, @@ -600,1306 +554,6 @@ impl PeerHandler { } Ok(None) } - - /// Requests an account range from any suitable peer given the state trie's root and the starting hash and the limit hash. - /// Will also return a boolean indicating if there is more state to be fetched towards the right of the trie - /// (Note that the boolean will be true even if the remaining state is ouside the boundary set by the limit hash) - /// - /// # Returns - /// - /// The account range or `None` if: - /// - /// - There are no available peers (the node just started up or was rejected by all other nodes) - /// - No peer returned a valid response in the given time and retry limits - pub async fn request_account_range( - &mut self, - start: H256, - limit: H256, - account_state_snapshots_dir: &Path, - pivot_header: &mut BlockHeader, - block_sync_state: &mut SnapBlockSyncState, - ) -> Result<(), PeerHandlerError> { - METRICS - .current_step - .set(CurrentStepValue::RequestingAccountRanges); - // 1) split the range in chunks of same length - let start_u256 = U256::from_big_endian(&start.0); - let limit_u256 = U256::from_big_endian(&limit.0); - - let chunk_count = 800; - let chunk_size = (limit_u256 - start_u256) / chunk_count; - - // list of tasks to be executed - let mut tasks_queue_not_started = VecDeque::<(H256, H256)>::new(); - for i in 0..(chunk_count as u64) { - let chunk_start_u256 = chunk_size * i + start_u256; - // We subtract one because ranges are inclusive - let chunk_end_u256 = chunk_start_u256 + chunk_size - 1u64; - let chunk_start = H256::from_uint(&(chunk_start_u256)); - let chunk_end = H256::from_uint(&(chunk_end_u256)); - tasks_queue_not_started.push_back((chunk_start, chunk_end)); - } - // Modify the last chunk to include the limit - let last_task = tasks_queue_not_started - .back_mut() - .ok_or(PeerHandlerError::NoTasks)?; - last_task.1 = limit; - - // 2) request the chunks from peers - - let mut downloaded_count = 0_u64; - let mut all_account_hashes = Vec::new(); - let mut all_accounts_state = Vec::new(); - - // channel to send the tasks to the peers - let (task_sender, mut task_receiver) = - tokio::sync::mpsc::channel::<(Vec, H256, Option<(H256, H256)>)>(1000); - - debug!("Starting to download account ranges from peers"); - - *METRICS.account_tries_download_start_time.lock().await = Some(SystemTime::now()); - - let mut completed_tasks = 0; - let mut chunk_file = 0; - let mut last_update: SystemTime = SystemTime::now(); - let mut write_set = tokio::task::JoinSet::new(); - - let mut logged_no_free_peers_count = 0; - - loop { - if all_accounts_state.len() * size_of::() >= RANGE_FILE_CHUNK_SIZE { - let current_account_hashes = std::mem::take(&mut all_account_hashes); - let current_account_states = std::mem::take(&mut all_accounts_state); - - let account_state_chunk = current_account_hashes - .into_iter() - .zip(current_account_states) - .collect::>(); - - if !std::fs::exists(account_state_snapshots_dir) - .map_err(|_| PeerHandlerError::NoStateSnapshotsDir)? - { - std::fs::create_dir_all(account_state_snapshots_dir) - .map_err(|_| PeerHandlerError::CreateStateSnapshotsDir)?; - } - - let account_state_snapshots_dir_cloned = account_state_snapshots_dir.to_path_buf(); - write_set.spawn(async move { - let path = get_account_state_snapshot_file( - &account_state_snapshots_dir_cloned, - chunk_file, - ); - // TODO: check the error type and handle it properly - dump_accounts_to_file(&path, account_state_chunk) - }); - - chunk_file += 1; - } - - if last_update - .elapsed() - .expect("Time shouldn't be in the past") - >= Duration::from_secs(1) - { - METRICS - .downloaded_account_tries - .store(downloaded_count, Ordering::Relaxed); - last_update = SystemTime::now(); - } - - if let Ok((accounts, peer_id, chunk_start_end)) = task_receiver.try_recv() { - if let Some((chunk_start, chunk_end)) = chunk_start_end { - if chunk_start <= chunk_end { - tasks_queue_not_started.push_back((chunk_start, chunk_end)); - } else { - completed_tasks += 1; - } - } - if chunk_start_end.is_none() { - completed_tasks += 1; - } - if accounts.is_empty() { - self.peer_table.record_failure(&peer_id).await?; - continue; - } - self.peer_table.record_success(&peer_id).await?; - - downloaded_count += accounts.len() as u64; - - debug!( - "Downloaded {} accounts from peer {} (current count: {downloaded_count})", - accounts.len(), - peer_id - ); - all_account_hashes.extend(accounts.iter().map(|unit| unit.hash)); - all_accounts_state.extend(accounts.iter().map(|unit| unit.account)); - } - - let Some((peer_id, connection)) = self - .peer_table - .get_best_peer(&SUPPORTED_ETH_CAPABILITIES) - .await - .inspect_err(|err| warn!(%err, "Error requesting a peer for account range")) - .unwrap_or(None) - else { - // Log ~ once every 10 seconds - if logged_no_free_peers_count == 0 { - trace!("We are missing peers in request_account_range"); - logged_no_free_peers_count = 1000; - } - logged_no_free_peers_count -= 1; - // Sleep a bit to avoid busy polling - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - }; - - let Some((chunk_start, chunk_end)) = tasks_queue_not_started.pop_front() else { - if completed_tasks >= chunk_count { - debug!("All account ranges downloaded successfully"); - break; - } - continue; - }; - - let tx = task_sender.clone(); - - if block_is_stale(pivot_header) { - debug!("request_account_range became stale, updating pivot"); - *pivot_header = update_pivot( - pivot_header.number, - pivot_header.timestamp, - self, - block_sync_state, - ) - .await - .expect("Should be able to update pivot") - } - - let peer_table = self.peer_table.clone(); - - tokio::spawn(PeerHandler::request_account_range_worker( - peer_id, - connection, - peer_table, - chunk_start, - chunk_end, - pivot_header.state_root, - tx, - )); - } - - write_set - .join_all() - .await - .into_iter() - .collect::, DumpError>>() - .map_err(PeerHandlerError::DumpError)?; - - // TODO: This is repeated code, consider refactoring - { - let current_account_hashes = std::mem::take(&mut all_account_hashes); - let current_account_states = std::mem::take(&mut all_accounts_state); - - let account_state_chunk = current_account_hashes - .into_iter() - .zip(current_account_states) - .collect::>(); - - if !std::fs::exists(account_state_snapshots_dir) - .map_err(|_| PeerHandlerError::NoStateSnapshotsDir)? - { - std::fs::create_dir_all(account_state_snapshots_dir) - .map_err(|_| PeerHandlerError::CreateStateSnapshotsDir)?; - } - - let path = get_account_state_snapshot_file(account_state_snapshots_dir, chunk_file); - dump_accounts_to_file(&path, account_state_chunk) - .inspect_err(|err| { - error!( - "We had an error dumping the last accounts to disk {}", - err.error - ) - }) - .map_err(|_| PeerHandlerError::WriteStateSnapshotsDir(chunk_file))?; - } - - METRICS - .downloaded_account_tries - .store(downloaded_count, Ordering::Relaxed); - *METRICS.account_tries_download_end_time.lock().await = Some(SystemTime::now()); - - Ok(()) - } - - #[allow(clippy::type_complexity)] - async fn request_account_range_worker( - peer_id: H256, - mut connection: PeerConnection, - mut peer_table: PeerTable, - chunk_start: H256, - chunk_end: H256, - state_root: H256, - tx: tokio::sync::mpsc::Sender<(Vec, H256, Option<(H256, H256)>)>, - ) -> Result<(), PeerHandlerError> { - debug!( - "Requesting account range from peer {peer_id}, chunk: {chunk_start:?} - {chunk_end:?}" - ); - let request_id = rand::random(); - let request = RLPxMessage::GetAccountRange(GetAccountRange { - id: request_id, - root_hash: state_root, - starting_hash: chunk_start, - limit_hash: chunk_end, - response_bytes: MAX_RESPONSE_BYTES, - }); - if let Ok(RLPxMessage::AccountRange(AccountRange { - id: _, - accounts, - proof, - })) = PeerHandler::make_request( - &mut peer_table, - peer_id, - &mut connection, - request, - PEER_REPLY_TIMEOUT, - ) - .await - { - if accounts.is_empty() { - tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) - .await - .ok(); - return Ok(()); - } - // Unzip & validate response - let proof = encodable_to_proof(&proof); - let (account_hashes, account_states): (Vec<_>, Vec<_>) = accounts - .clone() - .into_iter() - .map(|unit| (unit.hash, unit.account)) - .unzip(); - let encoded_accounts = account_states - .iter() - .map(|acc| acc.encode_to_vec()) - .collect::>(); - - let Ok(should_continue) = verify_range( - state_root, - &chunk_start, - &account_hashes, - &encoded_accounts, - &proof, - ) else { - tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) - .await - .ok(); - tracing::error!("Received invalid account range"); - return Ok(()); - }; - - // If the range has more accounts to fetch, we send the new chunk - let chunk_left = if should_continue { - let last_hash = match account_hashes.last() { - Some(last_hash) => last_hash, - None => { - tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) - .await - .ok(); - error!("Account hashes last failed, this shouldn't happen"); - return Err(PeerHandlerError::AccountHashes); - } - }; - let new_start_u256 = U256::from_big_endian(&last_hash.0) + 1; - let new_start = H256::from_uint(&new_start_u256); - Some((new_start, chunk_end)) - } else { - None - }; - tx.send(( - accounts - .into_iter() - .filter(|unit| unit.hash <= chunk_end) - .collect(), - peer_id, - chunk_left, - )) - .await - .ok(); - } else { - tracing::debug!("Failed to get account range"); - tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) - .await - .ok(); - } - Ok::<(), PeerHandlerError>(()) - } - - /// Requests bytecodes for the given code hashes - /// Returns the bytecodes or None if: - /// - There are no available peers (the node just started up or was rejected by all other nodes) - /// - No peer returned a valid response in the given time and retry limits - pub async fn request_bytecodes( - &mut self, - all_bytecode_hashes: &[H256], - ) -> Result>, PeerHandlerError> { - METRICS - .current_step - .set(CurrentStepValue::RequestingBytecodes); - const MAX_BYTECODES_REQUEST_SIZE: usize = 100; - // 1) split the range in chunks of same length - let chunk_count = 800; - let chunk_size = all_bytecode_hashes.len() / chunk_count; - - // list of tasks to be executed - // Types are (start_index, end_index, starting_hash) - // NOTE: end_index is NOT inclusive - let mut tasks_queue_not_started = VecDeque::<(usize, usize)>::new(); - for i in 0..chunk_count { - let chunk_start = chunk_size * i; - let chunk_end = chunk_start + chunk_size; - tasks_queue_not_started.push_back((chunk_start, chunk_end)); - } - // Modify the last chunk to include the limit - let last_task = tasks_queue_not_started - .back_mut() - .ok_or(PeerHandlerError::NoTasks)?; - last_task.1 = all_bytecode_hashes.len(); - - // 2) request the chunks from peers - let mut downloaded_count = 0_u64; - let mut all_bytecodes = vec![Bytes::new(); all_bytecode_hashes.len()]; - - // channel to send the tasks to the peers - struct TaskResult { - start_index: usize, - bytecodes: Vec, - peer_id: H256, - remaining_start: usize, - remaining_end: usize, - } - let (task_sender, mut task_receiver) = tokio::sync::mpsc::channel::(1000); - - debug!("Starting to download bytecodes from peers"); - - METRICS - .bytecodes_to_download - .fetch_add(all_bytecode_hashes.len() as u64, Ordering::Relaxed); - - let mut completed_tasks = 0; - - let mut logged_no_free_peers_count = 0; - - loop { - if let Ok(result) = task_receiver.try_recv() { - let TaskResult { - start_index, - bytecodes, - peer_id, - remaining_start, - remaining_end, - } = result; - - debug!( - "Downloaded {} bytecodes from peer {peer_id} (current count: {downloaded_count})", - bytecodes.len(), - ); - - if remaining_start < remaining_end { - tasks_queue_not_started.push_back((remaining_start, remaining_end)); - } else { - completed_tasks += 1; - } - if bytecodes.is_empty() { - self.peer_table.record_failure(&peer_id).await?; - continue; - } - - downloaded_count += bytecodes.len() as u64; - - self.peer_table.record_success(&peer_id).await?; - for (i, bytecode) in bytecodes.into_iter().enumerate() { - all_bytecodes[start_index + i] = bytecode; - } - } - - let Some((peer_id, mut connection)) = self - .peer_table - .get_best_peer(&SUPPORTED_ETH_CAPABILITIES) - .await - .inspect_err(|err| warn!(%err, "Error requesting a peer for bytecodes")) - .unwrap_or(None) - else { - // Log ~ once every 10 seconds - if logged_no_free_peers_count == 0 { - trace!("We are missing peers in request_bytecodes"); - logged_no_free_peers_count = 1000; - } - logged_no_free_peers_count -= 1; - // Sleep a bit to avoid busy polling - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - }; - - let Some((chunk_start, chunk_end)) = tasks_queue_not_started.pop_front() else { - if completed_tasks >= chunk_count { - debug!("All bytecodes downloaded successfully"); - break; - } - continue; - }; - - let tx = task_sender.clone(); - - let hashes_to_request: Vec<_> = all_bytecode_hashes - .iter() - .skip(chunk_start) - .take((chunk_end - chunk_start).min(MAX_BYTECODES_REQUEST_SIZE)) - .copied() - .collect(); - - let mut peer_table = self.peer_table.clone(); - - tokio::spawn(async move { - let empty_task_result = TaskResult { - start_index: chunk_start, - bytecodes: vec![], - peer_id, - remaining_start: chunk_start, - remaining_end: chunk_end, - }; - debug!( - "Requesting bytecode from peer {peer_id}, chunk: {chunk_start:?} - {chunk_end:?}" - ); - let request_id = rand::random(); - let request = RLPxMessage::GetByteCodes(GetByteCodes { - id: request_id, - hashes: hashes_to_request.clone(), - bytes: MAX_RESPONSE_BYTES, - }); - if let Ok(RLPxMessage::ByteCodes(ByteCodes { id: _, codes })) = - PeerHandler::make_request( - &mut peer_table, - peer_id, - &mut connection, - request, - PEER_REPLY_TIMEOUT, - ) - .await - { - if codes.is_empty() { - tx.send(empty_task_result).await.ok(); - // Too spammy - // tracing::error!("Received empty account range"); - return; - } - // Validate response by hashing bytecodes - let validated_codes: Vec = codes - .into_iter() - .zip(hashes_to_request) - .take_while(|(b, hash)| ethrex_common::utils::keccak(b) == *hash) - .map(|(b, _hash)| b) - .collect(); - let result = TaskResult { - start_index: chunk_start, - remaining_start: chunk_start + validated_codes.len(), - bytecodes: validated_codes, - peer_id, - remaining_end: chunk_end, - }; - tx.send(result).await.ok(); - } else { - tracing::debug!("Failed to get bytecode"); - tx.send(empty_task_result).await.ok(); - } - }); - } - - METRICS - .downloaded_bytecodes - .fetch_add(downloaded_count, Ordering::Relaxed); - debug!( - "Finished downloading bytecodes, total bytecodes: {}", - all_bytecode_hashes.len() - ); - - Ok(Some(all_bytecodes)) - } - - /// Requests storage ranges for accounts given their hashed address and storage roots, and the root of their state trie - /// account_hashes & storage_roots must have the same length - /// storage_roots must not contain empty trie hashes, we will treat empty ranges as invalid responses - /// Returns true if the last account's storage was not completely fetched by the request - /// Returns the list of hashed storage keys and values for each account's storage or None if: - /// - There are no available peers (the node just started up or was rejected by all other nodes) - /// - No peer returned a valid response in the given time and retry limits - pub async fn request_storage_ranges( - &mut self, - account_storage_roots: &mut AccountStorageRoots, - account_storages_snapshots_dir: &Path, - mut chunk_index: u64, - pivot_header: &mut BlockHeader, - store: Store, - ) -> Result { - METRICS - .current_step - .set(CurrentStepValue::RequestingStorageRanges); - debug!("Starting request_storage_ranges function"); - // 1) split the range in chunks of same length - let mut accounts_by_root_hash: BTreeMap<_, Vec<_>> = BTreeMap::new(); - for (account, (maybe_root_hash, _)) in &account_storage_roots.accounts_with_storage_root { - match maybe_root_hash { - Some(root) => { - accounts_by_root_hash - .entry(*root) - .or_default() - .push(*account); - } - None => { - let root = store - .get_account_state_by_acc_hash(pivot_header.hash(), *account) - .expect("Failed to get account in state trie") - .expect("Could not find account that should have been downloaded or healed") - .storage_root; - accounts_by_root_hash - .entry(root) - .or_default() - .push(*account); - } - } - } - let mut accounts_by_root_hash = Vec::from_iter(accounts_by_root_hash); - // TODO: Turn this into a stable sort for binary search. - accounts_by_root_hash.sort_unstable_by_key(|(_, accounts)| !accounts.len()); - let chunk_size = 300; - let chunk_count = (accounts_by_root_hash.len() / chunk_size) + 1; - - // list of tasks to be executed - // Types are (start_index, end_index, starting_hash) - // NOTE: end_index is NOT inclusive - - let mut tasks_queue_not_started = VecDeque::::new(); - for i in 0..chunk_count { - let chunk_start = chunk_size * i; - let chunk_end = (chunk_start + chunk_size).min(accounts_by_root_hash.len()); - tasks_queue_not_started.push_back(StorageTask { - start_index: chunk_start, - end_index: chunk_end, - start_hash: H256::zero(), - end_hash: None, - }); - } - - // channel to send the tasks to the peers - let (task_sender, mut task_receiver) = - tokio::sync::mpsc::channel::(1000); - - // channel to send the result of dumping storages - let mut disk_joinset: tokio::task::JoinSet> = - tokio::task::JoinSet::new(); - - let mut task_count = tasks_queue_not_started.len(); - let mut completed_tasks = 0; - - // TODO: in a refactor, delete this replace with a structure that can handle removes - let mut accounts_done: HashMap> = HashMap::new(); - // Maps storage root to vector of hashed addresses matching that root and - // vector of hashed storage keys and storage values. - let mut current_account_storages: BTreeMap = BTreeMap::new(); - - let mut logged_no_free_peers_count = 0; - - debug!("Starting request_storage_ranges loop"); - loop { - if current_account_storages - .values() - .map(|accounts| 32 * accounts.accounts.len() + 64 * accounts.storages.len()) - .sum::() - > RANGE_FILE_CHUNK_SIZE - { - let current_account_storages = std::mem::take(&mut current_account_storages); - let snapshot = current_account_storages.into_values().collect::>(); - - if !std::fs::exists(account_storages_snapshots_dir) - .map_err(|_| PeerHandlerError::NoStorageSnapshotsDir)? - { - std::fs::create_dir_all(account_storages_snapshots_dir) - .map_err(|_| PeerHandlerError::CreateStorageSnapshotsDir)?; - } - let account_storages_snapshots_dir_cloned = - account_storages_snapshots_dir.to_path_buf(); - if !disk_joinset.is_empty() { - debug!("Writing to disk"); - disk_joinset - .join_next() - .await - .expect("Shouldn't be empty") - .expect("Shouldn't have a join error") - .inspect_err(|err| { - error!("We found this error while dumping to file {err:?}") - }) - .map_err(PeerHandlerError::DumpError)?; - } - disk_joinset.spawn(async move { - let path = get_account_storages_snapshot_file( - &account_storages_snapshots_dir_cloned, - chunk_index, - ); - dump_storages_to_file(&path, snapshot) - }); - - chunk_index += 1; - } - - if let Ok(result) = task_receiver.try_recv() { - let StorageTaskResult { - start_index, - mut account_storages, - peer_id, - remaining_start, - remaining_end, - remaining_hash_range: (hash_start, hash_end), - } = result; - completed_tasks += 1; - - for (_, accounts) in accounts_by_root_hash[start_index..remaining_start].iter() { - for account in accounts { - if !accounts_done.contains_key(account) { - let (_, old_intervals) = account_storage_roots - .accounts_with_storage_root - .get_mut(account) - .ok_or(PeerHandlerError::UnrecoverableError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; - - if old_intervals.is_empty() { - accounts_done.insert(*account, vec![]); - } - } - } - } - - if remaining_start < remaining_end { - debug!("Failed to download entire chunk from peer {peer_id}"); - if hash_start.is_zero() { - // Task is common storage range request - let task = StorageTask { - start_index: remaining_start, - end_index: remaining_end, - start_hash: H256::zero(), - end_hash: None, - }; - tasks_queue_not_started.push_back(task); - task_count += 1; - } else if let Some(hash_end) = hash_end { - // Task was a big storage account result - if hash_start <= hash_end { - let task = StorageTask { - start_index: remaining_start, - end_index: remaining_end, - start_hash: hash_start, - end_hash: Some(hash_end), - }; - tasks_queue_not_started.push_back(task); - task_count += 1; - - let acc_hash = accounts_by_root_hash[remaining_start].1[0]; - let (_, old_intervals) = account_storage_roots - .accounts_with_storage_root - .get_mut(&acc_hash).ok_or(PeerHandlerError::UnrecoverableError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; - for (old_start, end) in old_intervals { - if end == &hash_end { - *old_start = hash_start; - } - } - account_storage_roots - .healed_accounts - .extend(accounts_by_root_hash[start_index].1.iter().copied()); - } else { - let mut acc_hash: H256 = H256::zero(); - // This search could potentially be expensive, but it's something that should happen very - // infrequently (only when we encounter an account we think it's big but it's not). In - // normal cases the vec we are iterating over just has one element (the big account). - for account in accounts_by_root_hash[remaining_start].1.iter() { - if let Some((_, old_intervals)) = account_storage_roots - .accounts_with_storage_root - .get(account) - { - if !old_intervals.is_empty() { - acc_hash = *account; - } - } else { - continue; - } - } - if acc_hash.is_zero() { - panic!("Should have found the account hash"); - } - let (_, old_intervals) = account_storage_roots - .accounts_with_storage_root - .get_mut(&acc_hash) - .ok_or(PeerHandlerError::UnrecoverableError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; - old_intervals.remove( - old_intervals - .iter() - .position(|(_old_start, end)| end == &hash_end) - .ok_or(PeerHandlerError::UnrecoverableError( - "Could not find an old interval that we were tracking" - .to_owned(), - ))?, - ); - if old_intervals.is_empty() { - for account in accounts_by_root_hash[remaining_start].1.iter() { - accounts_done.insert(*account, vec![]); - account_storage_roots.healed_accounts.insert(*account); - } - } - } - } else { - if remaining_start + 1 < remaining_end { - let task = StorageTask { - start_index: remaining_start + 1, - end_index: remaining_end, - start_hash: H256::zero(), - end_hash: None, - }; - tasks_queue_not_started.push_back(task); - task_count += 1; - } - // Task found a big storage account, so we split the chunk into multiple chunks - let start_hash_u256 = U256::from_big_endian(&hash_start.0); - let missing_storage_range = U256::MAX - start_hash_u256; - - // Big accounts need to be marked for storage healing unconditionally - for account in accounts_by_root_hash[remaining_start].1.iter() { - account_storage_roots.healed_accounts.insert(*account); - } - - let slot_count = account_storages - .last() - .map(|v| v.len()) - .ok_or(PeerHandlerError::NoAccountStorages)? - .max(1); - let storage_density = start_hash_u256 / slot_count; - - let slots_per_chunk = U256::from(10000); - let chunk_size = storage_density - .checked_mul(slots_per_chunk) - .unwrap_or(U256::MAX); - - let chunk_count = (missing_storage_range / chunk_size).as_usize().max(1); - - let maybe_old_intervals = account_storage_roots - .accounts_with_storage_root - .get(&accounts_by_root_hash[remaining_start].1[0]); - - if let Some((_, old_intervals)) = maybe_old_intervals { - if !old_intervals.is_empty() { - for (start_hash, end_hash) in old_intervals { - let task = StorageTask { - start_index: remaining_start, - end_index: remaining_start + 1, - start_hash: *start_hash, - end_hash: Some(*end_hash), - }; - - tasks_queue_not_started.push_back(task); - task_count += 1; - } - } else { - // TODO: DRY - account_storage_roots.accounts_with_storage_root.insert( - accounts_by_root_hash[remaining_start].1[0], - (None, vec![]), - ); - let (_, intervals) = account_storage_roots - .accounts_with_storage_root - .get_mut(&accounts_by_root_hash[remaining_start].1[0]) - .ok_or(PeerHandlerError::UnrecoverableError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; - - for i in 0..chunk_count { - let start_hash_u256 = start_hash_u256 + chunk_size * i; - let start_hash = H256::from_uint(&start_hash_u256); - let end_hash = if i == chunk_count - 1 { - H256::repeat_byte(0xff) - } else { - let end_hash_u256 = start_hash_u256 - .checked_add(chunk_size) - .unwrap_or(U256::MAX); - H256::from_uint(&end_hash_u256) - }; - - let task = StorageTask { - start_index: remaining_start, - end_index: remaining_start + 1, - start_hash, - end_hash: Some(end_hash), - }; - - intervals.push((start_hash, end_hash)); - - tasks_queue_not_started.push_back(task); - task_count += 1; - } - debug!("Split big storage account into {chunk_count} chunks."); - } - } else { - account_storage_roots.accounts_with_storage_root.insert( - accounts_by_root_hash[remaining_start].1[0], - (None, vec![]), - ); - let (_, intervals) = account_storage_roots - .accounts_with_storage_root - .get_mut(&accounts_by_root_hash[remaining_start].1[0]) - .ok_or(PeerHandlerError::UnrecoverableError("Trie to get the old download intervals for an account but did not find them".to_owned()))?; - - for i in 0..chunk_count { - let start_hash_u256 = start_hash_u256 + chunk_size * i; - let start_hash = H256::from_uint(&start_hash_u256); - let end_hash = if i == chunk_count - 1 { - H256::repeat_byte(0xff) - } else { - let end_hash_u256 = start_hash_u256 - .checked_add(chunk_size) - .unwrap_or(U256::MAX); - H256::from_uint(&end_hash_u256) - }; - - let task = StorageTask { - start_index: remaining_start, - end_index: remaining_start + 1, - start_hash, - end_hash: Some(end_hash), - }; - - intervals.push((start_hash, end_hash)); - - tasks_queue_not_started.push_back(task); - task_count += 1; - } - debug!("Split big storage account into {chunk_count} chunks."); - } - } - } - - if account_storages.is_empty() { - self.peer_table.record_failure(&peer_id).await?; - continue; - } - if let Some(hash_end) = hash_end { - // This is a big storage account, and the range might be empty - if account_storages[0].len() == 1 && account_storages[0][0].0 > hash_end { - continue; - } - } - - self.peer_table.record_success(&peer_id).await?; - - let n_storages = account_storages.len(); - let n_slots = account_storages - .iter() - .map(|storage| storage.len()) - .sum::(); - - // These take into account we downloaded the same thing for different accounts - let effective_slots: usize = account_storages - .iter() - .enumerate() - .map(|(i, storages)| { - accounts_by_root_hash[start_index + i].1.len() * storages.len() - }) - .sum(); - - METRICS - .storage_leaves_downloaded - .inc_by(effective_slots as u64); - - debug!("Downloaded {n_storages} storages ({n_slots} slots) from peer {peer_id}"); - debug!( - "Total tasks: {task_count}, completed tasks: {completed_tasks}, queued tasks: {}", - tasks_queue_not_started.len() - ); - // THEN: update insert to read with the correct structure and reuse - // tries, only changing the prefix for insertion. - if account_storages.len() == 1 { - let (root_hash, accounts) = &accounts_by_root_hash[start_index]; - // We downloaded a big storage account - current_account_storages - .entry(*root_hash) - .or_insert_with(|| AccountsWithStorage { - accounts: accounts.clone(), - storages: Vec::new(), - }) - .storages - .extend(account_storages.remove(0)); - } else { - for (i, storages) in account_storages.into_iter().enumerate() { - let (root_hash, accounts) = &accounts_by_root_hash[start_index + i]; - current_account_storages.insert( - *root_hash, - AccountsWithStorage { - accounts: accounts.clone(), - storages, - }, - ); - } - } - } - - if block_is_stale(pivot_header) { - info!("request_storage_ranges became stale, breaking"); - break; - } - - let Some((peer_id, connection)) = self - .peer_table - .get_best_peer(&SUPPORTED_ETH_CAPABILITIES) - .await - .inspect_err(|err| warn!(%err, "Error requesting a peer for storage ranges")) - .unwrap_or(None) - else { - // Log ~ once every 10 seconds - if logged_no_free_peers_count == 0 { - trace!("We are missing peers in request_storage_ranges"); - logged_no_free_peers_count = 1000; - } - logged_no_free_peers_count -= 1; - // Sleep a bit to avoid busy polling - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - }; - - let Some(task) = tasks_queue_not_started.pop_front() else { - if completed_tasks >= task_count { - break; - } - continue; - }; - - let tx = task_sender.clone(); - - // FIXME: this unzip is probably pointless and takes up unnecessary memory. - let (chunk_account_hashes, chunk_storage_roots): (Vec<_>, Vec<_>) = - accounts_by_root_hash[task.start_index..task.end_index] - .iter() - .map(|(root, storages)| (storages[0], *root)) - .unzip(); - - if task_count - completed_tasks < 30 { - debug!( - "Assigning task: {task:?}, account_hash: {}, storage_root: {}", - chunk_account_hashes.first().unwrap_or(&H256::zero()), - chunk_storage_roots.first().unwrap_or(&H256::zero()), - ); - } - let peer_table = self.peer_table.clone(); - - tokio::spawn(PeerHandler::request_storage_ranges_worker( - task, - peer_id, - connection, - peer_table, - pivot_header.state_root, - chunk_account_hashes, - chunk_storage_roots, - tx, - )); - } - - { - let snapshot = current_account_storages.into_values().collect::>(); - - if !std::fs::exists(account_storages_snapshots_dir) - .map_err(|_| PeerHandlerError::NoStorageSnapshotsDir)? - { - std::fs::create_dir_all(account_storages_snapshots_dir) - .map_err(|_| PeerHandlerError::CreateStorageSnapshotsDir)?; - } - let path = - get_account_storages_snapshot_file(account_storages_snapshots_dir, chunk_index); - dump_storages_to_file(&path, snapshot) - .map_err(|_| PeerHandlerError::WriteStorageSnapshotsDir(chunk_index))?; - } - disk_joinset - .join_all() - .await - .into_iter() - .map(|result| { - result - .inspect_err(|err| error!("We found this error while dumping to file {err:?}")) - }) - .collect::, DumpError>>() - .map_err(PeerHandlerError::DumpError)?; - - for (account_done, intervals) in accounts_done { - if intervals.is_empty() { - account_storage_roots - .accounts_with_storage_root - .remove(&account_done); - } - } - - // Dropping the task sender so that the recv returns None - drop(task_sender); - - Ok(chunk_index + 1) - } - - #[allow(clippy::too_many_arguments)] - async fn request_storage_ranges_worker( - task: StorageTask, - peer_id: H256, - mut connection: PeerConnection, - mut peer_table: PeerTable, - state_root: H256, - chunk_account_hashes: Vec, - chunk_storage_roots: Vec, - tx: tokio::sync::mpsc::Sender, - ) -> Result<(), PeerHandlerError> { - let start = task.start_index; - let end = task.end_index; - let start_hash = task.start_hash; - - let empty_task_result = StorageTaskResult { - start_index: task.start_index, - account_storages: Vec::new(), - peer_id, - remaining_start: task.start_index, - remaining_end: task.end_index, - remaining_hash_range: (start_hash, task.end_hash), - }; - let request_id = rand::random(); - let request = RLPxMessage::GetStorageRanges(GetStorageRanges { - id: request_id, - root_hash: state_root, - account_hashes: chunk_account_hashes, - starting_hash: start_hash, - limit_hash: task.end_hash.unwrap_or(HASH_MAX), - response_bytes: MAX_RESPONSE_BYTES, - }); - let Ok(RLPxMessage::StorageRanges(StorageRanges { - id: _, - slots, - proof, - })) = PeerHandler::make_request( - &mut peer_table, - peer_id, - &mut connection, - request, - PEER_REPLY_TIMEOUT, - ) - .await - else { - tracing::debug!("Failed to get storage range"); - tx.send(empty_task_result).await.ok(); - return Ok(()); - }; - if slots.is_empty() && proof.is_empty() { - tx.send(empty_task_result).await.ok(); - tracing::debug!("Received empty storage range"); - return Ok(()); - } - // Check we got some data and no more than the requested amount - if slots.len() > chunk_storage_roots.len() || slots.is_empty() { - tx.send(empty_task_result).await.ok(); - return Ok(()); - } - // Unzip & validate response - let proof = encodable_to_proof(&proof); - let mut account_storages: Vec> = vec![]; - let mut should_continue = false; - // Validate each storage range - let mut storage_roots = chunk_storage_roots.into_iter(); - let last_slot_index = slots.len() - 1; - for (i, next_account_slots) in slots.into_iter().enumerate() { - // We won't accept empty storage ranges - if next_account_slots.is_empty() { - // This shouldn't happen - error!("Received empty storage range, skipping"); - tx.send(empty_task_result.clone()).await.ok(); - return Ok(()); - } - let encoded_values = next_account_slots - .iter() - .map(|slot| slot.data.encode_to_vec()) - .collect::>(); - let hashed_keys: Vec<_> = next_account_slots.iter().map(|slot| slot.hash).collect(); - - let storage_root = match storage_roots.next() { - Some(root) => root, - None => { - tx.send(empty_task_result.clone()).await.ok(); - error!("No storage root for account {i}"); - return Err(PeerHandlerError::NoStorageRoots); - } - }; - - // The proof corresponds to the last slot, for the previous ones the slot must be the full range without edge proofs - if i == last_slot_index && !proof.is_empty() { - let Ok(sc) = verify_range( - storage_root, - &start_hash, - &hashed_keys, - &encoded_values, - &proof, - ) else { - tx.send(empty_task_result).await.ok(); - return Ok(()); - }; - should_continue = sc; - } else if verify_range( - storage_root, - &start_hash, - &hashed_keys, - &encoded_values, - &[], - ) - .is_err() - { - tx.send(empty_task_result.clone()).await.ok(); - return Ok(()); - } - - account_storages.push( - next_account_slots - .iter() - .map(|slot| (slot.hash, slot.data)) - .collect(), - ); - } - let (remaining_start, remaining_end, remaining_start_hash) = if should_continue { - let last_account_storage = match account_storages.last() { - Some(storage) => storage, - None => { - tx.send(empty_task_result.clone()).await.ok(); - error!("No account storage found, this shouldn't happen"); - return Err(PeerHandlerError::NoAccountStorages); - } - }; - let (last_hash, _) = match last_account_storage.last() { - Some(last_hash) => last_hash, - None => { - tx.send(empty_task_result.clone()).await.ok(); - error!("No last hash found, this shouldn't happen"); - return Err(PeerHandlerError::NoAccountStorages); - } - }; - let next_hash_u256 = U256::from_big_endian(&last_hash.0).saturating_add(1.into()); - let next_hash = H256::from_uint(&next_hash_u256); - (start + account_storages.len() - 1, end, next_hash) - } else { - (start + account_storages.len(), end, H256::zero()) - }; - let task_result = StorageTaskResult { - start_index: start, - account_storages, - peer_id, - remaining_start, - remaining_end, - remaining_hash_range: (remaining_start_hash, task.end_hash), - }; - tx.send(task_result).await.ok(); - Ok::<(), PeerHandlerError>(()) - } - - pub async fn request_state_trienodes( - peer_id: H256, - mut connection: PeerConnection, - mut peer_table: PeerTable, - state_root: H256, - paths: Vec, - ) -> Result, RequestStateTrieNodesError> { - let expected_nodes = paths.len(); - // Keep track of peers we requested from so we can penalize unresponsive peers when we get a response - // This is so we avoid penalizing peers due to requesting stale data - - let request_id = rand::random(); - let request = RLPxMessage::GetTrieNodes(GetTrieNodes { - id: request_id, - root_hash: state_root, - // [acc_path, acc_path,...] -> [[acc_path], [acc_path]] - paths: paths - .iter() - .map(|vec| vec![Bytes::from(vec.path.encode_compact())]) - .collect(), - bytes: MAX_RESPONSE_BYTES, - }); - let nodes = match PeerHandler::make_request( - &mut peer_table, - peer_id, - &mut connection, - request, - PEER_REPLY_TIMEOUT, - ) - .await - { - Ok(RLPxMessage::TrieNodes(trie_nodes)) => trie_nodes - .nodes - .iter() - .map(|node| Node::decode(node)) - .collect::, _>>() - .map_err(|e| { - RequestStateTrieNodesError::RequestError(PeerConnectionError::RLPDecodeError(e)) - }), - Ok(other_msg) => Err(RequestStateTrieNodesError::RequestError( - PeerConnectionError::UnexpectedResponse( - "TrieNodes".to_string(), - other_msg.to_string(), - ), - )), - Err(other_err) => Err(RequestStateTrieNodesError::RequestError(other_err)), - }?; - - if nodes.is_empty() || nodes.len() > expected_nodes { - return Err(RequestStateTrieNodesError::InvalidData); - } - - for (index, node) in nodes.iter().enumerate() { - if node.compute_hash().finalize() != paths[index].hash { - error!( - "A peer is sending wrong data for the state trie node {:?}", - paths[index].path - ); - return Err(RequestStateTrieNodesError::InvalidHash); - } - } - - Ok(nodes) - } - - /// Requests storage trie nodes given the root of the state trie where they are contained and - /// a hashmap mapping the path to the account in the state trie (aka hashed address) to the paths to the nodes in its storage trie (can be full or partial) - /// Returns the nodes or None if: - /// - There are no available peers (the node just started up or was rejected by all other nodes) - /// - No peer returned a valid response in the given time and retry limits - pub async fn request_storage_trienodes( - peer_id: H256, - mut connection: PeerConnection, - mut peer_table: PeerTable, - get_trie_nodes: GetTrieNodes, - ) -> Result { - // Keep track of peers we requested from so we can penalize unresponsive peers when we get a response - // This is so we avoid penalizing peers due to requesting stale data - let id = get_trie_nodes.id; - let request = RLPxMessage::GetTrieNodes(get_trie_nodes); - match PeerHandler::make_request( - &mut peer_table, - peer_id, - &mut connection, - request, - PEER_REPLY_TIMEOUT, - ) - .await - { - Ok(RLPxMessage::TrieNodes(trie_nodes)) => Ok(trie_nodes), - Ok(other_msg) => Err(RequestStorageTrieNodes::RequestError( - id, - PeerConnectionError::UnexpectedResponse( - "TrieNodes".to_string(), - other_msg.to_string(), - ), - )), - Err(e) => Err(RequestStorageTrieNodes::RequestError(id, e)), - } - } - /// Returns the PeerData for each connected Peer pub async fn read_connected_peers(&mut self) -> Vec { self.peer_table @@ -1985,40 +639,12 @@ fn format_duration(duration: Duration) -> String { format!("{hours:02}h {minutes:02}m {seconds:02}s") } -pub struct DumpError { - pub path: PathBuf, - pub contents: Vec, - pub error: ErrorKind, -} - -impl core::fmt::Debug for DumpError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("DumpError") - .field("path", &self.path) - .field("contents_len", &self.contents.len()) - .field("error", &self.error) - .finish() - } -} - #[derive(thiserror::Error, Debug)] pub enum PeerHandlerError { #[error("Failed to send message to peer: {0}")] SendMessageToPeer(String), #[error("Failed to receive block headers")] BlockHeaders, - #[error("Accounts state snapshots dir does not exist")] - NoStateSnapshotsDir, - #[error("Failed to create accounts state snapshots dir")] - CreateStateSnapshotsDir, - #[error("Failed to write account_state_snapshot chunk {0}")] - WriteStateSnapshotsDir(u64), - #[error("Accounts storage snapshots dir does not exist")] - NoStorageSnapshotsDir, - #[error("Failed to create accounts storage snapshots dir")] - CreateStorageSnapshotsDir, - #[error("Failed to write account_storages_snapshot chunk {0}")] - WriteStorageSnapshotsDir(u64), #[error("Received unexpected response from peer {0}")] UnexpectedResponseFromPeer(H256), #[error("Received an empty response from peer {0}")] @@ -2031,44 +657,10 @@ pub enum PeerHandlerError { InvalidHeaders, #[error("Storage Full")] StorageFull, - #[error("No tasks in queue")] - NoTasks, - #[error("No account hashes")] - AccountHashes, - #[error("No account storages")] - NoAccountStorages, - #[error("No storage roots")] - NoStorageRoots, #[error("No response from peer")] NoResponseFromPeer, - #[error("Dumping snapshots to disk failed {0:?}")] - DumpError(DumpError), - #[error("Encountered an unexpected error. This is a bug {0}")] - UnrecoverableError(String), #[error("Error in Peer Table: {0}")] PeerTableError(#[from] PeerTableError), -} - -#[derive(Debug, Clone)] -pub struct RequestMetadata { - pub hash: H256, - pub path: Nibbles, - /// What node is the parent of this node - pub parent_path: Nibbles, -} - -#[derive(Debug, thiserror::Error)] -pub enum RequestStateTrieNodesError { - #[error("Send request error")] - RequestError(PeerConnectionError), - #[error("Invalid data")] - InvalidData, - #[error("Invalid Hash")] - InvalidHash, -} - -#[derive(Debug, thiserror::Error)] -pub enum RequestStorageTrieNodes { - #[error("Send request error")] - RequestError(u64, PeerConnectionError), + #[error("Snap error: {0}")] + Snap(#[from] SnapError), } diff --git a/crates/networking/p2p/rlpx/connection/server.rs b/crates/networking/p2p/rlpx/connection/server.rs index 8d0997f33af..eea97789a7d 100644 --- a/crates/networking/p2p/rlpx/connection/server.rs +++ b/crates/networking/p2p/rlpx/connection/server.rs @@ -1095,11 +1095,13 @@ async fn handle_incoming_message( } Message::GetByteCodes(req) => { let storage_clone = state.storage.clone(); - let response = process_byte_codes_request(req, storage_clone).map_err(|_| { - PeerConnectionError::InternalError( - "Failed to execute bytecode retrieval task".to_string(), - ) - })?; + let response = process_byte_codes_request(req, storage_clone) + .await + .map_err(|_| { + PeerConnectionError::InternalError( + "Failed to execute bytecode retrieval task".to_string(), + ) + })?; send(state, Message::ByteCodes(response)).await? } Message::GetTrieNodes(req) => { diff --git a/crates/networking/p2p/rlpx/error.rs b/crates/networking/p2p/rlpx/error.rs index f475f317d26..88a56e59198 100644 --- a/crates/networking/p2p/rlpx/error.rs +++ b/crates/networking/p2p/rlpx/error.rs @@ -1,5 +1,6 @@ use super::{message::Message, p2p::DisconnectReason}; use crate::peer_table::PeerTableError; +use crate::snap::error::SnapError; use aes::cipher::InvalidLength; use ethrex_blockchain::error::{ChainError, MempoolError}; use ethrex_rlp::error::{RLPDecodeError, RLPEncodeError}; @@ -131,3 +132,14 @@ impl From for PeerConnectionError { PeerConnectionError::RecvError(e.to_string()) } } + +impl From for PeerConnectionError { + fn from(e: SnapError) -> Self { + match e { + SnapError::Store(e) => PeerConnectionError::StoreError(e), + SnapError::Protocol(e) => e, + SnapError::BadRequest(msg) => PeerConnectionError::BadRequest(msg), + other => PeerConnectionError::InternalError(other.to_string()), + } + } +} diff --git a/crates/networking/p2p/rlpx/snap.rs b/crates/networking/p2p/rlpx/snap/codec.rs similarity index 82% rename from crates/networking/p2p/rlpx/snap.rs rename to crates/networking/p2p/rlpx/snap/codec.rs index a249fc1aa57..58f4604cfed 100644 --- a/crates/networking/p2p/rlpx/snap.rs +++ b/crates/networking/p2p/rlpx/snap/codec.rs @@ -1,12 +1,18 @@ -use super::{ +//! Snap protocol message encoding/decoding +//! +//! This module implements RLPxMessage for snap protocol messages, +//! as well as RLP encoding/decoding for helper types. + +use super::messages::{ + AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, GetStorageRanges, + GetTrieNodes, StorageRanges, StorageSlot, TrieNodes, +}; +use crate::rlpx::{ message::RLPxMessage, utils::{snappy_compress, snappy_decompress}, }; use bytes::{BufMut, Bytes}; -use ethrex_common::{ - H256, U256, - types::{AccountState, AccountStateSlimCodec}, -}; +use ethrex_common::{H256, U256, types::AccountStateSlimCodec}; use ethrex_rlp::{ decode::RLPDecode, encode::RLPEncode, @@ -14,74 +20,29 @@ use ethrex_rlp::{ structs::{Decoder, Encoder}, }; -// Snap Capability Messages - -#[derive(Debug, Clone)] -pub struct GetAccountRange { - // id is a u64 chosen by the requesting peer, the responding peer must mirror the value for the response - pub id: u64, - pub root_hash: H256, - pub starting_hash: H256, - pub limit_hash: H256, - pub response_bytes: u64, -} - -#[derive(Debug, Clone)] -pub struct AccountRange { - // id is a u64 chosen by the requesting peer, the responding peer must mirror the value for the response - pub id: u64, - pub accounts: Vec, - pub proof: Vec, -} - -#[derive(Debug, Clone)] -pub struct GetStorageRanges { - pub id: u64, - pub root_hash: H256, - pub account_hashes: Vec, - pub starting_hash: H256, - pub limit_hash: H256, - pub response_bytes: u64, -} - -#[derive(Debug, Clone)] -pub struct StorageRanges { - pub id: u64, - pub slots: Vec>, - pub proof: Vec, -} - -#[derive(Debug, Clone)] -pub struct GetByteCodes { - pub id: u64, - pub hashes: Vec, - pub bytes: u64, -} - -#[derive(Debug, Clone)] -pub struct ByteCodes { - pub id: u64, - pub codes: Vec, -} - -#[derive(Debug, Clone)] -pub struct GetTrieNodes { - pub id: u64, - pub root_hash: H256, - // [[acc_path, slot_path_1, slot_path_2,...]...] - // The paths can be either full paths (hash) or only the partial path (compact-encoded nibbles) - pub paths: Vec>, - pub bytes: u64, +// ============================================================================= +// MESSAGE CODES +// ============================================================================= + +/// Snap protocol message codes +pub mod codes { + pub const GET_ACCOUNT_RANGE: u8 = 0x00; + pub const ACCOUNT_RANGE: u8 = 0x01; + pub const GET_STORAGE_RANGES: u8 = 0x02; + pub const STORAGE_RANGES: u8 = 0x03; + pub const GET_BYTE_CODES: u8 = 0x04; + pub const BYTE_CODES: u8 = 0x05; + pub const GET_TRIE_NODES: u8 = 0x06; + pub const TRIE_NODES: u8 = 0x07; } -#[derive(Debug, Clone)] -pub struct TrieNodes { - pub id: u64, - pub nodes: Vec, -} +// ============================================================================= +// RLPX MESSAGE IMPLEMENTATIONS +// ============================================================================= impl RLPxMessage for GetAccountRange { - const CODE: u8 = 0x00; + const CODE: u8 = codes::GET_ACCOUNT_RANGE; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -118,7 +79,8 @@ impl RLPxMessage for GetAccountRange { } impl RLPxMessage for AccountRange { - const CODE: u8 = 0x01; + const CODE: u8 = codes::ACCOUNT_RANGE; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -149,7 +111,8 @@ impl RLPxMessage for AccountRange { } impl RLPxMessage for GetStorageRanges { - const CODE: u8 = 0x02; + const CODE: u8 = codes::GET_STORAGE_RANGES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -172,12 +135,14 @@ impl RLPxMessage for GetStorageRanges { let (id, decoder) = decoder.decode_field("request-id")?; let (root_hash, decoder) = decoder.decode_field("rootHash")?; let (account_hashes, decoder) = decoder.decode_field("accountHashes")?; + // Handle empty starting_hash as default (zero hash) let (starting_hash, decoder): (Bytes, _) = decoder.decode_field("startingHash")?; let starting_hash = if !starting_hash.is_empty() { H256::from_slice(&starting_hash) } else { Default::default() }; + // Handle empty limit_hash as max hash let (limit_hash, decoder): (Bytes, _) = decoder.decode_field("limitHash")?; let limit_hash = if !limit_hash.is_empty() { H256::from_slice(&limit_hash) @@ -199,7 +164,8 @@ impl RLPxMessage for GetStorageRanges { } impl RLPxMessage for StorageRanges { - const CODE: u8 = 0x03; + const CODE: u8 = codes::STORAGE_RANGES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -226,7 +192,8 @@ impl RLPxMessage for StorageRanges { } impl RLPxMessage for GetByteCodes { - const CODE: u8 = 0x04; + const CODE: u8 = codes::GET_BYTE_CODES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -253,7 +220,8 @@ impl RLPxMessage for GetByteCodes { } impl RLPxMessage for ByteCodes { - const CODE: u8 = 0x05; + const CODE: u8 = codes::BYTE_CODES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -278,7 +246,8 @@ impl RLPxMessage for ByteCodes { } impl RLPxMessage for GetTrieNodes { - const CODE: u8 = 0x06; + const CODE: u8 = codes::GET_TRIE_NODES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -312,7 +281,8 @@ impl RLPxMessage for GetTrieNodes { } impl RLPxMessage for TrieNodes { - const CODE: u8 = 0x07; + const CODE: u8 = codes::TRIE_NODES; + fn encode(&self, buf: &mut dyn BufMut) -> Result<(), RLPEncodeError> { let mut encoded_data = vec![]; Encoder::new(&mut encoded_data) @@ -336,19 +306,9 @@ impl RLPxMessage for TrieNodes { } } -// Intermediate structures - -#[derive(Debug, Clone)] -pub struct AccountRangeUnit { - pub hash: H256, - pub account: AccountState, -} - -#[derive(Debug, Clone)] -pub struct StorageSlot { - pub hash: H256, - pub data: U256, -} +// ============================================================================= +// RLP IMPLEMENTATIONS FOR HELPER TYPES +// ============================================================================= impl RLPEncode for AccountRangeUnit { fn encode(&self, buf: &mut dyn BufMut) { diff --git a/crates/networking/p2p/rlpx/snap/messages.rs b/crates/networking/p2p/rlpx/snap/messages.rs new file mode 100644 index 00000000000..699e592c5af --- /dev/null +++ b/crates/networking/p2p/rlpx/snap/messages.rs @@ -0,0 +1,134 @@ +//! Snap protocol message definitions +//! +//! This module contains the message types used in the snap sync protocol. +//! Each message type implements RLPxMessage for encoding/decoding. + +use bytes::Bytes; +use ethrex_common::{H256, U256, types::AccountState}; + +// ============================================================================= +// REQUEST MESSAGES +// ============================================================================= + +/// Request a range of accounts from the state trie. +#[derive(Debug, Clone)] +pub struct GetAccountRange { + /// Request ID - the responding peer must mirror this value + pub id: u64, + /// State root hash to query against + pub root_hash: H256, + /// Starting hash of the account range + pub starting_hash: H256, + /// Limit hash of the account range (inclusive) + pub limit_hash: H256, + /// Maximum response size in bytes + pub response_bytes: u64, +} + +/// Request storage ranges for multiple accounts. +#[derive(Debug, Clone)] +pub struct GetStorageRanges { + /// Request ID - the responding peer must mirror this value + pub id: u64, + /// State root hash to query against + pub root_hash: H256, + /// List of account hashes to get storage for + pub account_hashes: Vec, + /// Starting hash of the storage range + pub starting_hash: H256, + /// Limit hash of the storage range (inclusive) + pub limit_hash: H256, + /// Maximum response size in bytes + pub response_bytes: u64, +} + +/// Request bytecodes by their hashes. +#[derive(Debug, Clone)] +pub struct GetByteCodes { + /// Request ID - the responding peer must mirror this value + pub id: u64, + /// List of code hashes to retrieve + pub hashes: Vec, + /// Maximum response size in bytes + pub bytes: u64, +} + +/// Request trie nodes from state or storage tries. +#[derive(Debug, Clone)] +pub struct GetTrieNodes { + /// Request ID - the responding peer must mirror this value + pub id: u64, + /// State root hash to query against + pub root_hash: H256, + /// Paths to trie nodes: [[acc_path, slot_path_1, slot_path_2,...]...] + /// Paths can be full paths (hash) or partial paths (compact-encoded nibbles) + pub paths: Vec>, + /// Maximum response size in bytes + pub bytes: u64, +} + +// ============================================================================= +// RESPONSE MESSAGES +// ============================================================================= + +/// Response containing a range of accounts. +#[derive(Debug, Clone)] +pub struct AccountRange { + /// Request ID - mirrors the value from the request + pub id: u64, + /// List of accounts in the range + pub accounts: Vec, + /// Merkle proof for the returned range + pub proof: Vec, +} + +/// Response containing storage ranges for accounts. +#[derive(Debug, Clone)] +pub struct StorageRanges { + /// Request ID - mirrors the value from the request + pub id: u64, + /// Storage slots for each requested account + pub slots: Vec>, + /// Merkle proof for the returned range + pub proof: Vec, +} + +/// Response containing bytecodes. +#[derive(Debug, Clone)] +pub struct ByteCodes { + /// Request ID - mirrors the value from the request + pub id: u64, + /// Contract bytecodes + pub codes: Vec, +} + +/// Response containing trie nodes. +#[derive(Debug, Clone)] +pub struct TrieNodes { + /// Request ID - mirrors the value from the request + pub id: u64, + /// Trie nodes + pub nodes: Vec, +} + +// ============================================================================= +// HELPER TYPES +// ============================================================================= + +/// A single account entry in an AccountRange response. +#[derive(Debug, Clone)] +pub struct AccountRangeUnit { + /// Hash of the account address + pub hash: H256, + /// Account state + pub account: AccountState, +} + +/// A single storage slot entry. +#[derive(Debug, Clone)] +pub struct StorageSlot { + /// Hash of the storage key + pub hash: H256, + /// Storage value + pub data: U256, +} diff --git a/crates/networking/p2p/rlpx/snap/mod.rs b/crates/networking/p2p/rlpx/snap/mod.rs new file mode 100644 index 00000000000..647a92de7e9 --- /dev/null +++ b/crates/networking/p2p/rlpx/snap/mod.rs @@ -0,0 +1,29 @@ +//! Snap Sync Protocol RLPx Messages +//! +//! This module contains the message types and codec implementations for +//! the snap sync protocol (snap/1). +//! +//! ## Module Structure +//! +//! - `messages`: Message struct definitions +//! - `codec`: RLPxMessage and RLP encoding implementations +//! +//! ## Protocol Overview +//! +//! The snap protocol defines 8 message types: +//! - GetAccountRange / AccountRange: Request/response for account state ranges +//! - GetStorageRanges / StorageRanges: Request/response for storage ranges +//! - GetByteCodes / ByteCodes: Request/response for contract bytecodes +//! - GetTrieNodes / TrieNodes: Request/response for trie nodes + +mod codec; +mod messages; + +// Re-export all message types +pub use messages::{ + AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, GetStorageRanges, + GetTrieNodes, StorageRanges, StorageSlot, TrieNodes, +}; + +// Re-export message codes for protocol handling +pub use codec::codes; diff --git a/crates/networking/p2p/snap.rs b/crates/networking/p2p/snap.rs deleted file mode 100644 index 9bfaafe1f55..00000000000 --- a/crates/networking/p2p/snap.rs +++ /dev/null @@ -1,1008 +0,0 @@ -use bytes::Bytes; -use ethrex_rlp::encode::RLPEncode; -use ethrex_storage::{Store, error::StoreError}; - -use crate::rlpx::{ - error::PeerConnectionError, - snap::{ - AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, GetStorageRanges, - GetTrieNodes, StorageRanges, StorageSlot, TrieNodes, - }, -}; -use ethrex_common::types::AccountStateSlimCodec; - -// Request Processing - -pub async fn process_account_range_request( - request: GetAccountRange, - store: Store, -) -> Result { - tokio::task::spawn_blocking(move || { - let mut accounts = vec![]; - let mut bytes_used = 0; - for (hash, account) in store.iter_accounts_from(request.root_hash, request.starting_hash)? { - debug_assert!(hash >= request.starting_hash); - bytes_used += 32 + AccountStateSlimCodec(account).length() as u64; - accounts.push(AccountRangeUnit { hash, account }); - if hash >= request.limit_hash || bytes_used >= request.response_bytes { - break; - } - } - let proof = proof_to_encodable(store.get_account_range_proof( - request.root_hash, - request.starting_hash, - accounts.last().map(|acc| acc.hash), - )?); - Ok(AccountRange { - id: request.id, - accounts, - proof, - }) - }) - .await - .map_err(|e| StoreError::Custom(format!("task panicked: {e}")))? -} - -pub async fn process_storage_ranges_request( - request: GetStorageRanges, - store: Store, -) -> Result { - tokio::task::spawn_blocking(move || { - let mut slots = vec![]; - let mut proof = vec![]; - let mut bytes_used = 0; - - for hashed_address in request.account_hashes { - let mut account_slots = vec![]; - let mut res_capped = false; - - if let Some(storage_iter) = - store.iter_storage_from(request.root_hash, hashed_address, request.starting_hash)? - { - for (hash, data) in storage_iter { - debug_assert!(hash >= request.starting_hash); - bytes_used += 64_u64; // slot size - account_slots.push(StorageSlot { hash, data }); - if hash >= request.limit_hash || bytes_used >= request.response_bytes { - if bytes_used >= request.response_bytes { - res_capped = true; - } - break; - } - } - } - - // Generate proofs only if the response doesn't contain the full storage range for the account - // Aka if the starting hash is not zero or if the response was capped due to byte limit - if !request.starting_hash.is_zero() || res_capped && !account_slots.is_empty() { - proof.extend(proof_to_encodable( - store - .get_storage_range_proof( - request.root_hash, - hashed_address, - request.starting_hash, - account_slots.last().map(|acc| acc.hash), - )? - .unwrap_or_default(), - )); - } - - if !account_slots.is_empty() { - slots.push(account_slots); - } - - if bytes_used >= request.response_bytes { - break; - } - } - Ok(StorageRanges { - id: request.id, - slots, - proof, - }) - }) - .await - .map_err(|e| StoreError::Custom(format!("task panicked: {e}")))? -} - -pub fn process_byte_codes_request( - request: GetByteCodes, - store: Store, -) -> Result { - let mut codes = vec![]; - let mut bytes_used = 0; - for code_hash in request.hashes { - if let Some(code) = store.get_account_code(code_hash)?.map(|c| c.bytecode) { - bytes_used += code.len() as u64; - codes.push(code); - } - if bytes_used >= request.bytes { - break; - } - } - Ok(ByteCodes { - id: request.id, - codes, - }) -} - -pub async fn process_trie_nodes_request( - request: GetTrieNodes, - store: Store, -) -> Result { - tokio::task::spawn_blocking(move || { - let mut nodes = vec![]; - let mut remaining_bytes = request.bytes; - for paths in request.paths { - if paths.is_empty() { - return Err(PeerConnectionError::BadRequest( - "zero-item pathset requested".to_string(), - )); - } - let trie_nodes = store.get_trie_nodes( - request.root_hash, - paths.into_iter().map(|bytes| bytes.to_vec()).collect(), - remaining_bytes, - )?; - nodes.extend(trie_nodes.iter().map(|nodes| Bytes::copy_from_slice(nodes))); - remaining_bytes = remaining_bytes - .saturating_sub(trie_nodes.iter().fold(0, |acc, nodes| acc + nodes.len()) as u64); - if remaining_bytes == 0 { - break; - } - } - - Ok(TrieNodes { - id: request.id, - nodes, - }) - }) - .await - .map_err(|e| StoreError::Custom(format!("task panicked: {e}")))? -} - -// Helper method to convert proof to RLP-encodable format -#[inline] -pub(crate) fn proof_to_encodable(proof: Vec>) -> Vec { - proof.into_iter().map(Bytes::from).collect() -} - -// Helper method to obtain proof from RLP-encodable format -#[inline] -pub(crate) fn encodable_to_proof(proof: &[Bytes]) -> Vec> { - proof.iter().map(|bytes| bytes.to_vec()).collect() -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use ethrex_common::{BigEndianHash, H256, types::AccountStateSlimCodec}; - use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; - use ethrex_storage::EngineType; - use ethrex_trie::EMPTY_TRIE_HASH; - - use super::*; - - // Hive `AccounRange` Tests - // Requests & invariantes taken from https://github.com/ethereum/go-ethereum/blob/3e567b8b2901611f004b5a6070a9b6d286be128d/cmd/devp2p/internal/ethtest/snap.go#L69 - - use lazy_static::lazy_static; - - lazy_static! { - // Constant values for hive `AccountRange` tests - static ref HASH_MIN: H256 = H256::zero(); - static ref HASH_MAX: H256 = - H256::from_str("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",) - .unwrap(); - static ref HASH_FIRST: H256 = - H256::from_str("0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6") - .unwrap(); - static ref HASH_SECOND: H256 = - H256::from_str("0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f") - .unwrap(); - static ref HASH_FIRST_MINUS_500: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 500)); - static ref HASH_FIRST_MINUS_450: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 450)); - static ref HASH_FIRST_MINUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 1)); - static ref HASH_FIRST_PLUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() + 1)); - } - - #[tokio::test] - async fn hive_account_range_a() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MAX, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 86); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!( - res.accounts.last().unwrap().hash, - H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") - .unwrap() - ); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_b() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MAX, - response_bytes: 3000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 65); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!( - res.accounts.last().unwrap().hash, - H256::from_str("0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6") - .unwrap() - ); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_c() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MAX, - response_bytes: 2000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 44); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!( - res.accounts.last().unwrap().hash, - H256::from_str("0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595") - .unwrap() - ); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_d() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MAX, - response_bytes: 1, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_e() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MAX, - response_bytes: 0, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_f() -> Result<(), StoreError> { - // In this test, we request a range where startingHash is before the first available - // account key, and limitHash is after. The server should return the first and second - // account of the state (because the second account is the 'next available'). - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST_MINUS_500, - limit_hash: *HASH_FIRST_PLUS_ONE, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 2); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_SECOND); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_g() -> Result<(), StoreError> { - // Here we request range where both bounds are before the first available account key. - // This should return the first account (even though it's out of bounds). - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST_MINUS_500, - limit_hash: *HASH_FIRST_MINUS_450, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_h() -> Result<(), StoreError> { - // In this test, both startingHash and limitHash are zero. - // The server should return the first available account. - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_MIN, - limit_hash: *HASH_MIN, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_i() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST, - limit_hash: *HASH_MAX, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 86); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!( - res.accounts.last().unwrap().hash, - H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") - .unwrap() - ); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_j() -> Result<(), StoreError> { - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST_PLUS_ONE, - limit_hash: *HASH_MAX, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 86); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_SECOND); - assert_eq!( - res.accounts.last().unwrap().hash, - H256::from_str("0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa") - .unwrap() - ); - Ok(()) - } - - // Tests for different roots skipped (we don't have other state's data loaded) - - // Non-sensical requests - - #[tokio::test] - async fn hive_account_range_k() -> Result<(), StoreError> { - // In this test, the startingHash is the first available key, and limitHash is - // a key before startingHash (wrong order). The server should return the first available key. - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST, - limit_hash: *HASH_FIRST_MINUS_ONE, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - #[tokio::test] - async fn hive_account_range_m() -> Result<(), StoreError> { - // In this test, the startingHash is the first available key and limitHash is zero. - // (wrong order). The server should return the first available key. - let (store, root) = setup_initial_state()?; - let request = GetAccountRange { - id: 0, - root_hash: root, - starting_hash: *HASH_FIRST, - limit_hash: *HASH_MIN, - response_bytes: 4000, - }; - let res = process_account_range_request(request, store).await.unwrap(); - // Check test invariants - assert_eq!(res.accounts.len(), 1); - assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); - assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); - Ok(()) - } - - // Initial state setup for hive snap tests - - fn setup_initial_state() -> Result<(Store, H256), StoreError> { - // We cannot process the old blocks that hive uses for the devp2p snap tests - // So I copied the state from a geth execution of the test suite - - // State was trimmed to only the first 100 accounts (as the furthest account used by the tests is account 87) - // If the full 408 account state is needed check out previous commits the PR that added this code - - let accounts: Vec<(&str, Vec)> = vec![ - ( - "0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6", - vec![ - 228_u8, 1, 128, 160, 223, 151, 249, 75, 196, 116, 113, 135, 6, 6, 246, 38, 251, - 122, 11, 66, 238, 210, 212, 95, 204, 132, 220, 18, 0, 206, 98, 247, 131, 29, - 169, 144, 128, - ], - ), - ( - "0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f", - vec![196, 128, 1, 128, 128], - ), - ( - "0x00aa781aff39a8284ef43790e3a511b2caa50803613c5096bc782e8de08fa4c5", - vec![ - 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, - 128, 128, - ], - ), - ( - "0x016d92531f4754834b0502de5b0342ceff21cde5bef386a83d2292f4445782c2", - vec![196, 128, 1, 128, 128], - ), - ( - "0x02547b56492bfe767f3d18be2aab96441c449cd945770ef7ef8555acc505b2e4", - vec![196, 128, 1, 128, 128], - ), - ( - "0x025f478d53bf78add6fa3708d9e061d59bfe14b21329b2a4cf1156d4f81b3d2d", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x0267c643f67b47cac9efacf6fcf0e4f4e1b273a727ded155db60eb9907939eb6", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x0304d8eaccf0b942c468074250cbcb625ec5c4688b6b5d17d2a9bdd8dd565d5a", - vec![ - 228, 1, 128, 160, 224, 12, 73, 166, 88, 73, 208, 92, 191, 39, 164, 215, 120, - 138, 104, 188, 123, 96, 19, 174, 51, 65, 29, 64, 188, 137, 40, 47, 192, 100, - 243, 61, 128, - ], - ), - ( - "0x0463e52cda557221b0b66bd7285b043071df4c2ab146260f4e010970f3a0cccf", - vec![196, 1, 128, 128, 128], - ), - ( - "0x04d9aa4f67f8b24d70a0ffd757e82456d9184113106b7d9e8eb6c3e8a8df27ee", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x053df2c3b574026812b154a99b13b626220af85cd01bb1693b1d42591054bce6", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x0579e46a5ed8a88504ac7d579b12eb346fbe4fd7e281bdd226b891f8abed4789", - vec![ - 228, 1, 128, 160, 61, 14, 43, 165, 55, 243, 89, 65, 6, 135, 9, 69, 15, 37, 254, - 228, 90, 175, 77, 198, 174, 46, 210, 42, 209, 46, 7, 67, 172, 124, 84, 167, - 128, - ], - ), - ( - "0x05f6de281d8c2b5d98e8e01cd529bd76416b248caf11e0552047c5f1d516aab6", - vec![196, 128, 1, 128, 128], - ), - ( - "0x07b49045c401bcc408f983d91a199c908cdf0d646049b5b83629a70b0117e295", - vec![ - 228, 1, 128, 160, 134, 154, 203, 146, 159, 89, 28, 84, 203, 133, 132, 42, 81, - 242, 150, 99, 94, 125, 137, 87, 152, 197, 71, 162, 147, 175, 228, 62, 123, 247, - 244, 23, 128, - ], - ), - ( - "0x0993fd5b750fe4414f93c7880b89744abb96f7af1171ed5f47026bdf01df1874", - vec![196, 128, 1, 128, 128], - ), - ( - "0x099d5081762b8b265e8ba4cd8e43f08be4715d903a0b1d96b3d9c4e811cbfb33", - vec![196, 128, 1, 128, 128], - ), - ( - "0x09d6e6745d272389182a510994e2b54d14b731fac96b9c9ef434bc1924315371", - vec![196, 128, 128, 128, 128], - ), - ( - "0x0a93a7231976ad485379a3b66c2d8983ba0b2ca87abaf0ca44836b2a06a2b102", - vec![196, 128, 1, 128, 128], - ), - ( - "0x0b564e4a0203cbcec8301709a7449e2e7371910778df64c89f48507390f2d129", - vec![196, 1, 128, 128, 128], - ), - ( - "0x0cd2a7c53c76f228ed3aa7a29644b1915fde9ec22e0433808bf5467d914e7c7a", - vec![ - 228, 1, 128, 160, 7, 84, 3, 90, 164, 7, 51, 129, 162, 17, 52, 43, 80, 125, 232, - 231, 117, 201, 124, 150, 16, 150, 230, 226, 39, 93, 240, 191, 203, 179, 160, - 28, 128, - ], - ), - ( - "0x0e0e4646090b881949ec9991e48dec768ccd1980896aefd0d51fd56fd5689790", - vec![ - 228, 1, 128, 160, 96, 252, 105, 16, 13, 142, 99, 38, 103, 200, 11, 148, 212, - 52, 0, 136, 35, 237, 117, 65, 107, 113, 203, 209, 18, 180, 208, 176, 47, 86, - 48, 39, 128, - ], - ), - ( - "0x0e27113c09de0a0cb0ff268c677aba17d39a3190fe15aec0ff7f54184955cba4", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x0e57ffa6cc6cbd96c1400150417dd9b30d958c58f63c36230a90a02b076f78b5", - vec![196, 128, 1, 128, 128], - ), - ( - "0x0f30822f90f33f1d1ba6d1521a00935630d2c81ab12fa03d4a0f4915033134f3", - vec![ - 228, 1, 128, 160, 128, 120, 243, 37, 157, 129, 153, 183, 202, 57, 213, 30, 53, - 213, 181, 141, 113, 255, 20, 134, 6, 115, 16, 96, 56, 109, 50, 60, 93, 25, 24, - 44, 128, - ], - ), - ( - "0x1017b10a7cc3732d729fe1f71ced25e5b7bc73dc62ca61309a8c7e5ac0af2f72", - vec![196, 1, 128, 128, 128], - ), - ( - "0x1098f06082dc467088ecedb143f9464ebb02f19dc10bd7491b03ba68d751ce45", - vec![196, 1, 128, 128, 128], - ), - ( - "0x11eb0304c1baa92e67239f6947cb93e485a7db05e2b477e1167a8960458fa8cc", - vec![196, 1, 128, 128, 128], - ), - ( - "0x12be3bf1f9b1dab5f908ca964115bee3bcff5371f84ede45bc60591b21117c51", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x12c1bb3dddf0f06f62d70ed5b7f7db7d89b591b3f23a838062631c4809c37196", - vec![196, 128, 1, 128, 128], - ), - ( - "0x12e394ad62e51261b4b95c431496e46a39055d7ada7dbf243f938b6d79054630", - vec![196, 1, 128, 128, 128], - ), - ( - "0x13cfc46f6bdb7a1c30448d41880d061c3b8d36c55a29f1c0c8d95a8e882b8c25", - vec![ - 228, 1, 128, 160, 148, 79, 9, 90, 251, 209, 56, 62, 93, 15, 145, 239, 2, 137, - 93, 57, 143, 79, 118, 253, 182, 216, 106, 223, 71, 101, 242, 91, 220, 48, 79, - 95, 128, - ], - ), - ( - "0x15293aec87177f6c88f58bc51274ba75f1331f5cb94f0c973b1deab8b3524dfe", - vec![196, 128, 1, 128, 128], - ), - ( - "0x170c927130fe8f1db3ae682c22b57f33f54eb987a7902ec251fe5dba358a2b25", - vec![196, 128, 1, 128, 128], - ), - ( - "0x17350c7adae7f08d7bbb8befcc97234462831638443cd6dfea186cbf5a08b7c7", - vec![ - 228, 1, 128, 160, 76, 231, 156, 217, 100, 86, 80, 240, 160, 14, 255, 168, 111, - 111, 234, 115, 60, 236, 234, 158, 162, 105, 100, 130, 143, 242, 92, 240, 87, - 123, 201, 116, 128, - ], - ), - ( - "0x174f1a19ff1d9ef72d0988653f31074cb59e2cf37cd9d2992c7b0dd3d77d84f9", - vec![196, 128, 1, 128, 128], - ), - ( - "0x17984cc4b4aac0492699d37662b53ec2acf8cbe540c968b817061e4ed27026d0", - vec![196, 128, 1, 128, 128], - ), - ( - "0x181abdd5e212171007e085fdc284a84d42d5bfc160960d881ccb6a10005ff089", - vec![196, 1, 128, 128, 128], - ), - ( - "0x188111c233bf6516bb9da8b5c4c31809a42e8604cd0158d933435cfd8e06e413", - vec![196, 1, 128, 128, 128], - ), - ( - "0x18f4256a59e1b2e01e96ac465e1d14a45d789ce49728f42082289fc25cf32b8d", - vec![196, 128, 1, 128, 128], - ), - ( - "0x1960414a11f8896c7fc4243aba7ed8179b0bc6979b7c25da7557b17f5dee7bf7", - vec![196, 1, 128, 128, 128], - ), - ( - "0x1a28912018f78f7e754df6b9fcec33bea25e5a232224db622e0c3343cf079eff", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x1bf7626cec5330a127e439e68e6ee1a1537e73b2de1aa6d6f7e06bc0f1e9d763", - vec![196, 128, 1, 128, 128], - ), - ( - "0x1c248f110218eaae2feb51bc82e9dcc2844bf93b88172c52afcb86383d262323", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595", - vec![ - 228, 1, 128, 160, 175, 134, 126, 108, 186, 232, 16, 202, 169, 36, 184, 182, - 172, 61, 140, 8, 145, 131, 20, 145, 166, 144, 109, 208, 190, 122, 211, 36, 220, - 209, 83, 61, 128, - ], - ), - ( - "0x1d38ada74301c31f3fd7d92dd5ce52dc37ae633e82ac29c4ef18dfc141298e26", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x1d6ee979097e29141ad6b97ae19bb592420652b7000003c55eb52d5225c3307d", - vec![ - 228, 1, 128, 160, 247, 53, 145, 231, 145, 175, 76, 124, 95, 160, 57, 195, 61, - 217, 209, 105, 202, 177, 75, 29, 155, 12, 167, 139, 204, 78, 116, 13, 85, 59, - 26, 207, 128, - ], - ), - ( - "0x1dff76635b74ddba16bba3054cc568eed2571ea6becaabd0592b980463f157e2", - vec![196, 1, 128, 128, 128], - ), - ( - "0x1ee7e0292fba90d9733f619f976a2655c484adb30135ef0c5153b5a2f32169df", - vec![196, 1, 128, 128, 128], - ), - ( - "0x209b102e507b8dfc6acfe2cf55f4133b9209357af679a6d507e6ee87112bfe10", - vec![196, 1, 128, 128, 128], - ), - ( - "0x210ce6d692a21d75de3764b6c0356c63a51550ebec2c01f56c154c24b1cf8888", - vec![196, 1, 128, 128, 128], - ), - ( - "0x2116ab29b4cb8547af547fe472b7ce30713f234ed49cb1801ea6d3cf9c796d57", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x2290ea88cc63f09ab5e8c989a67e2e06613311801e39c84aae3badd8bb38409c", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x2369a492b6cddcc0218617a060b40df0e7dda26abe48ba4e4108c532d3f2b84f", - vec![196, 1, 128, 128, 128], - ), - ( - "0x2374954008440ca3d17b1472d34cc52a6493a94fb490d5fb427184d7d5fd1cbf", - vec![196, 1, 128, 128, 128], - ), - ( - "0x23ddaac09188c12e5d88009afa4a34041175c5531f45be53f1560a1cbfec4e8a", - vec![ - 228, 1, 128, 160, 71, 250, 72, 226, 93, 54, 105, 169, 187, 25, 12, 89, 147, - 143, 75, 228, 157, 226, 208, 131, 105, 110, 185, 57, 195, 180, 7, 46, 198, 126, - 67, 177, 128, - ], - ), - ( - "0x246cc8a2b79a30ec71390d829d0cb37cce1b953e89cb14deae4945526714a71c", - vec![196, 128, 1, 128, 128], - ), - ( - "0x255ec86eac03ba59f6dfcaa02128adbb22c561ae0c49e9e62e4fff363750626e", - vec![ - 228, 1, 128, 160, 102, 235, 22, 7, 27, 163, 121, 191, 12, 99, 47, 203, 82, 249, - 23, 90, 101, 107, 239, 98, 173, 240, 190, 245, 52, 154, 127, 90, 106, 173, 93, - 136, 128, - ], - ), - ( - "0x26ce7d83dfb0ab0e7f15c42aeb9e8c0c5dba538b07c8e64b35fb64a37267dd96", - vec![ - 228, 1, 128, 160, 36, 52, 191, 198, 67, 236, 54, 65, 22, 205, 113, 81, 154, 57, - 118, 98, 178, 12, 82, 209, 173, 207, 240, 184, 48, 232, 10, 115, 142, 25, 243, - 14, 128, - ], - ), - ( - "0x2705244734f69af78e16c74784e1dc921cb8b6a98fe76f577cc441c831e973bf", - vec![196, 1, 128, 128, 128], - ), - ( - "0x28f25652ec67d8df6a2e33730e5d0983443e3f759792a0128c06756e8eb6c37f", - vec![ - 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, - 128, 128, - ], - ), - ( - "0x2a248c1755e977920284c8054fceeb20530dc07cd8bbe876f3ce02000818cc3a", - vec![196, 1, 128, 128, 128], - ), - ( - "0x2a39afbe88f572c23c90da2d059af3de125f1da5c3753c530dc5619a4857119f", - vec![ - 228, 1, 128, 160, 130, 137, 181, 88, 134, 95, 44, 161, 245, 76, 152, 181, 255, - 93, 249, 95, 7, 194, 78, 198, 5, 226, 71, 181, 140, 119, 152, 96, 93, 205, 121, - 79, 128, - ], - ), - ( - "0x2b8d12301a8af18405b3c826b6edcc60e8e034810f00716ca48bebb84c4ce7ab", - vec![196, 1, 128, 128, 128], - ), - ( - "0x2baa718b760c0cbd0ec40a3c6df7f2948b40ba096e6e4b116b636f0cca023bde", - vec![196, 128, 1, 128, 128], - ), - ( - "0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6", - vec![ - 228, 128, 128, 128, 160, 142, 3, 136, 236, 246, 76, 250, 118, 179, 166, 175, - 21, 159, 119, 69, 21, 25, 167, 249, 187, 134, 46, 76, 206, 36, 23, 92, 121, 31, - 220, 176, 223, - ], - ), - ( - "0x2fe5767f605b7b821675b223a22e4e5055154f75e7f3041fdffaa02e4787fab8", - vec![196, 128, 1, 128, 128], - ), - ( - "0x303f57a0355c50bf1a0e1cf0fa8f9bdbc8d443b70f2ad93ac1c6b9c1d1fe29a2", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x30ce5b7591126d5464dfb4fc576a970b1368475ce097e244132b06d8cc8ccffe", - vec![196, 128, 1, 128, 128], - ), - ( - "0x315ccc15883d06b4e743f8252c999bf1ee994583ff6114d89c0f3ddee828302b", - vec![196, 1, 128, 128, 128], - ), - ( - "0x3197690074092fe51694bdb96aaab9ae94dac87f129785e498ab171a363d3b40", - vec![196, 128, 1, 128, 128], - ), - ( - "0x34a715e08b77afd68cde30b62e222542f3db90758370400c94d0563959a1d1a0", - vec![ - 228, 1, 128, 160, 79, 68, 99, 41, 181, 238, 61, 19, 212, 246, 181, 229, 242, - 16, 221, 194, 217, 15, 237, 186, 56, 75, 149, 14, 54, 161, 209, 154, 249, 92, - 92, 177, 128, - ], - ), - ( - "0x37310559ceaade42e45b3e3f05925aadca9e60aeeb9dd60d824875d9e9e71e26", - vec![ - 228, 1, 128, 160, 114, 200, 146, 33, 218, 237, 204, 221, 63, 187, 166, 108, 27, - 8, 27, 54, 52, 206, 137, 213, 160, 105, 190, 151, 255, 120, 50, 119, 143, 123, - 2, 58, 128, - ], - ), - ( - "0x37d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42", - vec![ - 248, 68, 128, 42, 160, 172, 49, 98, 168, 185, 219, 180, 49, 139, 132, 33, 159, - 49, 64, 231, 169, 236, 53, 18, 98, 52, 18, 2, 151, 221, 225, 15, 81, 178, 95, - 106, 38, 160, 245, 122, 205, 64, 37, 152, 114, 96, 109, 118, 25, 126, 240, 82, - 243, 211, 85, 136, 218, 223, 145, 158, 225, 240, 227, 203, 155, 98, 211, 244, - 176, 44, - ], - ), - ( - "0x37ddfcbcb4b2498578f90e0fcfef9965dcde4d4dfabe2f2836d2257faa169947", - vec![ - 228, 1, 128, 160, 82, 214, 210, 145, 58, 228, 75, 202, 17, 181, 161, 22, 2, 29, - 185, 124, 145, 161, 62, 56, 94, 212, 139, 160, 102, 40, 231, 66, 1, 35, 29, - 186, 128, - ], - ), - ( - "0x37e51740ad994839549a56ef8606d71ace79adc5f55c988958d1c450eea5ac2d", - vec![196, 1, 128, 128, 128], - ), - ( - "0x38152bce526b7e1c2bedfc9d297250fcead02818be7806638564377af145103b", - vec![ - 228, 1, 128, 160, 108, 0, 224, 145, 218, 227, 212, 34, 111, 172, 214, 190, 128, - 44, 134, 93, 93, 176, 245, 36, 117, 77, 34, 102, 100, 6, 19, 139, 84, 250, 176, - 230, 128, - ], - ), - ( - "0x3848b7da914222540b71e398081d04e3849d2ee0d328168a3cc173a1cd4e783b", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x389093badcaa24c3a8cbb4461f262fba44c4f178a162664087924e85f3d55710", - vec![196, 1, 128, 128, 128], - ), - ( - "0x3897cb9b6f68765022f3c74f84a9f2833132858f661f4bc91ccd7a98f4e5b1ee", - vec![196, 1, 128, 128, 128], - ), - ( - "0x395b92f75f8e06b5378a84ba03379f025d785d8b626b2b6a1c84b718244b9a91", - vec![ - 228, 1, 128, 160, 84, 70, 184, 24, 244, 198, 105, 102, 156, 211, 49, 71, 38, - 255, 19, 76, 241, 140, 88, 169, 165, 54, 223, 19, 199, 0, 97, 7, 5, 168, 183, - 200, 128, - ], - ), - ( - "0x3be526914a7d688e00adca06a0c47c580cb7aa934115ca26006a1ed5455dd2ce", - vec![196, 128, 1, 128, 128], - ), - ( - "0x3e57e37bc3f588c244ffe4da1f48a360fa540b77c92f0c76919ec4ee22b63599", - vec![196, 128, 1, 128, 128], - ), - ( - "0x415ded122ff7b6fe5862f5c443ea0375e372862b9001c5fe527d276a3a420280", - vec![196, 1, 128, 128, 128], - ), - ( - "0x419809ad1512ed1ab3fb570f98ceb2f1d1b5dea39578583cd2b03e9378bbe418", - vec![196, 1, 128, 128, 128], - ), - ( - "0x4363d332a0d4df8582a84932729892387c623fe1ec42e2cfcbe85c183ed98e0e", - vec![ - 213, 130, 1, 146, 143, 192, 151, 206, 123, 201, 7, 21, 179, 73, 233, 122, 138, - 101, 46, 31, 128, 128, - ], - ), - ( - "0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099", - vec![ - 228, 1, 1, 160, 190, 61, 117, 161, 114, 155, 225, 87, 231, 156, 59, 119, 240, - 2, 6, 219, 77, 84, 227, 234, 20, 55, 90, 1, 84, 81, 200, 142, 192, 103, 199, - 144, 128, - ], - ), - ( - "0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa", - vec![196, 1, 128, 128, 128], - ), - ( - "0x465311df0bf146d43750ed7d11b0451b5f6d5bfc69b8a216ef2f1c79c93cd848", - vec![196, 128, 1, 128, 128], - ), - ( - "0x47450e5beefbd5e3a3f80cbbac474bb3db98d5e609aa8d15485c3f0d733dea3a", - vec![ - 228, 1, 128, 160, 84, 66, 224, 39, 157, 63, 17, 73, 222, 76, 232, 217, 226, - 211, 240, 29, 24, 84, 117, 80, 56, 172, 26, 15, 174, 92, 72, 116, 155, 247, 31, - 32, 128, - ], - ), - ( - "0x482814ea8f103c39dcf6ba7e75df37145bde813964d82e81e5d7e3747b95303d", - vec![196, 128, 1, 128, 128], - ), - ( - "0x4845aac9f26fcd628b39b83d1ccb5c554450b9666b66f83aa93a1523f4db0ab6", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x48e291f8a256ab15da8401c8cae555d5417a992dff3848926fa5b71655740059", - vec![ - 228, 1, 128, 160, 162, 231, 8, 75, 169, 206, 193, 121, 81, 156, 126, 137, 80, - 198, 106, 211, 203, 168, 88, 106, 96, 207, 249, 244, 214, 12, 24, 141, 214, 33, - 82, 42, 128, - ], - ), - ( - "0x4973f6aa8cf5b1190fc95379aa01cff99570ee6b670725880217237fb49e4b24", - vec![ - 228, 1, 128, 160, 174, 46, 127, 28, 147, 60, 108, 168, 76, 232, 190, 129, 30, - 244, 17, 222, 231, 115, 251, 105, 80, 128, 86, 215, 36, 72, 4, 142, 161, 219, - 92, 71, 128, - ], - ), - ( - "0x4b238e08b80378d0815e109f350a08e5d41ec4094df2cfce7bc8b9e3115bda70", - vec![ - 228, 1, 128, 160, 17, 245, 211, 153, 202, 143, 183, 169, 175, 90, 212, 129, - 190, 96, 207, 97, 212, 84, 147, 205, 32, 32, 108, 157, 10, 35, 124, 231, 215, - 87, 30, 95, 128, - ], - ), - ( - "0x4b9f335ce0bdffdd77fdb9830961c5bc7090ae94703d0392d3f0ff10e6a4fbab", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x4bd8ef9873a5e85d4805dbcb0dbf6810e558ea175167549ef80545a9cafbb0e1", - vec![ - 228, 1, 128, 160, 161, 73, 19, 213, 72, 172, 29, 63, 153, 98, 162, 26, 86, 159, - 229, 47, 20, 54, 182, 210, 245, 234, 78, 54, 222, 19, 234, 133, 94, 222, 84, - 224, 128, - ], - ), - ( - "0x4c2765139cace1d217e238cc7ccfbb751ef200e0eae7ec244e77f37e92dfaee5", - vec![196, 1, 128, 128, 128], - ), - ( - "0x4c310e1f5d2f2e03562c4a5c473ae044b9ee19411f07097ced41e85bd99c3364", - vec![196, 128, 1, 128, 128], - ), - ( - "0x4ccd31891378d2025ef58980481608f11f5b35a988e877652e7cbb0a6127287c", - vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], - ), - ( - "0x4ceaf2371fcfb54a4d8bc1c804d90b06b3c32c9f17112b57c29b30a25cf8ca12", - vec![196, 128, 1, 128, 128], - ), - ]; - - // Create a store and load it up with the accounts - let store = Store::new("null", EngineType::InMemory).unwrap(); - let mut state_trie = store.open_direct_state_trie(*EMPTY_TRIE_HASH)?; - for (address, account) in accounts { - let hashed_address = H256::from_str(address).unwrap().as_bytes().to_vec(); - let AccountStateSlimCodec(account) = RLPDecode::decode(&account).unwrap(); - state_trie - .insert(hashed_address, account.encode_to_vec()) - .unwrap(); - } - Ok((store, state_trie.hash().unwrap())) - } -} diff --git a/crates/networking/p2p/snap/client.rs b/crates/networking/p2p/snap/client.rs new file mode 100644 index 00000000000..0757f73a434 --- /dev/null +++ b/crates/networking/p2p/snap/client.rs @@ -0,0 +1,1401 @@ +//! Snap sync client - functions for requesting snap protocol data from peers +//! +//! This module contains all the client-side snap protocol request functions. + +use crate::rlpx::message::Message as RLPxMessage; +use crate::{ + metrics::{CurrentStepValue, METRICS}, + peer_handler::PeerHandler, + peer_table::PeerTable, + rlpx::{ + connection::server::PeerConnection, + error::PeerConnectionError, + p2p::SUPPORTED_SNAP_CAPABILITIES, + snap::{ + AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, + GetStorageRanges, GetTrieNodes, StorageRanges, TrieNodes, + }, + }, + snap::{constants::*, encodable_to_proof, error::SnapError}, + sync::{AccountStorageRoots, SnapBlockSyncState, block_is_stale, update_pivot}, + utils::{ + AccountsWithStorage, dump_accounts_to_file, dump_storages_to_file, + get_account_state_snapshot_file, get_account_storages_snapshot_file, + }, +}; +use bytes::Bytes; +use ethrex_common::{ + BigEndianHash, H256, U256, + types::{AccountState, BlockHeader}, +}; +use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; +use ethrex_storage::Store; +use ethrex_trie::Nibbles; +use ethrex_trie::{Node, verify_range}; +use std::{ + collections::{BTreeMap, HashMap, VecDeque}, + path::Path, + sync::atomic::Ordering, + time::{Duration, SystemTime}, +}; +use tracing::{debug, error, info, trace, warn}; + +// Re-export DumpError from error module +pub use super::error::DumpError; + +/// Metadata for requesting trie nodes +#[derive(Debug, Clone)] +pub struct RequestMetadata { + pub hash: H256, + pub path: Nibbles, + /// What node is the parent of this node + pub parent_path: Nibbles, +} + +/// Error type for storage trie node requests (includes request ID for tracking) +#[derive(Debug, thiserror::Error)] +#[error("Storage trie node request {request_id} failed: {source}")] +pub struct RequestStorageTrieNodesError { + pub request_id: u64, + #[source] + pub source: SnapError, +} + +#[derive(Clone)] +struct StorageTaskResult { + start_index: usize, + account_storages: Vec>, + peer_id: H256, + remaining_start: usize, + remaining_end: usize, + remaining_hash_range: (H256, Option), +} + +#[derive(Debug)] +struct StorageTask { + start_index: usize, + end_index: usize, + start_hash: H256, + // end_hash is None if the task is for the first big storage request + end_hash: Option, +} + +/// Requests an account range from any suitable peer given the state trie's root and the starting hash and the limit hash. +/// Will also return a boolean indicating if there is more state to be fetched towards the right of the trie +/// (Note that the boolean will be true even if the remaining state is ouside the boundary set by the limit hash) +/// +/// # Returns +/// +/// The account range or `None` if: +/// +/// - There are no available peers (the node just started up or was rejected by all other nodes) +/// - No peer returned a valid response in the given time and retry limits +pub async fn request_account_range( + peers: &mut PeerHandler, + start: H256, + limit: H256, + account_state_snapshots_dir: &Path, + pivot_header: &mut BlockHeader, + block_sync_state: &mut SnapBlockSyncState, +) -> Result<(), SnapError> { + METRICS + .current_step + .set(CurrentStepValue::RequestingAccountRanges); + // 1) split the range in chunks of same length + let start_u256 = U256::from_big_endian(&start.0); + let limit_u256 = U256::from_big_endian(&limit.0); + + let range = limit_u256 - start_u256; + let chunk_count = U256::from(ACCOUNT_RANGE_CHUNK_COUNT) + .min(range.max(U256::one())) + .as_usize(); + let chunk_size = range / chunk_count; + + // list of tasks to be executed + let mut tasks_queue_not_started = VecDeque::<(H256, H256)>::new(); + for i in 0..(chunk_count as u64) { + let chunk_start_u256 = chunk_size * i + start_u256; + // We subtract one because ranges are inclusive + let chunk_end_u256 = chunk_start_u256 + chunk_size - 1u64; + let chunk_start = H256::from_uint(&(chunk_start_u256)); + let chunk_end = H256::from_uint(&(chunk_end_u256)); + tasks_queue_not_started.push_back((chunk_start, chunk_end)); + } + // Modify the last chunk to include the limit + let last_task = tasks_queue_not_started + .back_mut() + .ok_or(SnapError::NoTasks)?; + last_task.1 = limit; + + // 2) request the chunks from peers + + let mut downloaded_count = 0_u64; + let mut all_account_hashes = Vec::new(); + let mut all_accounts_state = Vec::new(); + + // channel to send the tasks to the peers + let (task_sender, mut task_receiver) = + tokio::sync::mpsc::channel::<(Vec, H256, Option<(H256, H256)>)>(1000); + + info!("Starting to download account ranges from peers"); + + *METRICS.account_tries_download_start_time.lock().await = Some(SystemTime::now()); + + let mut completed_tasks = 0; + let mut chunk_file = 0; + let mut last_update: SystemTime = SystemTime::now(); + let mut write_set = tokio::task::JoinSet::new(); + + let mut logged_no_free_peers_count = 0; + + loop { + if all_accounts_state.len() * size_of::() >= RANGE_FILE_CHUNK_SIZE { + let current_account_hashes = std::mem::take(&mut all_account_hashes); + let current_account_states = std::mem::take(&mut all_accounts_state); + + let account_state_chunk = current_account_hashes + .into_iter() + .zip(current_account_states) + .collect::>(); + + if !std::fs::exists(account_state_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("State snapshots directory does not exist".to_string()) + })? { + std::fs::create_dir_all(account_state_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("Failed to create state snapshots directory".to_string()) + })?; + } + + let account_state_snapshots_dir_cloned = account_state_snapshots_dir.to_path_buf(); + write_set.spawn(async move { + let path = get_account_state_snapshot_file( + &account_state_snapshots_dir_cloned, + chunk_file, + ); + // TODO: check the error type and handle it properly + dump_accounts_to_file(&path, account_state_chunk) + }); + + chunk_file += 1; + } + + if last_update + .elapsed() + .expect("Time shouldn't be in the past") + >= Duration::from_secs(1) + { + METRICS + .downloaded_account_tries + .store(downloaded_count, Ordering::Relaxed); + last_update = SystemTime::now(); + } + + if let Ok((accounts, peer_id, chunk_start_end)) = task_receiver.try_recv() { + if let Some((chunk_start, chunk_end)) = chunk_start_end { + if chunk_start <= chunk_end { + tasks_queue_not_started.push_back((chunk_start, chunk_end)); + } else { + completed_tasks += 1; + } + } + if chunk_start_end.is_none() { + completed_tasks += 1; + } + if accounts.is_empty() { + peers.peer_table.record_failure(&peer_id).await?; + continue; + } + peers.peer_table.record_success(&peer_id).await?; + + downloaded_count += accounts.len() as u64; + + debug!( + "Downloaded {} accounts from peer {} (current count: {downloaded_count})", + accounts.len(), + peer_id + ); + all_account_hashes.extend(accounts.iter().map(|unit| unit.hash)); + all_accounts_state.extend(accounts.iter().map(|unit| unit.account)); + } + + let Some((peer_id, connection)) = peers + .peer_table + .get_best_peer(&SUPPORTED_SNAP_CAPABILITIES) + .await + .inspect_err(|err| warn!(%err, "Error requesting a peer for account range")) + .unwrap_or(None) + else { + // Log ~ once every 10 seconds + if logged_no_free_peers_count == 0 { + trace!("We are missing peers in request_account_range"); + logged_no_free_peers_count = 1000; + } + logged_no_free_peers_count -= 1; + // Sleep a bit to avoid busy polling + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + }; + + let Some((chunk_start, chunk_end)) = tasks_queue_not_started.pop_front() else { + if completed_tasks >= chunk_count { + info!("All account ranges downloaded successfully"); + break; + } + continue; + }; + + let tx = task_sender.clone(); + + if block_is_stale(pivot_header) { + info!("request_account_range became stale, updating pivot"); + *pivot_header = update_pivot( + pivot_header.number, + pivot_header.timestamp, + peers, + block_sync_state, + ) + .await + .expect("Should be able to update pivot") + } + + let peer_table = peers.peer_table.clone(); + + tokio::spawn(request_account_range_worker( + peer_id, + connection, + peer_table, + chunk_start, + chunk_end, + pivot_header.state_root, + tx, + )); + } + + write_set + .join_all() + .await + .into_iter() + .collect::, DumpError>>() + .map_err(SnapError::from)?; + + // TODO: This is repeated code, consider refactoring + { + let current_account_hashes = std::mem::take(&mut all_account_hashes); + let current_account_states = std::mem::take(&mut all_accounts_state); + + let account_state_chunk = current_account_hashes + .into_iter() + .zip(current_account_states) + .collect::>(); + + if !std::fs::exists(account_state_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("State snapshots directory does not exist".to_string()) + })? { + std::fs::create_dir_all(account_state_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("Failed to create state snapshots directory".to_string()) + })?; + } + + let path = get_account_state_snapshot_file(account_state_snapshots_dir, chunk_file); + dump_accounts_to_file(&path, account_state_chunk) + .inspect_err(|err| { + error!( + "We had an error dumping the last accounts to disk {}", + err.error + ) + }) + .map_err(|_| { + SnapError::SnapshotDir(format!( + "Failed to write state snapshot chunk {}", + chunk_file + )) + })?; + } + + METRICS + .downloaded_account_tries + .store(downloaded_count, Ordering::Relaxed); + *METRICS.account_tries_download_end_time.lock().await = Some(SystemTime::now()); + + Ok(()) +} + +/// Requests bytecodes for the given code hashes +/// Returns the bytecodes or None if: +/// - There are no available peers (the node just started up or was rejected by all other nodes) +/// - No peer returned a valid response in the given time and retry limits +pub async fn request_bytecodes( + peers: &mut PeerHandler, + all_bytecode_hashes: &[H256], +) -> Result>, SnapError> { + METRICS + .current_step + .set(CurrentStepValue::RequestingBytecodes); + if all_bytecode_hashes.is_empty() { + return Ok(Some(Vec::new())); + } + const MAX_BYTECODES_REQUEST_SIZE: usize = 100; + // 1) split the range in chunks of same length + let chunk_count = 800; + let chunk_count = chunk_count.min(all_bytecode_hashes.len()); + let chunk_size = all_bytecode_hashes.len() / chunk_count; + + // list of tasks to be executed + // Types are (start_index, end_index, starting_hash) + // NOTE: end_index is NOT inclusive + let mut tasks_queue_not_started = VecDeque::<(usize, usize)>::new(); + for i in 0..chunk_count { + let chunk_start = chunk_size * i; + let chunk_end = chunk_start + chunk_size; + tasks_queue_not_started.push_back((chunk_start, chunk_end)); + } + // Modify the last chunk to include the limit + let last_task = tasks_queue_not_started + .back_mut() + .ok_or(SnapError::NoTasks)?; + last_task.1 = all_bytecode_hashes.len(); + + // 2) request the chunks from peers + let mut downloaded_count = 0_u64; + let mut all_bytecodes = vec![Bytes::new(); all_bytecode_hashes.len()]; + + // channel to send the tasks to the peers + struct TaskResult { + start_index: usize, + bytecodes: Vec, + peer_id: H256, + remaining_start: usize, + remaining_end: usize, + } + let (task_sender, mut task_receiver) = tokio::sync::mpsc::channel::(1000); + + info!("Starting to download bytecodes from peers"); + + METRICS + .bytecodes_to_download + .fetch_add(all_bytecode_hashes.len() as u64, Ordering::Relaxed); + + let mut completed_tasks = 0; + + let mut logged_no_free_peers_count = 0; + + loop { + if let Ok(result) = task_receiver.try_recv() { + let TaskResult { + start_index, + bytecodes, + peer_id, + remaining_start, + remaining_end, + } = result; + + debug!( + "Downloaded {} bytecodes from peer {peer_id} (current count: {downloaded_count})", + bytecodes.len(), + ); + + if remaining_start < remaining_end { + tasks_queue_not_started.push_back((remaining_start, remaining_end)); + } else { + completed_tasks += 1; + } + if bytecodes.is_empty() { + peers.peer_table.record_failure(&peer_id).await?; + continue; + } + + downloaded_count += bytecodes.len() as u64; + + peers.peer_table.record_success(&peer_id).await?; + for (i, bytecode) in bytecodes.into_iter().enumerate() { + all_bytecodes[start_index + i] = bytecode; + } + } + + let Some((peer_id, mut connection)) = peers + .peer_table + .get_best_peer(&SUPPORTED_SNAP_CAPABILITIES) + .await + .inspect_err(|err| warn!(%err, "Error requesting a peer for bytecodes")) + .unwrap_or(None) + else { + // Log ~ once every 10 seconds + if logged_no_free_peers_count == 0 { + trace!("We are missing peers in request_bytecodes"); + logged_no_free_peers_count = 1000; + } + logged_no_free_peers_count -= 1; + // Sleep a bit to avoid busy polling + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + }; + + let Some((chunk_start, chunk_end)) = tasks_queue_not_started.pop_front() else { + if completed_tasks >= chunk_count { + info!("All bytecodes downloaded successfully"); + break; + } + continue; + }; + + let tx = task_sender.clone(); + + let hashes_to_request: Vec<_> = all_bytecode_hashes + .iter() + .skip(chunk_start) + .take((chunk_end - chunk_start).min(MAX_BYTECODES_REQUEST_SIZE)) + .copied() + .collect(); + + let mut peer_table = peers.peer_table.clone(); + + tokio::spawn(async move { + let empty_task_result = TaskResult { + start_index: chunk_start, + bytecodes: vec![], + peer_id, + remaining_start: chunk_start, + remaining_end: chunk_end, + }; + debug!( + "Requesting bytecode from peer {peer_id}, chunk: {chunk_start:?} - {chunk_end:?}" + ); + let request_id = rand::random(); + let request = RLPxMessage::GetByteCodes(GetByteCodes { + id: request_id, + hashes: hashes_to_request.clone(), + bytes: MAX_RESPONSE_BYTES, + }); + if let Ok(RLPxMessage::ByteCodes(ByteCodes { id: _, codes })) = + PeerHandler::make_request( + &mut peer_table, + peer_id, + &mut connection, + request, + PEER_REPLY_TIMEOUT, + ) + .await + { + if codes.is_empty() { + tx.send(empty_task_result).await.ok(); + // Too spammy + // tracing::error!("Received empty account range"); + return; + } + // Validate response by hashing bytecodes + let validated_codes: Vec = codes + .into_iter() + .zip(hashes_to_request) + .take_while(|(b, hash)| ethrex_common::utils::keccak(b) == *hash) + .map(|(b, _hash)| b) + .collect(); + let result = TaskResult { + start_index: chunk_start, + remaining_start: chunk_start + validated_codes.len(), + bytecodes: validated_codes, + peer_id, + remaining_end: chunk_end, + }; + tx.send(result).await.ok(); + } else { + tracing::debug!("Failed to get bytecode"); + tx.send(empty_task_result).await.ok(); + } + }); + } + + METRICS + .downloaded_bytecodes + .fetch_add(downloaded_count, Ordering::Relaxed); + info!( + "Finished downloading bytecodes, total bytecodes: {}", + all_bytecode_hashes.len() + ); + + Ok(Some(all_bytecodes)) +} + +/// Requests storage ranges for accounts given their hashed address and storage roots, and the root of their state trie +/// account_hashes & storage_roots must have the same length +/// storage_roots must not contain empty trie hashes, we will treat empty ranges as invalid responses +/// Returns true if the last account's storage was not completely fetched by the request +/// Returns the list of hashed storage keys and values for each account's storage or None if: +/// - There are no available peers (the node just started up or was rejected by all other nodes) +/// - No peer returned a valid response in the given time and retry limits +pub async fn request_storage_ranges( + peers: &mut PeerHandler, + account_storage_roots: &mut AccountStorageRoots, + account_storages_snapshots_dir: &Path, + mut chunk_index: u64, + pivot_header: &mut BlockHeader, + store: Store, +) -> Result { + METRICS + .current_step + .set(CurrentStepValue::RequestingStorageRanges); + debug!("Starting request_storage_ranges function"); + // 1) split the range in chunks of same length + let mut accounts_by_root_hash: BTreeMap<_, Vec<_>> = BTreeMap::new(); + for (account, (maybe_root_hash, _)) in &account_storage_roots.accounts_with_storage_root { + match maybe_root_hash { + Some(root) => { + accounts_by_root_hash + .entry(*root) + .or_default() + .push(*account); + } + None => { + let root = store + .get_account_state_by_acc_hash(pivot_header.hash(), *account)? + .ok_or_else(|| { + SnapError::InternalError( + "Could not find account that should have been downloaded or healed" + .to_string(), + ) + })? + .storage_root; + accounts_by_root_hash + .entry(root) + .or_default() + .push(*account); + } + } + } + let mut accounts_by_root_hash = Vec::from_iter(accounts_by_root_hash); + // TODO: Turn this into a stable sort for binary search. + accounts_by_root_hash.sort_unstable_by_key(|(_, accounts)| !accounts.len()); + let chunk_size = STORAGE_BATCH_SIZE; + let chunk_count = (accounts_by_root_hash.len() / chunk_size) + 1; + + // list of tasks to be executed + // Types are (start_index, end_index, starting_hash) + // NOTE: end_index is NOT inclusive + + let mut tasks_queue_not_started = VecDeque::::new(); + for i in 0..chunk_count { + let chunk_start = chunk_size * i; + let chunk_end = (chunk_start + chunk_size).min(accounts_by_root_hash.len()); + tasks_queue_not_started.push_back(StorageTask { + start_index: chunk_start, + end_index: chunk_end, + start_hash: H256::zero(), + end_hash: None, + }); + } + + // channel to send the tasks to the peers + let (task_sender, mut task_receiver) = tokio::sync::mpsc::channel::(1000); + + // channel to send the result of dumping storages + let mut disk_joinset: tokio::task::JoinSet> = tokio::task::JoinSet::new(); + + let mut task_count = tasks_queue_not_started.len(); + let mut completed_tasks = 0; + + // TODO: in a refactor, delete this replace with a structure that can handle removes + let mut accounts_done: HashMap> = HashMap::new(); + // Maps storage root to vector of hashed addresses matching that root and + // vector of hashed storage keys and storage values. + let mut current_account_storages: BTreeMap = BTreeMap::new(); + + let mut logged_no_free_peers_count = 0; + + debug!("Starting request_storage_ranges loop"); + loop { + if current_account_storages + .values() + .map(|accounts| 32 * accounts.accounts.len() + 64 * accounts.storages.len()) + .sum::() + > RANGE_FILE_CHUNK_SIZE + { + let current_account_storages = std::mem::take(&mut current_account_storages); + let snapshot = current_account_storages.into_values().collect::>(); + + if !std::fs::exists(account_storages_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("Storage snapshots directory does not exist".to_string()) + })? { + std::fs::create_dir_all(account_storages_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir( + "Failed to create storage snapshots directory".to_string(), + ) + })?; + } + let account_storages_snapshots_dir_cloned = + account_storages_snapshots_dir.to_path_buf(); + if !disk_joinset.is_empty() { + debug!("Writing to disk"); + disk_joinset + .join_next() + .await + .expect("Shouldn't be empty") + .expect("Shouldn't have a join error") + .inspect_err(|err| error!("We found this error while dumping to file {err:?}")) + .map_err(SnapError::from)?; + } + disk_joinset.spawn(async move { + let path = get_account_storages_snapshot_file( + &account_storages_snapshots_dir_cloned, + chunk_index, + ); + dump_storages_to_file(&path, snapshot) + }); + + chunk_index += 1; + } + + if let Ok(result) = task_receiver.try_recv() { + let StorageTaskResult { + start_index, + mut account_storages, + peer_id, + remaining_start, + remaining_end, + remaining_hash_range: (hash_start, hash_end), + } = result; + completed_tasks += 1; + + for (_, accounts) in accounts_by_root_hash[start_index..remaining_start].iter() { + for account in accounts { + if !accounts_done.contains_key(account) { + let (_, old_intervals) = account_storage_roots + .accounts_with_storage_root + .get_mut(account) + .ok_or(SnapError::InternalError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; + + if old_intervals.is_empty() { + accounts_done.insert(*account, vec![]); + } + } + } + } + + if remaining_start < remaining_end { + debug!("Failed to download entire chunk from peer {peer_id}"); + if hash_start.is_zero() { + // Task is common storage range request + let task = StorageTask { + start_index: remaining_start, + end_index: remaining_end, + start_hash: H256::zero(), + end_hash: None, + }; + tasks_queue_not_started.push_back(task); + task_count += 1; + } else if let Some(hash_end) = hash_end { + // Task was a big storage account result + if hash_start <= hash_end { + let task = StorageTask { + start_index: remaining_start, + end_index: remaining_end, + start_hash: hash_start, + end_hash: Some(hash_end), + }; + tasks_queue_not_started.push_back(task); + task_count += 1; + + let acc_hash = *accounts_by_root_hash[remaining_start] + .1 + .first() + .ok_or(SnapError::InternalError("Empty accounts vector".to_owned()))?; + let (_, old_intervals) = account_storage_roots + .accounts_with_storage_root + .get_mut(&acc_hash).ok_or(SnapError::InternalError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; + for (old_start, end) in old_intervals { + if end == &hash_end { + *old_start = hash_start; + } + } + account_storage_roots + .healed_accounts + .extend(accounts_by_root_hash[start_index].1.iter().copied()); + } else { + let mut acc_hash: H256 = H256::zero(); + // This search could potentially be expensive, but it's something that should happen very + // infrequently (only when we encounter an account we think it's big but it's not). In + // normal cases the vec we are iterating over just has one element (the big account). + for account in accounts_by_root_hash[remaining_start].1.iter() { + if let Some((_, old_intervals)) = account_storage_roots + .accounts_with_storage_root + .get(account) + { + if !old_intervals.is_empty() { + acc_hash = *account; + } + } else { + continue; + } + } + if acc_hash.is_zero() { + panic!("Should have found the account hash"); + } + let (_, old_intervals) = account_storage_roots + .accounts_with_storage_root + .get_mut(&acc_hash) + .ok_or(SnapError::InternalError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; + old_intervals.remove( + old_intervals + .iter() + .position(|(_old_start, end)| end == &hash_end) + .ok_or(SnapError::InternalError( + "Could not find an old interval that we were tracking" + .to_owned(), + ))?, + ); + if old_intervals.is_empty() { + for account in accounts_by_root_hash[remaining_start].1.iter() { + accounts_done.insert(*account, vec![]); + account_storage_roots.healed_accounts.insert(*account); + } + } + } + } else { + if remaining_start + 1 < remaining_end { + let task = StorageTask { + start_index: remaining_start + 1, + end_index: remaining_end, + start_hash: H256::zero(), + end_hash: None, + }; + tasks_queue_not_started.push_back(task); + task_count += 1; + } + // Task found a big storage account, so we split the chunk into multiple chunks + let start_hash_u256 = U256::from_big_endian(&hash_start.0); + let missing_storage_range = U256::MAX - start_hash_u256; + + // Big accounts need to be marked for storage healing unconditionally + for account in accounts_by_root_hash[remaining_start].1.iter() { + account_storage_roots.healed_accounts.insert(*account); + } + + let slot_count = account_storages + .last() + .map(|v| v.len()) + .ok_or(SnapError::NoAccountStorages)? + .max(1); + let storage_density = start_hash_u256 / slot_count; + + let slots_per_chunk = U256::from(10000); + let chunk_size = storage_density + .checked_mul(slots_per_chunk) + .unwrap_or(U256::MAX); + + let chunk_count = (missing_storage_range / chunk_size).as_usize().max(1); + + let first_acc_hash = *accounts_by_root_hash[remaining_start] + .1 + .first() + .ok_or(SnapError::InternalError("Empty accounts vector".to_owned()))?; + + let maybe_old_intervals = account_storage_roots + .accounts_with_storage_root + .get(&first_acc_hash); + + if let Some((_, old_intervals)) = maybe_old_intervals { + if !old_intervals.is_empty() { + for (start_hash, end_hash) in old_intervals { + let task = StorageTask { + start_index: remaining_start, + end_index: remaining_start + 1, + start_hash: *start_hash, + end_hash: Some(*end_hash), + }; + + tasks_queue_not_started.push_back(task); + task_count += 1; + } + } else { + // TODO: DRY + account_storage_roots + .accounts_with_storage_root + .insert(first_acc_hash, (None, vec![])); + let (_, intervals) = account_storage_roots + .accounts_with_storage_root + .get_mut(&first_acc_hash) + .ok_or(SnapError::InternalError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; + + for i in 0..chunk_count { + let start_hash_u256 = start_hash_u256 + chunk_size * i; + let start_hash = H256::from_uint(&start_hash_u256); + let end_hash = if i == chunk_count - 1 { + HASH_MAX + } else { + let end_hash_u256 = start_hash_u256 + .checked_add(chunk_size) + .unwrap_or(U256::MAX); + H256::from_uint(&end_hash_u256) + }; + + let task = StorageTask { + start_index: remaining_start, + end_index: remaining_start + 1, + start_hash, + end_hash: Some(end_hash), + }; + + intervals.push((start_hash, end_hash)); + + tasks_queue_not_started.push_back(task); + task_count += 1; + } + debug!("Split big storage account into {chunk_count} chunks."); + } + } else { + account_storage_roots + .accounts_with_storage_root + .insert(first_acc_hash, (None, vec![])); + let (_, intervals) = account_storage_roots + .accounts_with_storage_root + .get_mut(&first_acc_hash) + .ok_or(SnapError::InternalError("Tried to get the old download intervals for an account but did not find them".to_owned()))?; + + for i in 0..chunk_count { + let start_hash_u256 = start_hash_u256 + chunk_size * i; + let start_hash = H256::from_uint(&start_hash_u256); + let end_hash = if i == chunk_count - 1 { + HASH_MAX + } else { + let end_hash_u256 = + start_hash_u256.checked_add(chunk_size).unwrap_or(U256::MAX); + H256::from_uint(&end_hash_u256) + }; + + let task = StorageTask { + start_index: remaining_start, + end_index: remaining_start + 1, + start_hash, + end_hash: Some(end_hash), + }; + + intervals.push((start_hash, end_hash)); + + tasks_queue_not_started.push_back(task); + task_count += 1; + } + debug!("Split big storage account into {chunk_count} chunks."); + } + } + } + + if account_storages.is_empty() { + peers.peer_table.record_failure(&peer_id).await?; + continue; + } + if let Some(hash_end) = hash_end { + // This is a big storage account, and the range might be empty + if account_storages[0].len() == 1 && account_storages[0][0].0 > hash_end { + continue; + } + } + + peers.peer_table.record_success(&peer_id).await?; + + let n_storages = account_storages.len(); + let n_slots = account_storages + .iter() + .map(|storage| storage.len()) + .sum::(); + + // These take into account we downloaded the same thing for different accounts + let effective_slots: usize = account_storages + .iter() + .enumerate() + .map(|(i, storages)| { + accounts_by_root_hash[start_index + i].1.len() * storages.len() + }) + .sum(); + + METRICS + .storage_leaves_downloaded + .inc_by(effective_slots as u64); + + debug!("Downloaded {n_storages} storages ({n_slots} slots) from peer {peer_id}"); + debug!( + "Total tasks: {task_count}, completed tasks: {completed_tasks}, queued tasks: {}", + tasks_queue_not_started.len() + ); + // THEN: update insert to read with the correct structure and reuse + // tries, only changing the prefix for insertion. + if account_storages.len() == 1 { + let (root_hash, accounts) = &accounts_by_root_hash[start_index]; + // We downloaded a big storage account + current_account_storages + .entry(*root_hash) + .or_insert_with(|| AccountsWithStorage { + accounts: accounts.clone(), + storages: Vec::new(), + }) + .storages + .extend(account_storages.remove(0)); + } else { + for (i, storages) in account_storages.into_iter().enumerate() { + let (root_hash, accounts) = &accounts_by_root_hash[start_index + i]; + current_account_storages.insert( + *root_hash, + AccountsWithStorage { + accounts: accounts.clone(), + storages, + }, + ); + } + } + } + + if block_is_stale(pivot_header) { + info!("request_storage_ranges became stale, breaking"); + break; + } + + let Some((peer_id, connection)) = peers + .peer_table + .get_best_peer(&SUPPORTED_SNAP_CAPABILITIES) + .await + .inspect_err(|err| warn!(%err, "Error requesting a peer for storage ranges")) + .unwrap_or(None) + else { + // Log ~ once every 10 seconds + if logged_no_free_peers_count == 0 { + trace!("We are missing peers in request_storage_ranges"); + logged_no_free_peers_count = 1000; + } + logged_no_free_peers_count -= 1; + // Sleep a bit to avoid busy polling + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + }; + + let Some(task) = tasks_queue_not_started.pop_front() else { + if completed_tasks >= task_count { + break; + } + continue; + }; + + let tx = task_sender.clone(); + + // FIXME: this unzip is probably pointless and takes up unnecessary memory. + let (chunk_account_hashes, chunk_storage_roots): (Vec<_>, Vec<_>) = accounts_by_root_hash + [task.start_index..task.end_index] + .iter() + .map(|(root, storages)| (*storages.first().unwrap_or(&H256::zero()), *root)) + .unzip(); + + if task_count - completed_tasks < 30 { + debug!( + "Assigning task: {task:?}, account_hash: {}, storage_root: {}", + chunk_account_hashes.first().unwrap_or(&H256::zero()), + chunk_storage_roots.first().unwrap_or(&H256::zero()), + ); + } + let peer_table = peers.peer_table.clone(); + + tokio::spawn(request_storage_ranges_worker( + task, + peer_id, + connection, + peer_table, + pivot_header.state_root, + chunk_account_hashes, + chunk_storage_roots, + tx, + )); + } + + { + let snapshot = current_account_storages.into_values().collect::>(); + + if !std::fs::exists(account_storages_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("Storage snapshots directory does not exist".to_string()) + })? { + std::fs::create_dir_all(account_storages_snapshots_dir).map_err(|_| { + SnapError::SnapshotDir("Failed to create storage snapshots directory".to_string()) + })?; + } + let path = get_account_storages_snapshot_file(account_storages_snapshots_dir, chunk_index); + dump_storages_to_file(&path, snapshot).map_err(|_| { + SnapError::SnapshotDir(format!( + "Failed to write storage snapshot chunk {}", + chunk_index + )) + })?; + } + disk_joinset + .join_all() + .await + .into_iter() + .map(|result| { + result.inspect_err(|err| error!("We found this error while dumping to file {err:?}")) + }) + .collect::, DumpError>>() + .map_err(SnapError::from)?; + + for (account_done, intervals) in accounts_done { + if intervals.is_empty() { + account_storage_roots + .accounts_with_storage_root + .remove(&account_done); + } + } + + // Dropping the task sender so that the recv returns None + drop(task_sender); + + Ok(chunk_index + 1) +} + +pub async fn request_state_trienodes( + peer_id: H256, + mut connection: PeerConnection, + mut peer_table: PeerTable, + state_root: H256, + paths: Vec, +) -> Result, SnapError> { + let expected_nodes = paths.len(); + // Keep track of peers we requested from so we can penalize unresponsive peers when we get a response + // This is so we avoid penalizing peers due to requesting stale data + + let request_id = rand::random(); + let request = RLPxMessage::GetTrieNodes(GetTrieNodes { + id: request_id, + root_hash: state_root, + // [acc_path, acc_path,...] -> [[acc_path], [acc_path]] + paths: paths + .iter() + .map(|vec| vec![Bytes::from(vec.path.encode_compact())]) + .collect(), + bytes: MAX_RESPONSE_BYTES, + }); + let nodes = match PeerHandler::make_request( + &mut peer_table, + peer_id, + &mut connection, + request, + PEER_REPLY_TIMEOUT, + ) + .await + { + Ok(RLPxMessage::TrieNodes(trie_nodes)) => trie_nodes + .nodes + .iter() + .map(|node| Node::decode(node)) + .collect::, _>>() + .map_err(SnapError::from), + Ok(other_msg) => Err(SnapError::Protocol( + PeerConnectionError::UnexpectedResponse("TrieNodes".to_string(), other_msg.to_string()), + )), + Err(other_err) => Err(SnapError::Protocol(other_err)), + }?; + + if nodes.is_empty() || nodes.len() > expected_nodes { + return Err(SnapError::InvalidData); + } + + for (index, node) in nodes.iter().enumerate() { + if node.compute_hash().finalize() != paths[index].hash { + error!( + "A peer is sending wrong data for the state trie node {:?}", + paths[index].path + ); + return Err(SnapError::InvalidHash); + } + } + + Ok(nodes) +} + +/// Requests storage trie nodes given the root of the state trie where they are contained and +/// a hashmap mapping the path to the account in the state trie (aka hashed address) to the paths to the nodes in its storage trie (can be full or partial) +/// Returns the nodes or None if: +/// - There are no available peers (the node just started up or was rejected by all other nodes) +/// - No peer returned a valid response in the given time and retry limits +pub async fn request_storage_trienodes( + peer_id: H256, + mut connection: PeerConnection, + mut peer_table: PeerTable, + get_trie_nodes: GetTrieNodes, +) -> Result { + // Keep track of peers we requested from so we can penalize unresponsive peers when we get a response + // This is so we avoid penalizing peers due to requesting stale data + let request_id = get_trie_nodes.id; + let request = RLPxMessage::GetTrieNodes(get_trie_nodes); + match PeerHandler::make_request( + &mut peer_table, + peer_id, + &mut connection, + request, + PEER_REPLY_TIMEOUT, + ) + .await + { + Ok(RLPxMessage::TrieNodes(trie_nodes)) => Ok(trie_nodes), + Ok(other_msg) => Err(RequestStorageTrieNodesError { + request_id, + source: SnapError::Protocol(PeerConnectionError::UnexpectedResponse( + "TrieNodes".to_string(), + other_msg.to_string(), + )), + }), + Err(e) => Err(RequestStorageTrieNodesError { + request_id, + source: SnapError::Protocol(e), + }), + } +} + +#[allow(clippy::type_complexity)] +async fn request_account_range_worker( + peer_id: H256, + mut connection: PeerConnection, + mut peer_table: PeerTable, + chunk_start: H256, + chunk_end: H256, + state_root: H256, + tx: tokio::sync::mpsc::Sender<(Vec, H256, Option<(H256, H256)>)>, +) -> Result<(), SnapError> { + debug!("Requesting account range from peer {peer_id}, chunk: {chunk_start:?} - {chunk_end:?}"); + let request_id = rand::random(); + let request = RLPxMessage::GetAccountRange(GetAccountRange { + id: request_id, + root_hash: state_root, + starting_hash: chunk_start, + limit_hash: chunk_end, + response_bytes: MAX_RESPONSE_BYTES, + }); + if let Ok(RLPxMessage::AccountRange(AccountRange { + id: _, + accounts, + proof, + })) = PeerHandler::make_request( + &mut peer_table, + peer_id, + &mut connection, + request, + PEER_REPLY_TIMEOUT, + ) + .await + { + if accounts.is_empty() { + tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) + .await + .ok(); + return Ok(()); + } + // Unzip & validate response + let proof = encodable_to_proof(&proof); + let (account_hashes, account_states): (Vec<_>, Vec<_>) = accounts + .clone() + .into_iter() + .map(|unit| (unit.hash, unit.account)) + .unzip(); + let encoded_accounts = account_states + .iter() + .map(|acc| acc.encode_to_vec()) + .collect::>(); + + let Ok(should_continue) = verify_range( + state_root, + &chunk_start, + &account_hashes, + &encoded_accounts, + &proof, + ) else { + tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) + .await + .ok(); + tracing::error!("Received invalid account range"); + return Ok(()); + }; + + // If the range has more accounts to fetch, we send the new chunk + let chunk_left = if should_continue { + let last_hash = match account_hashes.last() { + Some(last_hash) => last_hash, + None => { + tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) + .await + .ok(); + error!("Account hashes last failed, this shouldn't happen"); + return Err(SnapError::NoAccountHashes); + } + }; + let new_start_u256 = U256::from_big_endian(&last_hash.0) + 1; + let new_start = H256::from_uint(&new_start_u256); + Some((new_start, chunk_end)) + } else { + None + }; + tx.send(( + accounts + .into_iter() + .filter(|unit| unit.hash <= chunk_end) + .collect(), + peer_id, + chunk_left, + )) + .await + .ok(); + } else { + tracing::debug!("Failed to get account range"); + tx.send((Vec::new(), peer_id, Some((chunk_start, chunk_end)))) + .await + .ok(); + } + Ok::<(), SnapError>(()) +} + +#[allow(clippy::too_many_arguments)] +async fn request_storage_ranges_worker( + task: StorageTask, + peer_id: H256, + mut connection: PeerConnection, + mut peer_table: PeerTable, + state_root: H256, + chunk_account_hashes: Vec, + chunk_storage_roots: Vec, + tx: tokio::sync::mpsc::Sender, +) -> Result<(), SnapError> { + let start = task.start_index; + let end = task.end_index; + let start_hash = task.start_hash; + + let empty_task_result = StorageTaskResult { + start_index: task.start_index, + account_storages: Vec::new(), + peer_id, + remaining_start: task.start_index, + remaining_end: task.end_index, + remaining_hash_range: (start_hash, task.end_hash), + }; + let request_id = rand::random(); + let request = RLPxMessage::GetStorageRanges(GetStorageRanges { + id: request_id, + root_hash: state_root, + account_hashes: chunk_account_hashes, + starting_hash: start_hash, + limit_hash: task.end_hash.unwrap_or(HASH_MAX), + response_bytes: MAX_RESPONSE_BYTES, + }); + let Ok(RLPxMessage::StorageRanges(StorageRanges { + id: _, + slots, + proof, + })) = PeerHandler::make_request( + &mut peer_table, + peer_id, + &mut connection, + request, + PEER_REPLY_TIMEOUT, + ) + .await + else { + tracing::debug!("Failed to get storage range"); + tx.send(empty_task_result).await.ok(); + return Ok(()); + }; + if slots.is_empty() && proof.is_empty() { + tx.send(empty_task_result).await.ok(); + tracing::debug!("Received empty storage range"); + return Ok(()); + } + // Check we got some data and no more than the requested amount + if slots.len() > chunk_storage_roots.len() || slots.is_empty() { + tx.send(empty_task_result).await.ok(); + return Ok(()); + } + // Unzip & validate response + let proof = encodable_to_proof(&proof); + let mut account_storages: Vec> = vec![]; + let mut should_continue = false; + // Validate each storage range + let mut storage_roots = chunk_storage_roots.into_iter(); + let last_slot_index = slots.len() - 1; + for (i, next_account_slots) in slots.into_iter().enumerate() { + // We won't accept empty storage ranges + if next_account_slots.is_empty() { + // This shouldn't happen + error!("Received empty storage range, skipping"); + tx.send(empty_task_result.clone()).await.ok(); + return Ok(()); + } + let encoded_values = next_account_slots + .iter() + .map(|slot| slot.data.encode_to_vec()) + .collect::>(); + let hashed_keys: Vec<_> = next_account_slots.iter().map(|slot| slot.hash).collect(); + + let storage_root = match storage_roots.next() { + Some(root) => root, + None => { + tx.send(empty_task_result.clone()).await.ok(); + error!("No storage root for account {i}"); + return Err(SnapError::NoStorageRoots); + } + }; + + // The proof corresponds to the last slot, for the previous ones the slot must be the full range without edge proofs + if i == last_slot_index && !proof.is_empty() { + let Ok(sc) = verify_range( + storage_root, + &start_hash, + &hashed_keys, + &encoded_values, + &proof, + ) else { + tx.send(empty_task_result).await.ok(); + return Ok(()); + }; + should_continue = sc; + } else if verify_range( + storage_root, + &start_hash, + &hashed_keys, + &encoded_values, + &[], + ) + .is_err() + { + tx.send(empty_task_result.clone()).await.ok(); + return Ok(()); + } + + account_storages.push( + next_account_slots + .iter() + .map(|slot| (slot.hash, slot.data)) + .collect(), + ); + } + let (remaining_start, remaining_end, remaining_start_hash) = if should_continue { + let last_account_storage = match account_storages.last() { + Some(storage) => storage, + None => { + tx.send(empty_task_result.clone()).await.ok(); + error!("No account storage found, this shouldn't happen"); + return Err(SnapError::NoAccountStorages); + } + }; + let (last_hash, _) = match last_account_storage.last() { + Some(last_hash) => last_hash, + None => { + tx.send(empty_task_result.clone()).await.ok(); + error!("No last hash found, this shouldn't happen"); + return Err(SnapError::NoAccountStorages); + } + }; + let next_hash_u256 = U256::from_big_endian(&last_hash.0).saturating_add(1.into()); + let next_hash = H256::from_uint(&next_hash_u256); + (start + account_storages.len() - 1, end, next_hash) + } else { + (start + account_storages.len(), end, H256::zero()) + }; + let task_result = StorageTaskResult { + start_index: start, + account_storages, + peer_id, + remaining_start, + remaining_end, + remaining_hash_range: (remaining_start_hash, task.end_hash), + }; + tx.send(task_result).await.ok(); + Ok::<(), SnapError>(()) +} diff --git a/crates/networking/p2p/snap/constants.rs b/crates/networking/p2p/snap/constants.rs new file mode 100644 index 00000000000..81bdc59365e --- /dev/null +++ b/crates/networking/p2p/snap/constants.rs @@ -0,0 +1,121 @@ +//! Snap Sync Protocol Constants +//! +//! This module centralizes all constants used in the snap sync implementation. +//! Constants are organized by their functional area. + +use ethrex_common::H256; +use std::time::Duration; + +// ============================================================================= +// RESPONSE LIMITS +// ============================================================================= + +/// Maximum response size in bytes for snap protocol requests (512 KB). +/// +/// This limits the amount of data a peer can return in a single response, +/// preventing memory exhaustion and ensuring reasonable response times. +pub const MAX_RESPONSE_BYTES: u64 = 512 * 1024; + +/// Maximum number of accounts/items to request in a single snap request. +/// +/// This magic number is not part of the protocol specification and is taken +/// from geth. See: +/// +pub const SNAP_LIMIT: usize = 128; + +// ============================================================================= +// HASH BOUNDARIES +// ============================================================================= + +/// Maximum hash value (all bits set to 1). +/// +/// Used as the upper bound when requesting the full range of accounts/storage. +pub const HASH_MAX: H256 = H256([0xFF; 32]); + +// ============================================================================= +// BATCH SIZES +// ============================================================================= + +/// Size of the in-memory buffer before flushing to disk during snap sync (64 MB). +/// +/// During account range and storage range downloads, data is accumulated in memory +/// before being written to temporary files. This constant controls memory usage +/// during the initial snap sync phases. +pub const RANGE_FILE_CHUNK_SIZE: usize = 1024 * 1024 * 64; + +/// Number of chunks to split the account range into for parallel downloading. +pub const ACCOUNT_RANGE_CHUNK_COUNT: usize = 800; + +/// Number of storage accounts to process per batch during state healing. +pub const STORAGE_BATCH_SIZE: usize = 300; + +/// Number of trie nodes to request per batch during state/storage healing. +pub const NODE_BATCH_SIZE: usize = 500; + +/// Number of bytecodes to download per batch. +pub const BYTECODE_CHUNK_SIZE: usize = 50_000; + +/// Buffer size for code hash collection before writing. +pub const CODE_HASH_WRITE_BUFFER_SIZE: usize = 100_000; + +// ============================================================================= +// REQUEST CONFIGURATION +// ============================================================================= + +/// Timeout for peer responses in snap sync operations. +pub const PEER_REPLY_TIMEOUT: Duration = Duration::from_secs(15); + +/// Number of retry attempts when selecting a peer for a request. +pub const PEER_SELECT_RETRY_ATTEMPTS: u32 = 3; + +/// Number of retry attempts for individual requests. +pub const REQUEST_RETRY_ATTEMPTS: u32 = 5; + +/// Maximum number of concurrent in-flight requests during storage healing. +pub const MAX_IN_FLIGHT_REQUESTS: u32 = 77; + +// ============================================================================= +// BLOCK SYNC CONFIGURATION +// ============================================================================= + +/// Maximum number of block headers to fetch in a single request. +pub const MAX_HEADER_CHUNK: u64 = 500_000; + +/// Maximum number of block bodies to request per request. +/// +/// This value is taken from geth. Higher values may cause peer disconnections. +/// See: +/// +pub const MAX_BLOCK_BODIES_TO_REQUEST: usize = 128; + +/// Maximum attempts before giving up on header downloads during syncing. +pub const MAX_HEADER_FETCH_ATTEMPTS: u64 = 100; + +// ============================================================================= +// SNAP SYNC THRESHOLDS +// ============================================================================= + +/// Minimum number of blocks from the head to full sync during a snap sync. +/// +/// After snap syncing state, we full sync at least this many recent blocks +/// to ensure we have complete execution history for recent blocks. +pub const MIN_FULL_BLOCKS: u64 = 10_000; + +/// Number of blocks to execute in a single batch during full sync. +pub const EXECUTE_BATCH_SIZE_DEFAULT: usize = 1024; + +/// Average time between blocks (used for timestamp-based calculations). +pub const SECONDS_PER_BLOCK: u64 = 12; + +/// Assumed percentage of slots that are missing blocks. +/// +/// This is used to adjust timestamp-based pivot updates and to find "safe" +/// blocks in the chain that are unlikely to be re-orged. +pub const MISSING_SLOTS_PERCENTAGE: f64 = 0.8; + +// ============================================================================= +// PROGRESS REPORTING +// ============================================================================= + +/// Interval between progress reports during healing operations. +pub const SHOW_PROGRESS_INTERVAL_DURATION: Duration = Duration::from_secs(2); diff --git a/crates/networking/p2p/snap/error.rs b/crates/networking/p2p/snap/error.rs new file mode 100644 index 00000000000..8b229e49dbc --- /dev/null +++ b/crates/networking/p2p/snap/error.rs @@ -0,0 +1,147 @@ +//! Unified error types for the snap sync protocol +//! +//! This module consolidates all snap-related errors into a unified `SnapError` type +//! for consistent error handling across server and client operations. + +use crate::peer_table::PeerTableError; +use crate::rlpx::error::PeerConnectionError; +use ethrex_rlp::error::RLPDecodeError; +use ethrex_storage::error::StoreError; +use ethrex_trie::TrieError; +use std::io::ErrorKind; +use std::path::PathBuf; +use thiserror::Error; + +/// Unified error type for snap sync protocol operations +#[derive(Debug, Error)] +pub enum SnapError { + /// Storage layer errors + #[error(transparent)] + Store(#[from] StoreError), + + /// Protocol/connection errors + #[error(transparent)] + Protocol(#[from] PeerConnectionError), + + /// Trie operation errors + #[error(transparent)] + Trie(#[from] TrieError), + + /// RLP decoding errors + #[error(transparent)] + RlpDecode(#[from] RLPDecodeError), + + /// Peer table errors + #[error(transparent)] + PeerTable(#[from] PeerTableError), + + /// Bad request from peer (invalid or malformed request) + #[error("Bad request: {0}")] + BadRequest(String), + + /// Response validation failed (invalid proof, hash mismatch, etc.) + #[error("Response validation failed: {0}")] + ValidationError(String), + + /// Peer selection failed (no suitable peers available) + #[error("Peer selection failed: {0}")] + PeerSelection(String), + + /// Task queue is empty when it shouldn't be + #[error("No tasks in queue")] + NoTasks, + + /// Missing account data + #[error("No account hashes available")] + NoAccountHashes, + + /// Missing storage data + #[error("No account storages available")] + NoAccountStorages, + + /// Missing storage roots + #[error("No storage roots available")] + NoStorageRoots, + + /// Unexpected internal error (indicates a bug) + #[error("Unexpected internal error: {0}")] + InternalError(String), + + /// File system operation failed + #[error("File system error: {operation} at {}: {kind:?}", path.display())] + FileSystem { + operation: &'static str, + path: PathBuf, + kind: ErrorKind, + }, + + /// Snapshot directory operations + #[error("Snapshot directory error: {0}")] + SnapshotDir(String), + + /// Task was spawned but panicked + #[error("Task panicked: {0}")] + TaskPanic(String), + + /// Invalid data received from peer + #[error("Invalid data received")] + InvalidData, + + /// Hash mismatch in received data + #[error("Hash mismatch in received data")] + InvalidHash, +} + +impl SnapError { + /// Creates a file system error for directory not existing + pub fn dir_not_exists(path: PathBuf) -> Self { + Self::FileSystem { + operation: "check exists", + path, + kind: ErrorKind::NotFound, + } + } + + /// Creates a file system error for directory creation failure + pub fn dir_create_failed(path: PathBuf) -> Self { + Self::FileSystem { + operation: "create directory", + path, + kind: ErrorKind::Other, + } + } + + /// Creates a file system error for write failure + pub fn write_failed(path: PathBuf, kind: ErrorKind) -> Self { + Self::FileSystem { + operation: "write", + path, + kind, + } + } +} + +/// Converts a tokio task JoinError into SnapError +impl From for SnapError { + fn from(err: tokio::task::JoinError) -> Self { + SnapError::TaskPanic(err.to_string()) + } +} + +/// Error that occurs when dumping snapshots to disk +#[derive(Debug, thiserror::Error)] +#[error("Failed to dump snapshot to {}: {:?}", path.display(), error)] +pub struct DumpError { + pub path: PathBuf, + pub error: ErrorKind, +} + +impl From for SnapError { + fn from(err: DumpError) -> Self { + SnapError::FileSystem { + operation: "dump snapshot", + path: err.path, + kind: err.error, + } + } +} diff --git a/crates/networking/p2p/snap/mod.rs b/crates/networking/p2p/snap/mod.rs new file mode 100644 index 00000000000..1b0fee1005d --- /dev/null +++ b/crates/networking/p2p/snap/mod.rs @@ -0,0 +1,47 @@ +//! Snap Sync Protocol Implementation +//! +//! This module contains the snap sync protocol implementation including +//! server-side request processing and client-side request methods. +//! The snap protocol enables fast state synchronization by requesting +//! account ranges, storage ranges, bytecodes, and trie nodes. +//! +//! ## Module Structure +//! +//! - `server`: Server-side request processing functions +//! - `client`: Client-side request methods for PeerHandler +//! - `constants`: Protocol constants and configuration values +//! - `error`: Unified error types for snap protocol operations + +pub mod client; +pub mod constants; +pub mod error; +mod server; + +use bytes::Bytes; + +// Re-export public server functions +pub use server::{ + process_account_range_request, process_byte_codes_request, process_storage_ranges_request, + process_trie_nodes_request, +}; + +// Re-export error types +pub use error::{DumpError, SnapError}; + +// Re-export client types and functions +pub use client::{ + RequestMetadata, RequestStorageTrieNodesError, request_account_range, request_bytecodes, + request_state_trienodes, request_storage_ranges, request_storage_trienodes, +}; + +// Helper to convert proof to RLP-encodable format +#[inline] +pub(crate) fn proof_to_encodable(proof: Vec>) -> Vec { + proof.into_iter().map(Bytes::from).collect() +} + +// Helper to obtain proof from RLP-encodable format +#[inline] +pub(crate) fn encodable_to_proof(proof: &[Bytes]) -> Vec> { + proof.iter().map(|bytes| bytes.to_vec()).collect() +} diff --git a/crates/networking/p2p/snap/server.rs b/crates/networking/p2p/snap/server.rs new file mode 100644 index 00000000000..9e3325a0b9b --- /dev/null +++ b/crates/networking/p2p/snap/server.rs @@ -0,0 +1,166 @@ +use bytes::Bytes; +use ethrex_rlp::encode::RLPEncode; +use ethrex_storage::Store; + +use crate::rlpx::snap::{ + AccountRange, AccountRangeUnit, ByteCodes, GetAccountRange, GetByteCodes, GetStorageRanges, + GetTrieNodes, StorageRanges, StorageSlot, TrieNodes, +}; +use ethrex_common::types::AccountStateSlimCodec; + +use super::error::SnapError; +use super::proof_to_encodable; + +// Request Processing + +pub async fn process_account_range_request( + request: GetAccountRange, + store: Store, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut accounts = vec![]; + let mut bytes_used = 0; + for (hash, account) in store.iter_accounts_from(request.root_hash, request.starting_hash)? { + debug_assert!(hash >= request.starting_hash); + bytes_used += 32 + AccountStateSlimCodec(account).length() as u64; + accounts.push(AccountRangeUnit { hash, account }); + if hash >= request.limit_hash || bytes_used >= request.response_bytes { + break; + } + } + let proof = proof_to_encodable(store.get_account_range_proof( + request.root_hash, + request.starting_hash, + accounts.last().map(|acc| acc.hash), + )?); + Ok(AccountRange { + id: request.id, + accounts, + proof, + }) + }) + .await + .map_err(|e| SnapError::TaskPanic(e.to_string()))? +} + +pub async fn process_storage_ranges_request( + request: GetStorageRanges, + store: Store, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut slots = vec![]; + let mut proof = vec![]; + let mut bytes_used = 0; + + for hashed_address in request.account_hashes { + let mut account_slots = vec![]; + let mut res_capped = false; + + if let Some(storage_iter) = + store.iter_storage_from(request.root_hash, hashed_address, request.starting_hash)? + { + for (hash, data) in storage_iter { + debug_assert!(hash >= request.starting_hash); + bytes_used += 64_u64; // slot size + account_slots.push(StorageSlot { hash, data }); + if hash >= request.limit_hash || bytes_used >= request.response_bytes { + if bytes_used >= request.response_bytes { + res_capped = true; + } + break; + } + } + } + + // Generate proofs only if the response doesn't contain the full storage range for the account + // Aka if the starting hash is not zero or if the response was capped due to byte limit + if !request.starting_hash.is_zero() || res_capped && !account_slots.is_empty() { + proof.extend(proof_to_encodable( + store + .get_storage_range_proof( + request.root_hash, + hashed_address, + request.starting_hash, + account_slots.last().map(|acc| acc.hash), + )? + .unwrap_or_default(), + )); + } + + if !account_slots.is_empty() { + slots.push(account_slots); + } + + if bytes_used >= request.response_bytes { + break; + } + } + Ok(StorageRanges { + id: request.id, + slots, + proof, + }) + }) + .await + .map_err(|e| SnapError::TaskPanic(e.to_string()))? +} + +pub async fn process_byte_codes_request( + request: GetByteCodes, + store: Store, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut codes = vec![]; + let mut bytes_used = 0; + for code_hash in request.hashes { + if let Some(code) = store.get_account_code(code_hash)?.map(|c| c.bytecode) { + bytes_used += code.len() as u64; + codes.push(code); + } + if bytes_used >= request.bytes { + break; + } + } + Ok(ByteCodes { + id: request.id, + codes, + }) + }) + .await + .map_err(|e| SnapError::TaskPanic(e.to_string()))? +} + +pub async fn process_trie_nodes_request( + request: GetTrieNodes, + store: Store, +) -> Result { + tokio::task::spawn_blocking(move || { + let mut nodes = vec![]; + let mut remaining_bytes = request.bytes; + for paths in request.paths { + if paths.is_empty() { + return Err(SnapError::BadRequest( + "zero-item pathset requested".to_string(), + )); + } + let trie_nodes = store.get_trie_nodes( + request.root_hash, + paths.into_iter().map(|bytes| bytes.to_vec()).collect(), + remaining_bytes, + )?; + nodes.extend(trie_nodes.iter().map(|nodes| Bytes::copy_from_slice(nodes))); + remaining_bytes = remaining_bytes + .saturating_sub(trie_nodes.iter().fold(0, |acc, nodes| acc + nodes.len()) as u64); + if remaining_bytes == 0 { + break; + } + } + + Ok(TrieNodes { + id: request.id, + nodes, + }) + }) + .await + .map_err(|e| SnapError::TaskPanic(e.to_string()))? +} diff --git a/crates/networking/p2p/sync.rs b/crates/networking/p2p/sync.rs index faec6ac9031..4ad64c9ac0d 100644 --- a/crates/networking/p2p/sync.rs +++ b/crates/networking/p2p/sync.rs @@ -1,69 +1,41 @@ +//! Sync module - orchestrates full and snap synchronization +//! +//! This module provides the main `Syncer` type that coordinates synchronization +//! between full sync mode (all blocks executed) and snap sync mode (state fetched +//! via snap protocol). + mod code_collector; -mod state_healing; -mod storage_healing; +mod full; +mod healing; +mod snap_sync; -use crate::peer_handler::{BlockRequestOrder, PeerHandlerError, SNAP_LIMIT}; +use crate::metrics::METRICS; +use crate::peer_handler::{PeerHandler, PeerHandlerError}; use crate::peer_table::PeerTableError; -use crate::rlpx::p2p::SUPPORTED_ETH_CAPABILITIES; -use crate::sync::code_collector::CodeHashCollector; -use crate::sync::state_healing::heal_state_trie_wrap; -use crate::sync::storage_healing::heal_storage_trie; -use crate::utils::{ - current_unix_time, delete_leaves_folder, get_account_state_snapshots_dir, - get_account_storages_snapshots_dir, get_code_hashes_snapshots_dir, -}; -use crate::{ - metrics::{CurrentStepValue, METRICS}, - peer_handler::{MAX_BLOCK_BODIES_TO_REQUEST, PeerHandler}, -}; -use ethrex_blockchain::{BatchBlockProcessingFailure, Blockchain, error::ChainError}; -#[cfg(not(feature = "rocksdb"))] -use ethrex_common::U256; -use ethrex_common::types::Code; -use ethrex_common::{ - H256, - constants::{EMPTY_KECCACK_HASH, EMPTY_TRIE_HASH}, - types::{AccountState, Block, BlockHeader}, -}; -use ethrex_rlp::{decode::RLPDecode, error::RLPDecodeError}; +use crate::snap::constants::EXECUTE_BATCH_SIZE_DEFAULT; +use crate::utils::delete_leaves_folder; +use ethrex_blockchain::{Blockchain, error::ChainError}; +use ethrex_common::H256; +use ethrex_rlp::error::RLPDecodeError; use ethrex_storage::{Store, error::StoreError}; -#[cfg(feature = "rocksdb")] -use ethrex_trie::Trie; use ethrex_trie::TrieError; use ethrex_trie::trie_sorted::TrieGenerationError; -use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; -use std::{ - cmp::min, - collections::HashMap, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, +use std::collections::{BTreeMap, HashSet}; +use std::path::PathBuf; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, }; -use tokio::{sync::mpsc::error::SendError, time::Instant}; +use tokio::sync::mpsc::error::SendError; +use tokio::time::Instant; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -/// The minimum amount of blocks from the head that we want to full sync during a snap sync -const MIN_FULL_BLOCKS: u64 = 10_000; -/// Amount of blocks to execute in a single batch during FullSync -const EXECUTE_BATCH_SIZE_DEFAULT: usize = 1024; -/// Amount of seconds between blocks -const SECONDS_PER_BLOCK: u64 = 12; +use tracing::{error, info}; -/// Bytecodes to downloader per batch -const BYTECODE_CHUNK_SIZE: usize = 50_000; - -/// We assume this amount of slots are missing a block to adjust our timestamp -/// based update pivot algorithm. This is also used to try to find "safe" blocks in the chain -/// that are unlikely to be re-orged. -const MISSING_SLOTS_PERCENTAGE: f64 = 0.8; - -/// Maximum attempts before giving up on header downloads during syncing -const MAX_HEADER_FETCH_ATTEMPTS: u64 = 100; +// Re-export types used by submodules +pub use snap_sync::{ + SnapBlockSyncState, block_is_stale, calculate_staleness_timestamp, update_pivot, + validate_bytecodes, validate_state_root, validate_storage_root, +}; #[cfg(feature = "sync-test")] lazy_static::lazy_static! { @@ -162,967 +134,33 @@ impl Syncer { // Take picture of the current sync mode, we will update the original value when we need to if self.snap_enabled.load(Ordering::Relaxed) { METRICS.enable().await; - let sync_cycle_result = self.sync_cycle_snap(sync_head, store).await; - METRICS.disable().await; - sync_cycle_result - } else { - self.sync_cycle_full(sync_head, store).await - } - } - - /// Performs the sync cycle described in `start_sync`, returns an error if the sync fails at any given step and aborts all active processes - async fn sync_cycle_snap(&mut self, sync_head: H256, store: Store) -> Result<(), SyncError> { - // Request all block headers between the current head and the sync head - // We will begin from the current head so that we download the earliest state first - // This step is not parallelized - let mut block_sync_state = SnapBlockSyncState::new(store.clone()); - // Check if we have some blocks downloaded from a previous sync attempt - // This applies only to snap sync—full sync always starts fetching headers - // from the canonical block, which updates as new block headers are fetched. - let mut current_head = block_sync_state.get_current_head().await?; - let mut current_head_number = store - .get_block_number(current_head) - .await? - .ok_or(SyncError::BlockNumber(current_head))?; - info!( - "Syncing from current head {:?} to sync_head {:?}", - current_head, sync_head - ); - let pending_block = match store.get_pending_block(sync_head).await { - Ok(res) => res, - Err(e) => return Err(e.into()), - }; - - let mut attempts = 0; - - // We validate that we have the folders that are being used empty, as we currently assume - // they are. If they are not empty we empty the folder - delete_leaves_folder(&self.datadir); - - info!("Starting to download block headers from peers"); - - loop { - debug!("Requesting Block Headers from {current_head}"); - - let Some(mut block_headers) = self - .peers - .request_block_headers(current_head_number, sync_head) - .await? - else { - if attempts > MAX_HEADER_FETCH_ATTEMPTS { - warn!("Sync failed to find target block header, aborting"); - return Ok(()); - } - attempts += 1; - tokio::time::sleep(Duration::from_millis(1.1_f64.powf(attempts as f64) as u64)) - .await; - continue; - }; - - debug!("Sync Log 1: In snap sync"); - debug!( - "Sync Log 2: State block hashes len {}", - block_sync_state.block_hashes.len() - ); - - let (first_block_hash, first_block_number, first_block_parent_hash) = - match block_headers.first() { - Some(header) => (header.hash(), header.number, header.parent_hash), - None => continue, - }; - let (last_block_hash, last_block_number) = match block_headers.last() { - Some(header) => (header.hash(), header.number), - None => continue, - }; - // TODO(#2126): This is just a temporary solution to avoid a bug where the sync would get stuck - // on a loop when the target head is not found, i.e. on a reorg with a side-chain. - if first_block_hash == last_block_hash - && first_block_hash == current_head - && current_head != sync_head - { - // There is no path to the sync head this goes back until it find a common ancerstor - warn!("Sync failed to find target block header, going back to the previous parent"); - current_head = first_block_parent_hash; - continue; - } - - debug!( - "Received {} block headers| First Number: {} Last Number: {}", - block_headers.len(), - first_block_number, - last_block_number - ); - - // If we have a pending block from new_payload request - // attach it to the end if it matches the parent_hash of the latest received header - if let Some(ref block) = pending_block - && block.header.parent_hash == last_block_hash - { - block_headers.push(block.header.clone()); - } - - // Filter out everything after the sync_head - let mut sync_head_found = false; - if let Some(index) = block_headers - .iter() - .position(|header| header.hash() == sync_head) - { - sync_head_found = true; - block_headers.drain(index + 1..); - } - - // Update current fetch head - current_head = last_block_hash; - current_head_number = last_block_number; - - // If the sync head is not 0 we search to fullsync - let head_found = sync_head_found && store.get_latest_block_number().await? > 0; - // Or the head is very close to 0 - let head_close_to_0 = last_block_number < MIN_FULL_BLOCKS; - - if head_found || head_close_to_0 { - // Too few blocks for a snap sync, switching to full sync - info!("Sync head is found, switching to FullSync"); - self.snap_enabled.store(false, Ordering::Relaxed); - return self.sync_cycle_full(sync_head, store.clone()).await; - } - - // Discard the first header as we already have it - if block_headers.len() > 1 { - let block_headers_iter = block_headers.into_iter().skip(1); - - block_sync_state - .process_incoming_headers(block_headers_iter) - .await?; - } - - if sync_head_found { - break; - }; - } - - info!("All block headers downloaded successfully"); - - self.snap_sync(&store, &mut block_sync_state).await?; - - store.clear_snap_state().await?; - self.snap_enabled.store(false, Ordering::Relaxed); - - Ok(()) - } - - /// Performs the sync cycle described in `start_sync`. - /// - /// # Returns - /// - /// Returns an error if the sync fails at any given step and aborts all active processes - async fn sync_cycle_full( - &mut self, - mut sync_head: H256, - store: Store, - ) -> Result<(), SyncError> { - info!("Syncing to sync_head {:?}", sync_head); - - // Check if the sync_head is a pending block, if so, gather all pending blocks belonging to its chain - let mut pending_blocks = vec![]; - while let Some(block) = store.get_pending_block(sync_head).await? { - if store.is_canonical_sync(block.hash())? { - // Ignore canonical blocks still in pending - break; - } - sync_head = block.header.parent_hash; - pending_blocks.insert(0, block); - } - - // Request all block headers between the sync head and our local chain - // We will begin from the sync head so that we download the latest state first, ensuring we follow the correct chain - // This step is not parallelized - let mut start_block_number; - let mut end_block_number = 0; - let mut headers = vec![]; - let mut single_batch = true; - - let mut attempts = 0; - - // Request and store all block headers from the advertised sync head - loop { - let Some(mut block_headers) = self - .peers - .request_block_headers_from_hash(sync_head, BlockRequestOrder::NewToOld) - .await? - else { - if attempts > MAX_HEADER_FETCH_ATTEMPTS { - warn!("Sync failed to find target block header, aborting"); - return Ok(()); - } - attempts += 1; - tokio::time::sleep(Duration::from_millis(1.1_f64.powf(attempts as f64) as u64)) - .await; - continue; - }; - debug!("Sync Log 9: Received {} block headers", block_headers.len()); - - let first_header = block_headers.first().ok_or(SyncError::NoBlocks)?; - let last_header = block_headers.last().ok_or(SyncError::NoBlocks)?; - - info!( - "Received {} block headers| First Number: {} Last Number: {}", - block_headers.len(), - first_header.number, - last_header.number, - ); - end_block_number = end_block_number.max(first_header.number); - start_block_number = last_header.number; - - sync_head = last_header.parent_hash; - if store.is_canonical_sync(sync_head)? || sync_head.is_zero() { - // Incoming chain merged with current chain - // Filter out already canonical blocks from batch - let mut first_canon_block = block_headers.len(); - for (index, header) in block_headers.iter().enumerate() { - if store.is_canonical_sync(header.hash())? { - first_canon_block = index; - break; - } - } - block_headers.drain(first_canon_block..block_headers.len()); - if let Some(last_header) = block_headers.last() { - start_block_number = last_header.number; - } - // If the fullsync consists of a single batch of headers we can just keep them in memory instead of writing them to Store - if single_batch { - headers = block_headers.into_iter().rev().collect(); - } else { - store.add_fullsync_batch(block_headers).await?; - } - break; - } - store.add_fullsync_batch(block_headers).await?; - single_batch = false; - } - end_block_number += 1; - start_block_number = start_block_number.max(1); - - // Download block bodies and execute full blocks in batches - for start in (start_block_number..end_block_number).step_by(*EXECUTE_BATCH_SIZE) { - let batch_size = EXECUTE_BATCH_SIZE.min((end_block_number - start) as usize); - let final_batch = end_block_number == start + batch_size as u64; - // Retrieve batch from DB - if !single_batch { - headers = store - .read_fullsync_batch(start, batch_size as u64) - .await? - .into_iter() - .map(|opt| opt.ok_or(SyncError::MissingFullsyncBatch)) - .collect::, SyncError>>()?; - } - let mut blocks = Vec::new(); - // Request block bodies - // Download block bodies - while !headers.is_empty() { - let header_batch = &headers[..min(MAX_BLOCK_BODIES_TO_REQUEST, headers.len())]; - let bodies = self - .peers - .request_block_bodies(header_batch) - .await? - .ok_or(SyncError::BodiesNotFound)?; - debug!("Obtained: {} block bodies", bodies.len()); - let block_batch = headers - .drain(..bodies.len()) - .zip(bodies) - .map(|(header, body)| Block { header, body }); - blocks.extend(block_batch); - } - if !blocks.is_empty() { - // Execute blocks - info!( - "Executing {} blocks for full sync. First block hash: {:#?} Last block hash: {:#?}", - blocks.len(), - blocks.first().ok_or(SyncError::NoBlocks)?.hash(), - blocks.last().ok_or(SyncError::NoBlocks)?.hash() - ); - self.add_blocks_in_batch(blocks, final_batch, store.clone()) - .await?; - } - } - - // Execute pending blocks - if !pending_blocks.is_empty() { - info!( - "Executing {} blocks for full sync. First block hash: {:#?} Last block hash: {:#?}", - pending_blocks.len(), - pending_blocks.first().ok_or(SyncError::NoBlocks)?.hash(), - pending_blocks.last().ok_or(SyncError::NoBlocks)?.hash() - ); - self.add_blocks_in_batch(pending_blocks, true, store.clone()) - .await?; - } - - store.clear_fullsync_headers().await?; - Ok(()) - } - - async fn add_blocks_in_batch( - &self, - blocks: Vec, - final_batch: bool, - store: Store, - ) -> Result<(), SyncError> { - let execution_start = Instant::now(); - // Copy some values for later - let blocks_len = blocks.len(); - let numbers_and_hashes = blocks - .iter() - .map(|b| (b.header.number, b.hash())) - .collect::>(); - let (last_block_number, last_block_hash) = numbers_and_hashes - .last() - .cloned() - .ok_or(SyncError::InvalidRangeReceived)?; - let (first_block_number, first_block_hash) = numbers_and_hashes - .first() - .cloned() - .ok_or(SyncError::InvalidRangeReceived)?; - - let blocks_hashes = blocks.iter().map(|block| block.hash()).collect::>(); - // Run the batch - if let Err((err, batch_failure)) = Syncer::add_blocks( - self.blockchain.clone(), - blocks, - final_batch, - self.cancel_token.clone(), - ) - .await - { - if let Some(batch_failure) = batch_failure { - warn!("Failed to add block during FullSync: {err}"); - // Since running the batch failed we set the failing block and its descendants - // with having an invalid ancestor on the following cases. - if let ChainError::InvalidBlock(_) = err { - let mut block_hashes_with_invalid_ancestor: Vec = vec![]; - if let Some(index) = blocks_hashes - .iter() - .position(|x| x == &batch_failure.failed_block_hash) - { - block_hashes_with_invalid_ancestor = blocks_hashes[index..].to_vec(); - } - - for hash in block_hashes_with_invalid_ancestor { - store - .set_latest_valid_ancestor(hash, batch_failure.last_valid_hash) - .await?; - } - } - } - return Err(err.into()); - } - - store - .forkchoice_update( - numbers_and_hashes, - last_block_number, - last_block_hash, - None, - None, - ) - .await?; - - let execution_time: f64 = execution_start.elapsed().as_millis() as f64 / 1000.0; - let blocks_per_second = blocks_len as f64 / execution_time; - - info!( - "[SYNCING] Executed & stored {} blocks in {:.3} seconds.\n\ - Started at block with hash {} (number {}).\n\ - Finished at block with hash {} (number {}).\n\ - Blocks per second: {:.3}", - blocks_len, - execution_time, - first_block_hash, - first_block_number, - last_block_hash, - last_block_number, - blocks_per_second - ); - Ok(()) - } - - /// Executes the given blocks and stores them - /// If sync_head_found is true, they will be executed one by one - /// If sync_head_found is false, they will be executed in a single batch - async fn add_blocks( - blockchain: Arc, - blocks: Vec, - sync_head_found: bool, - cancel_token: CancellationToken, - ) -> Result<(), (ChainError, Option)> { - // If we found the sync head, run the blocks sequentially to store all the blocks's state - if sync_head_found { - tokio::task::spawn_blocking(move || { - let mut last_valid_hash = H256::default(); - for block in blocks { - let block_hash = block.hash(); - blockchain.add_block_pipeline(block).map_err(|e| { - ( - e, - Some(BatchBlockProcessingFailure { - last_valid_hash, - failed_block_hash: block_hash, - }), - ) - })?; - last_valid_hash = block_hash; - } - Ok(()) - }) - .await - .map_err(|e| (ChainError::Custom(e.to_string()), None))? - } else { - blockchain.add_blocks_in_batch(blocks, cancel_token).await - } - } -} - -/// Fetches all block bodies for the given block headers via p2p and stores them -async fn store_block_bodies( - mut block_headers: Vec, - mut peers: PeerHandler, - store: Store, -) -> Result<(), SyncError> { - loop { - debug!("Requesting Block Bodies "); - if let Some(block_bodies) = peers.request_block_bodies(&block_headers).await? { - debug!(" Received {} Block Bodies", block_bodies.len()); - // Track which bodies we have already fetched - let current_block_headers = block_headers.drain(..block_bodies.len()); - // Add bodies to storage - for (hash, body) in current_block_headers - .map(|h| h.hash()) - .zip(block_bodies.into_iter()) - { - store.add_block_body(hash, body).await?; - } - - // Check if we need to ask for another batch - if block_headers.is_empty() { - break; - } - } - } - Ok(()) -} - -/// Persisted State during the Block Sync phase for SnapSync -#[derive(Clone)] -pub struct SnapBlockSyncState { - block_hashes: Vec, - store: Store, -} - -impl SnapBlockSyncState { - fn new(store: Store) -> Self { - Self { - block_hashes: Vec::new(), - store, - } - } - - /// Obtain the current head from where to start or resume block sync - async fn get_current_head(&self) -> Result { - if let Some(head) = self.store.get_header_download_checkpoint().await? { - Ok(head) - } else { - self.store - .get_latest_canonical_block_hash() - .await? - .ok_or(SyncError::NoLatestCanonical) - } - } - - /// Stores incoming headers to the Store and saves their hashes - async fn process_incoming_headers( - &mut self, - block_headers: impl Iterator, - ) -> Result<(), SyncError> { - let mut block_headers_vec = Vec::with_capacity(block_headers.size_hint().1.unwrap_or(0)); - let mut block_hashes = Vec::with_capacity(block_headers.size_hint().1.unwrap_or(0)); - for header in block_headers { - block_hashes.push(header.hash()); - block_headers_vec.push(header); - } - self.store - .set_header_download_checkpoint( - *block_hashes.last().ok_or(SyncError::InvalidRangeReceived)?, - ) - .await?; - self.block_hashes.extend_from_slice(&block_hashes); - self.store.add_block_headers(block_headers_vec).await?; - Ok(()) - } -} - -impl Syncer { - async fn snap_sync( - &mut self, - store: &Store, - block_sync_state: &mut SnapBlockSyncState, - ) -> Result<(), SyncError> { - // snap-sync: launch tasks to fetch blocks and state in parallel - // - Fetch each block's body and its receipt via eth p2p requests - // - Fetch the pivot block's state via snap p2p requests - // - Execute blocks after the pivot (like in full-sync) - let pivot_hash = block_sync_state - .block_hashes - .last() - .ok_or(SyncError::NoBlockHeaders)?; - let mut pivot_header = store - .get_block_header_by_hash(*pivot_hash)? - .ok_or(SyncError::CorruptDB)?; - - while block_is_stale(&pivot_header) { - pivot_header = update_pivot( - pivot_header.number, - pivot_header.timestamp, + // We validate that we have the folders that are being used empty, as we currently assume + // they are. If they are not empty we empty the folder + delete_leaves_folder(&self.datadir); + let sync_cycle_result = snap_sync::sync_cycle_snap( &mut self.peers, - block_sync_state, - ) - .await?; - } - debug!( - "Selected block {} as pivot for snap sync", - pivot_header.number - ); - - let state_root = pivot_header.state_root; - let account_state_snapshots_dir = get_account_state_snapshots_dir(&self.datadir); - let account_storages_snapshots_dir = get_account_storages_snapshots_dir(&self.datadir); - - let code_hashes_snapshot_dir = get_code_hashes_snapshots_dir(&self.datadir); - std::fs::create_dir_all(&code_hashes_snapshot_dir).map_err(|_| SyncError::CorruptPath)?; - - // Create collector to store code hashes in files - let mut code_hash_collector: CodeHashCollector = - CodeHashCollector::new(code_hashes_snapshot_dir.clone()); - - let mut storage_accounts = AccountStorageRoots::default(); - if !std::env::var("SKIP_START_SNAP_SYNC").is_ok_and(|var| !var.is_empty()) { - // We start by downloading all of the leafs of the trie of accounts - // The function request_account_range writes the leafs into files in - // account_state_snapshots_dir - - info!("Starting to download account ranges from peers"); - self.peers - .request_account_range( - H256::zero(), - H256::repeat_byte(0xff), - account_state_snapshots_dir.as_ref(), - &mut pivot_header, - block_sync_state, - ) - .await?; - info!("Finish downloading account ranges from peers"); - - *METRICS.account_tries_insert_start_time.lock().await = Some(SystemTime::now()); - METRICS - .current_step - .set(CurrentStepValue::InsertingAccountRanges); - // We read the account leafs from the files in account_state_snapshots_dir, write it into - // the trie to compute the nodes and stores the accounts with storages for later use - - // Variable `accounts_with_storage` unused if not in rocksdb - #[allow(unused_variables)] - let (computed_state_root, accounts_with_storage) = insert_accounts( - store.clone(), - &mut storage_accounts, - &account_state_snapshots_dir, - &self.datadir, - &mut code_hash_collector, - ) - .await?; - info!( - "Finished inserting account ranges, total storage accounts: {}", - storage_accounts.accounts_with_storage_root.len() - ); - *METRICS.account_tries_insert_end_time.lock().await = Some(SystemTime::now()); - - debug!("Original state root: {state_root:?}"); - debug!("Computed state root after request_account_rages: {computed_state_root:?}"); - - *METRICS.storage_tries_download_start_time.lock().await = Some(SystemTime::now()); - // We start downloading the storage leafs. To do so, we need to be sure that the storage root - // is correct. To do so, we always heal the state trie before requesting storage rates - let mut chunk_index = 0_u64; - let mut state_leafs_healed = 0_u64; - let mut storage_range_request_attempts = 0; - loop { - while block_is_stale(&pivot_header) { - pivot_header = update_pivot( - pivot_header.number, - pivot_header.timestamp, - &mut self.peers, - block_sync_state, - ) - .await?; - } - // heal_state_trie_wrap returns false if we ran out of time before fully healing the trie - // We just need to update the pivot and start again - if !heal_state_trie_wrap( - pivot_header.state_root, - store.clone(), - &self.peers, - calculate_staleness_timestamp(pivot_header.timestamp), - &mut state_leafs_healed, - &mut storage_accounts, - &mut code_hash_collector, - ) - .await? - { - continue; - }; - - info!( - "Started request_storage_ranges with {} accounts with storage root unchanged", - storage_accounts.accounts_with_storage_root.len() - ); - storage_range_request_attempts += 1; - if storage_range_request_attempts < 5 { - chunk_index = self - .peers - .request_storage_ranges( - &mut storage_accounts, - account_storages_snapshots_dir.as_ref(), - chunk_index, - &mut pivot_header, - store.clone(), - ) - .await - .map_err(SyncError::PeerHandler)?; - } else { - for (acc_hash, (maybe_root, old_intervals)) in - storage_accounts.accounts_with_storage_root.iter() - { - // When we fall into this case what happened is there are certain accounts for which - // the storage root went back to a previous value we already had, and thus could not download - // their storage leaves because we were using an old value for their storage root. - // The fallback is to ensure we mark it for storage healing. - storage_accounts.healed_accounts.insert(*acc_hash); - debug!( - "We couldn't download these accounts on request_storage_ranges. Falling back to storage healing for it. - Account hash: {:x?}, {:x?}. Number of intervals {}", - acc_hash, - maybe_root, - old_intervals.len() - ); - } - - warn!("Storage could not be downloaded after multiple attempts. Marking for healing. - This could impact snap sync time (healing may take a while)."); - - storage_accounts.accounts_with_storage_root.clear(); - } - - debug!( - "Ended request_storage_ranges with {} accounts with storage root unchanged and not downloaded yet and with {} big/healed accounts", - storage_accounts.accounts_with_storage_root.len(), - // These accounts are marked as heals if they're a big account. This is - // because we don't know if the storage root is still valid - storage_accounts.healed_accounts.len(), - ); - if !block_is_stale(&pivot_header) { - break; - } - debug!("We stopped because of staleness, restarting loop"); - } - info!("Finished request_storage_ranges"); - *METRICS.storage_tries_download_end_time.lock().await = Some(SystemTime::now()); - - *METRICS.storage_tries_insert_start_time.lock().await = Some(SystemTime::now()); - METRICS - .current_step - .set(crate::metrics::CurrentStepValue::InsertingStorageRanges); - let account_storages_snapshots_dir = get_account_storages_snapshots_dir(&self.datadir); - - insert_storages( - store.clone(), - accounts_with_storage, - &account_storages_snapshots_dir, + self.blockchain.clone(), + &self.snap_enabled, + sync_head, + store, &self.datadir, ) - .await?; - - *METRICS.storage_tries_insert_end_time.lock().await = Some(SystemTime::now()); - - info!("Finished storing storage tries"); - } - - *METRICS.heal_start_time.lock().await = Some(SystemTime::now()); - info!("Starting Healing Process"); - let mut global_state_leafs_healed: u64 = 0; - let mut global_storage_leafs_healed: u64 = 0; - let mut healing_done = false; - while !healing_done { - // This if is an edge case for the skip snap sync scenario - if block_is_stale(&pivot_header) { - pivot_header = update_pivot( - pivot_header.number, - pivot_header.timestamp, - &mut self.peers, - block_sync_state, - ) - .await?; - } - healing_done = heal_state_trie_wrap( - pivot_header.state_root, - store.clone(), - &self.peers, - calculate_staleness_timestamp(pivot_header.timestamp), - &mut global_state_leafs_healed, - &mut storage_accounts, - &mut code_hash_collector, - ) - .await?; - if !healing_done { - continue; - } - healing_done = heal_storage_trie( - pivot_header.state_root, - &storage_accounts, + .await; + METRICS.disable().await; + sync_cycle_result + } else { + full::sync_cycle_full( &mut self.peers, - store.clone(), - HashMap::new(), - calculate_staleness_timestamp(pivot_header.timestamp), - &mut global_storage_leafs_healed, - ) - .await?; - } - *METRICS.heal_end_time.lock().await = Some(SystemTime::now()); - - store.generate_flatkeyvalue()?; - - debug_assert!(validate_state_root(store.clone(), pivot_header.state_root).await); - debug_assert!(validate_storage_root(store.clone(), pivot_header.state_root).await); - - info!("Finished healing"); - - // Finish code hash collection - code_hash_collector.finish().await?; - - *METRICS.bytecode_download_start_time.lock().await = Some(SystemTime::now()); - - let code_hashes_dir = get_code_hashes_snapshots_dir(&self.datadir); - let mut seen_code_hashes = HashSet::new(); - let mut code_hashes_to_download = Vec::new(); - - info!("Starting download bytecodes from peers"); - for entry in std::fs::read_dir(&code_hashes_dir) - .map_err(|_| SyncError::CodeHashesSnapshotsDirNotFound)? - { - let entry = entry.map_err(|_| SyncError::CorruptPath)?; - let snapshot_contents = std::fs::read(entry.path()) - .map_err(|err| SyncError::SnapshotReadError(entry.path(), err))?; - let code_hashes: Vec = RLPDecode::decode(&snapshot_contents) - .map_err(|_| SyncError::CodeHashesSnapshotDecodeError(entry.path()))?; - - for hash in code_hashes { - // If we haven't seen the code hash yet, add it to the list of hashes to download - if seen_code_hashes.insert(hash) { - code_hashes_to_download.push(hash); - - if code_hashes_to_download.len() >= BYTECODE_CHUNK_SIZE { - debug!( - "Starting bytecode download of {} hashes", - code_hashes_to_download.len() - ); - let bytecodes = self - .peers - .request_bytecodes(&code_hashes_to_download) - .await - .map_err(SyncError::PeerHandler)? - .ok_or(SyncError::BytecodesNotFound)?; - - store - .write_account_code_batch( - code_hashes_to_download - .drain(..) - .zip(bytecodes) - // SAFETY: hash already checked by the download worker - .map(|(hash, code)| { - (hash, Code::from_bytecode_unchecked(code, hash)) - }) - .collect(), - ) - .await?; - } - } - } - } - - // Download remaining bytecodes if any - if !code_hashes_to_download.is_empty() { - let bytecodes = self - .peers - .request_bytecodes(&code_hashes_to_download) - .await - .map_err(SyncError::PeerHandler)? - .ok_or(SyncError::BytecodesNotFound)?; - store - .write_account_code_batch( - code_hashes_to_download - .drain(..) - .zip(bytecodes) - // SAFETY: hash already checked by the download worker - .map(|(hash, code)| (hash, Code::from_bytecode_unchecked(code, hash))) - .collect(), - ) - .await?; - } - - info!("Finished download bytecodes from peers"); - - std::fs::remove_dir_all(code_hashes_dir) - .map_err(|_| SyncError::CodeHashesSnapshotsDirNotFound)?; - - *METRICS.bytecode_download_end_time.lock().await = Some(SystemTime::now()); - - debug_assert!(validate_bytecodes(store.clone(), pivot_header.state_root)); - - store_block_bodies( - vec![pivot_header.clone()], - self.peers.clone(), - store.clone(), - ) - .await?; - - let block = store - .get_block_by_hash(pivot_header.hash()) - .await? - .ok_or(SyncError::CorruptDB)?; - - store.add_block(block).await?; - - let numbers_and_hashes = block_sync_state - .block_hashes - .iter() - .rev() - .enumerate() - .map(|(i, hash)| (pivot_header.number - i as u64, *hash)) - .collect::>(); - - store - .forkchoice_update( - numbers_and_hashes, - pivot_header.number, - pivot_header.hash(), - None, - None, + self.blockchain.clone(), + self.cancel_token.clone(), + sync_head, + store, ) - .await?; - Ok(()) - } -} - -#[cfg(not(feature = "rocksdb"))] -use ethrex_rlp::encode::RLPEncode; - -#[cfg(not(feature = "rocksdb"))] -type StorageRoots = (H256, Vec<(ethrex_trie::Nibbles, Vec)>); - -#[cfg(not(feature = "rocksdb"))] -fn compute_storage_roots( - store: Store, - account_hash: H256, - key_value_pairs: &[(H256, U256)], -) -> Result { - use ethrex_trie::{Nibbles, Node}; - - let storage_trie = store.open_direct_storage_trie(account_hash, *EMPTY_TRIE_HASH)?; - let trie_hash = match storage_trie.db().get(Nibbles::default())? { - Some(noderlp) => Node::decode(&noderlp)?.compute_hash().finalize(), - None => *EMPTY_TRIE_HASH, - }; - let mut storage_trie = store.open_direct_storage_trie(account_hash, trie_hash)?; - - for (hashed_key, value) in key_value_pairs { - if let Err(err) = storage_trie.insert(hashed_key.0.to_vec(), value.encode_to_vec()) { - warn!( - "Failed to insert hashed key {hashed_key:?} in account hash: {account_hash:?}, err={err:?}" - ); - }; - METRICS.storage_leaves_inserted.inc(); - } - - let (_, changes) = storage_trie.collect_changes_since_last_hash(); - - Ok((account_hash, changes)) -} - -pub async fn update_pivot( - block_number: u64, - block_timestamp: u64, - peers: &mut PeerHandler, - block_sync_state: &mut SnapBlockSyncState, -) -> Result { - // We multiply the estimation by 0.9 in order to account for missing slots (~9% in tesnets) - let new_pivot_block_number = block_number - + ((current_unix_time().saturating_sub(block_timestamp) / SECONDS_PER_BLOCK) as f64 - * MISSING_SLOTS_PERCENTAGE) as u64; - debug!( - "Current pivot is stale (number: {}, timestamp: {}). New pivot number: {}", - block_number, block_timestamp, new_pivot_block_number - ); - loop { - let Some((peer_id, mut connection)) = peers - .peer_table - .get_best_peer(&SUPPORTED_ETH_CAPABILITIES) - .await? - else { - // When we come here, we may be waiting for requests to timeout. - // Because we're waiting for a timeout, we sleep so the rest of the code - // can get to them - debug!("We tried to get peers during update_pivot, but we found no free peers"); - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - }; - - let peer_score = peers.peer_table.get_score(&peer_id).await?; - debug!( - "Trying to update pivot to {new_pivot_block_number} with peer {peer_id} (score: {peer_score})" - ); - let Some(pivot) = peers - .get_block_header(peer_id, &mut connection, new_pivot_block_number) .await - .map_err(SyncError::PeerHandler)? - else { - // Penalize peer - peers.peer_table.record_failure(&peer_id).await?; - let peer_score = peers.peer_table.get_score(&peer_id).await?; - warn!( - "Received None pivot from peer {peer_id} (score after penalizing: {peer_score}). Retrying" - ); - continue; - }; - - // Reward peer - peers.peer_table.record_success(&peer_id).await?; - debug!("Succesfully updated pivot"); - let block_headers = peers - .request_block_headers(block_number + 1, pivot.hash()) - .await? - .ok_or(SyncError::NoBlockHeaders)?; - block_sync_state - .process_incoming_headers(block_headers.into_iter()) - .await?; - *METRICS.sync_head_hash.lock().await = pivot.hash(); - return Ok(pivot.clone()); + } } } -pub fn block_is_stale(block_header: &BlockHeader) -> bool { - calculate_staleness_timestamp(block_header.timestamp) < current_unix_time() -} - -pub fn calculate_staleness_timestamp(timestamp: u64) -> u64 { - timestamp + (SNAP_LIMIT as u64 * 12) -} #[derive(Debug, Default)] #[allow(clippy::type_complexity)] /// We store for optimization the accounts that need to heal storage @@ -1199,6 +237,8 @@ pub enum SyncError { PeerTableError(#[from] PeerTableError), #[error("Missing fullsync batch")] MissingFullsyncBatch, + #[error("Snap error: {0}")] + Snap(#[from] crate::snap::SnapError), } impl SyncError { @@ -1223,7 +263,8 @@ impl SyncError { | SyncError::BytecodeFileError | SyncError::NoLatestCanonical | SyncError::PeerTableError(_) - | SyncError::MissingFullsyncBatch => false, + | SyncError::MissingFullsyncBatch + | SyncError::Snap(_) => false, SyncError::Chain(_) | SyncError::Store(_) | SyncError::Send(_) @@ -1244,415 +285,3 @@ impl From> for SyncError { Self::Send(value.to_string()) } } - -pub async fn validate_state_root(store: Store, state_root: H256) -> bool { - info!("Starting validate_state_root"); - let validated = tokio::task::spawn_blocking(move || { - store - .open_locked_state_trie(state_root) - .expect("couldn't open trie") - .validate() - }) - .await - .expect("We should be able to create threads"); - - if validated.is_ok() { - info!("Succesfully validated tree, {state_root} found"); - } else { - error!("We have failed the validation of the state tree"); - std::process::exit(1); - } - validated.is_ok() -} - -pub async fn validate_storage_root(store: Store, state_root: H256) -> bool { - info!("Starting validate_storage_root"); - let is_valid = tokio::task::spawn_blocking(move || { - store - .iter_accounts(state_root) - .expect("couldn't iterate accounts") - .par_bridge() - .try_for_each(|(hashed_address, account_state)| { - let store_clone = store.clone(); - store_clone - .open_locked_storage_trie( - hashed_address, - state_root, - account_state.storage_root, - ) - .expect("couldn't open storage trie") - .validate() - }) - }) - .await - .expect("We should be able to create threads"); - info!("Finished validate_storage_root"); - if is_valid.is_err() { - std::process::exit(1); - } - is_valid.is_ok() -} - -pub fn validate_bytecodes(store: Store, state_root: H256) -> bool { - info!("Starting validate_bytecodes"); - let mut is_valid = true; - for (account_hash, account_state) in store - .iter_accounts(state_root) - .expect("we couldn't iterate over accounts") - { - if account_state.code_hash != *EMPTY_KECCACK_HASH - && !store - .get_account_code(account_state.code_hash) - .is_ok_and(|code| code.is_some()) - { - error!( - "Missing code hash {:x} for account {:x}", - account_state.code_hash, account_hash - ); - is_valid = false - } - } - if !is_valid { - std::process::exit(1); - } - is_valid -} - -#[cfg(not(feature = "rocksdb"))] -async fn insert_accounts( - store: Store, - storage_accounts: &mut AccountStorageRoots, - account_state_snapshots_dir: &Path, - _: &Path, - code_hash_collector: &mut CodeHashCollector, -) -> Result<(H256, BTreeSet), SyncError> { - let mut computed_state_root = *EMPTY_TRIE_HASH; - for entry in std::fs::read_dir(account_state_snapshots_dir) - .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? - { - let entry = entry - .map_err(|err| SyncError::SnapshotReadError(account_state_snapshots_dir.into(), err))?; - info!("Reading account file from entry {entry:?}"); - let snapshot_path = entry.path(); - let snapshot_contents = std::fs::read(&snapshot_path) - .map_err(|err| SyncError::SnapshotReadError(snapshot_path.clone(), err))?; - let account_states_snapshot: Vec<(H256, AccountState)> = - RLPDecode::decode(&snapshot_contents) - .map_err(|_| SyncError::SnapshotDecodeError(snapshot_path.clone()))?; - - storage_accounts.accounts_with_storage_root.extend( - account_states_snapshot.iter().filter_map(|(hash, state)| { - (state.storage_root != *EMPTY_TRIE_HASH) - .then_some((*hash, (Some(state.storage_root), Vec::new()))) - }), - ); - - // Collect valid code hashes from current account snapshot - let code_hashes_from_snapshot: Vec = account_states_snapshot - .iter() - .filter_map(|(_, state)| { - (state.code_hash != *EMPTY_KECCACK_HASH).then_some(state.code_hash) - }) - .collect(); - - code_hash_collector.extend(code_hashes_from_snapshot); - code_hash_collector.flush_if_needed().await?; - - info!("Inserting accounts into the state trie"); - - let store_clone = store.clone(); - let current_state_root: Result = - tokio::task::spawn_blocking(move || -> Result { - let mut trie = store_clone.open_direct_state_trie(computed_state_root)?; - - for (account_hash, account) in account_states_snapshot { - trie.insert(account_hash.0.to_vec(), account.encode_to_vec())?; - } - info!("Comitting to disk"); - let current_state_root = trie.hash()?; - Ok(current_state_root) - }) - .await?; - - computed_state_root = current_state_root?; - } - std::fs::remove_dir_all(account_state_snapshots_dir) - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; - info!("computed_state_root {computed_state_root}"); - Ok((computed_state_root, BTreeSet::new())) -} - -#[cfg(not(feature = "rocksdb"))] -async fn insert_storages( - store: Store, - _: BTreeSet, - account_storages_snapshots_dir: &Path, - _: &Path, -) -> Result<(), SyncError> { - use rayon::iter::IntoParallelIterator; - - for entry in std::fs::read_dir(account_storages_snapshots_dir) - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? - { - use crate::utils::AccountsWithStorage; - - let entry = entry.map_err(|err| { - SyncError::SnapshotReadError(account_storages_snapshots_dir.into(), err) - })?; - info!("Reading account storage file from entry {entry:?}"); - - let snapshot_path = entry.path(); - - let snapshot_contents = std::fs::read(&snapshot_path) - .map_err(|err| SyncError::SnapshotReadError(snapshot_path.clone(), err))?; - - #[expect(clippy::type_complexity)] - let account_storages_snapshot: Vec = - RLPDecode::decode(&snapshot_contents) - .map(|all_accounts: Vec<(Vec, Vec<(H256, U256)>)>| { - all_accounts - .into_iter() - .map(|(accounts, storages)| AccountsWithStorage { accounts, storages }) - .collect() - }) - .map_err(|_| SyncError::SnapshotDecodeError(snapshot_path.clone()))?; - - let store_clone = store.clone(); - info!("Starting compute of account_storages_snapshot"); - let storage_trie_node_changes = tokio::task::spawn_blocking(move || { - let store: Store = store_clone; - - account_storages_snapshot - .into_par_iter() - .flat_map(|account_storages| { - let storages: Arc<[_]> = account_storages.storages.into(); - account_storages - .accounts - .into_par_iter() - // FIXME: we probably want to make storages an Arc - .map(move |account| (account, storages.clone())) - }) - .map(|(account, storages)| compute_storage_roots(store.clone(), account, &storages)) - .collect::, SyncError>>() - }) - .await??; - info!("Writing to db"); - - store - .write_storage_trie_nodes_batch(storage_trie_node_changes) - .await?; - } - - std::fs::remove_dir_all(account_storages_snapshots_dir) - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; - - Ok(()) -} - -#[cfg(feature = "rocksdb")] -async fn insert_accounts( - store: Store, - storage_accounts: &mut AccountStorageRoots, - account_state_snapshots_dir: &Path, - datadir: &Path, - code_hash_collector: &mut CodeHashCollector, -) -> Result<(H256, BTreeSet), SyncError> { - use crate::utils::get_rocksdb_temp_accounts_dir; - use ethrex_trie::trie_sorted::trie_from_sorted_accounts_wrap; - - let trie = store.open_direct_state_trie(*EMPTY_TRIE_HASH)?; - let mut db_options = rocksdb::Options::default(); - db_options.create_if_missing(true); - let db = rocksdb::DB::open(&db_options, get_rocksdb_temp_accounts_dir(datadir)) - .map_err(|e| SyncError::AccountTempDBDirNotFound(e.to_string()))?; - let file_paths: Vec = std::fs::read_dir(account_state_snapshots_dir) - .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? - .collect::, _>>() - .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? - .into_iter() - .map(|res| res.path()) - .collect(); - db.ingest_external_file(file_paths) - .map_err(|err| SyncError::RocksDBError(err.into_string()))?; - let iter = db.full_iterator(rocksdb::IteratorMode::Start); - for account in iter { - let account = account.map_err(|err| SyncError::RocksDBError(err.into_string()))?; - let account_state = AccountState::decode(&account.1).map_err(SyncError::Rlp)?; - if account_state.code_hash != *EMPTY_KECCACK_HASH { - code_hash_collector.add(account_state.code_hash); - code_hash_collector.flush_if_needed().await?; - } - } - - let iter = db.full_iterator(rocksdb::IteratorMode::Start); - let compute_state_root = trie_from_sorted_accounts_wrap( - trie.db(), - &mut iter - .map(|k| k.expect("We shouldn't have a rocksdb error here")) // TODO: remove unwrap - .inspect(|(k, v)| { - METRICS - .account_tries_inserted - .fetch_add(1, Ordering::Relaxed); - let account_state = AccountState::decode(v).expect("We should have accounts here"); - if account_state.storage_root != *EMPTY_TRIE_HASH { - storage_accounts.accounts_with_storage_root.insert( - H256::from_slice(k), - (Some(account_state.storage_root), Vec::new()), - ); - } - }) - .map(|(k, v)| (H256::from_slice(&k), v.to_vec())), - ) - .map_err(SyncError::TrieGenerationError)?; - - drop(db); // close db before removing directory - - std::fs::remove_dir_all(account_state_snapshots_dir) - .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)?; - std::fs::remove_dir_all(get_rocksdb_temp_accounts_dir(datadir)) - .map_err(|e| SyncError::AccountTempDBDirNotFound(e.to_string()))?; - - let accounts_with_storage = - BTreeSet::from_iter(storage_accounts.accounts_with_storage_root.keys().copied()); - Ok((compute_state_root, accounts_with_storage)) -} - -#[cfg(feature = "rocksdb")] -async fn insert_storages( - store: Store, - accounts_with_storage: BTreeSet, - account_storages_snapshots_dir: &Path, - datadir: &Path, -) -> Result<(), SyncError> { - use crate::utils::get_rocksdb_temp_storage_dir; - use crossbeam::channel::{bounded, unbounded}; - use ethrex_trie::{ - Nibbles, Node, ThreadPool, - trie_sorted::{BUFFER_COUNT, SIZE_TO_WRITE_DB, trie_from_sorted_accounts}, - }; - use std::thread::scope; - - struct RocksDBIterator<'a> { - iter: rocksdb::DBRawIterator<'a>, - limit: H256, - } - - impl<'a> Iterator for RocksDBIterator<'a> { - type Item = (H256, Vec); - - fn next(&mut self) -> Option { - if !self.iter.valid() { - return None; - } - let return_value = { - let key = self.iter.key(); - let value = self.iter.value(); - match (key, value) { - (Some(key), Some(value)) => { - let hash = H256::from_slice(&key[0..32]); - let key = H256::from_slice(&key[32..]); - let value = value.to_vec(); - if hash != self.limit { - None - } else { - Some((key, value)) - } - } - _ => None, - } - }; - self.iter.next(); - return_value - } - } - - let mut db_options = rocksdb::Options::default(); - db_options.create_if_missing(true); - let db = rocksdb::DB::open(&db_options, get_rocksdb_temp_storage_dir(datadir)) - .map_err(|err: rocksdb::Error| SyncError::RocksDBError(err.into_string()))?; - let file_paths: Vec = std::fs::read_dir(account_storages_snapshots_dir) - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? - .collect::, _>>() - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? - .into_iter() - .map(|res| res.path()) - .collect(); - db.ingest_external_file(file_paths) - .map_err(|err| SyncError::RocksDBError(err.into_string()))?; - let snapshot = db.snapshot(); - - let account_with_storage_and_tries = accounts_with_storage - .into_iter() - .map(|account_hash| { - ( - account_hash, - store - .open_direct_storage_trie(account_hash, *EMPTY_TRIE_HASH) - .expect("Should be able to open trie"), - ) - }) - .collect::>(); - - let (sender, receiver) = unbounded::<()>(); - let mut counter = 0; - let thread_count = std::thread::available_parallelism() - .map(|num| num.into()) - .unwrap_or(8); - - let (buffer_sender, buffer_receiver) = bounded::>(BUFFER_COUNT as usize); - for _ in 0..BUFFER_COUNT { - let _ = buffer_sender.send(Vec::with_capacity(SIZE_TO_WRITE_DB as usize)); - } - - scope(|scope| { - let pool: Arc> = Arc::new(ThreadPool::new(thread_count, scope)); - for (account_hash, trie) in account_with_storage_and_tries.iter() { - let sender = sender.clone(); - let buffer_sender = buffer_sender.clone(); - let buffer_receiver = buffer_receiver.clone(); - if counter >= thread_count - 1 { - let _ = receiver.recv(); - counter -= 1; - } - counter += 1; - let pool_clone = pool.clone(); - let mut iter = snapshot.raw_iterator(); - let task = Box::new(move || { - let mut buffer: [u8; 64] = [0_u8; 64]; - buffer[..32].copy_from_slice(&account_hash.0); - iter.seek(buffer); - let iter = RocksDBIterator { - iter, - limit: *account_hash, - }; - - let _ = trie_from_sorted_accounts( - trie.db(), - &mut iter.inspect(|_| METRICS.storage_leaves_inserted.inc()), - pool_clone, - buffer_sender, - buffer_receiver, - ) - .inspect_err(|err: &TrieGenerationError| { - error!( - "we found an error while inserting the storage trie for the account {account_hash:x}, err {err}" - ); - }) - .map_err(SyncError::TrieGenerationError); - let _ = sender.send(()); - }); - pool.execute(task); - } - }); - - // close db before removing directory - drop(snapshot); - drop(db); - - std::fs::remove_dir_all(account_storages_snapshots_dir) - .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; - std::fs::remove_dir_all(get_rocksdb_temp_storage_dir(datadir)) - .map_err(|e| SyncError::StorageTempDBDirNotFound(e.to_string()))?; - - Ok(()) -} diff --git a/crates/networking/p2p/sync/code_collector.rs b/crates/networking/p2p/sync/code_collector.rs index 0038bc8c935..ced34ff88b1 100644 --- a/crates/networking/p2p/sync/code_collector.rs +++ b/crates/networking/p2p/sync/code_collector.rs @@ -1,4 +1,5 @@ use crate::peer_handler::DumpError; +use crate::snap::constants::CODE_HASH_WRITE_BUFFER_SIZE; use crate::sync::SyncError; use crate::utils::{dump_to_file, get_code_hashes_snapshot_file}; use ethrex_common::H256; @@ -8,9 +9,6 @@ use std::path::PathBuf; use tokio::task::JoinSet; use tracing::error; -/// Size of the buffer to store code hashes before flushing to a file -const CODE_HASH_WRITE_BUFFER_SIZE: usize = 100_000; - /// Manages code hash collection and async file writing pub struct CodeHashCollector { // Buffer to store code hashes diff --git a/crates/networking/p2p/sync/full.rs b/crates/networking/p2p/sync/full.rs new file mode 100644 index 00000000000..b2ccc1f28c5 --- /dev/null +++ b/crates/networking/p2p/sync/full.rs @@ -0,0 +1,297 @@ +//! Full sync implementation +//! +//! This module contains the logic for full synchronization mode where all blocks +//! are fetched via p2p eth requests and executed to rebuild the state. + +use std::cmp::min; +use std::sync::Arc; +use std::time::Duration; + +use ethrex_blockchain::{BatchBlockProcessingFailure, Blockchain, error::ChainError}; +use ethrex_common::{H256, types::Block}; +use ethrex_storage::Store; +use tokio::time::Instant; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, warn}; + +use crate::peer_handler::{BlockRequestOrder, PeerHandler}; +use crate::snap::constants::{MAX_BLOCK_BODIES_TO_REQUEST, MAX_HEADER_FETCH_ATTEMPTS}; + +use super::{EXECUTE_BATCH_SIZE, SyncError}; + +/// Performs full sync cycle - fetches and executes all blocks between current head and sync head +/// +/// # Returns +/// +/// Returns an error if the sync fails at any given step and aborts all active processes +pub async fn sync_cycle_full( + peers: &mut PeerHandler, + blockchain: Arc, + cancel_token: CancellationToken, + mut sync_head: H256, + store: Store, +) -> Result<(), SyncError> { + info!("Syncing to sync_head {:?}", sync_head); + + // Check if the sync_head is a pending block, if so, gather all pending blocks belonging to its chain + let mut pending_blocks = vec![]; + while let Some(block) = store.get_pending_block(sync_head).await? { + if store.is_canonical_sync(block.hash())? { + // Ignore canonical blocks still in pending + break; + } + sync_head = block.header.parent_hash; + pending_blocks.insert(0, block); + } + + // Request all block headers between the sync head and our local chain + // We will begin from the sync head so that we download the latest state first, ensuring we follow the correct chain + // This step is not parallelized + let mut start_block_number; + let mut end_block_number = 0; + let mut headers = vec![]; + let mut single_batch = true; + + let mut attempts = 0; + + // Request and store all block headers from the advertised sync head + loop { + let Some(mut block_headers) = peers + .request_block_headers_from_hash(sync_head, BlockRequestOrder::NewToOld) + .await? + else { + if attempts > MAX_HEADER_FETCH_ATTEMPTS { + warn!("Sync failed to find target block header, aborting"); + return Ok(()); + } + attempts += 1; + tokio::time::sleep(Duration::from_millis(1.1_f64.powf(attempts as f64) as u64)).await; + continue; + }; + debug!("Sync Log 9: Received {} block headers", block_headers.len()); + + let first_header = block_headers.first().ok_or(SyncError::NoBlocks)?; + let last_header = block_headers.last().ok_or(SyncError::NoBlocks)?; + + info!( + "Received {} block headers| First Number: {} Last Number: {}", + block_headers.len(), + first_header.number, + last_header.number, + ); + end_block_number = end_block_number.max(first_header.number); + start_block_number = last_header.number; + + sync_head = last_header.parent_hash; + if store.is_canonical_sync(sync_head)? || sync_head.is_zero() { + // Incoming chain merged with current chain + // Filter out already canonical blocks from batch + let mut first_canon_block = block_headers.len(); + for (index, header) in block_headers.iter().enumerate() { + if store.is_canonical_sync(header.hash())? { + first_canon_block = index; + break; + } + } + block_headers.drain(first_canon_block..block_headers.len()); + if let Some(last_header) = block_headers.last() { + start_block_number = last_header.number; + } + // If the fullsync consists of a single batch of headers we can just keep them in memory instead of writing them to Store + if single_batch { + headers = block_headers.into_iter().rev().collect(); + } else { + store.add_fullsync_batch(block_headers).await?; + } + break; + } + store.add_fullsync_batch(block_headers).await?; + single_batch = false; + } + end_block_number += 1; + start_block_number = start_block_number.max(1); + + // Download block bodies and execute full blocks in batches + for start in (start_block_number..end_block_number).step_by(*EXECUTE_BATCH_SIZE) { + let batch_size = EXECUTE_BATCH_SIZE.min((end_block_number - start) as usize); + let final_batch = end_block_number == start + batch_size as u64; + // Retrieve batch from DB + if !single_batch { + headers = store + .read_fullsync_batch(start, batch_size as u64) + .await? + .into_iter() + .map(|opt| opt.ok_or(SyncError::MissingFullsyncBatch)) + .collect::, SyncError>>()?; + } + let mut blocks = Vec::new(); + // Request block bodies + // Download block bodies + while !headers.is_empty() { + let header_batch = &headers[..min(MAX_BLOCK_BODIES_TO_REQUEST, headers.len())]; + let bodies = peers + .request_block_bodies(header_batch) + .await? + .ok_or(SyncError::BodiesNotFound)?; + debug!("Obtained: {} block bodies", bodies.len()); + let block_batch = headers + .drain(..bodies.len()) + .zip(bodies) + .map(|(header, body)| Block { header, body }); + blocks.extend(block_batch); + } + if !blocks.is_empty() { + // Execute blocks + info!( + "Executing {} blocks for full sync. First block hash: {:#?} Last block hash: {:#?}", + blocks.len(), + blocks.first().ok_or(SyncError::NoBlocks)?.hash(), + blocks.last().ok_or(SyncError::NoBlocks)?.hash() + ); + add_blocks_in_batch( + blockchain.clone(), + cancel_token.clone(), + blocks, + final_batch, + store.clone(), + ) + .await?; + } + } + + // Execute pending blocks + if !pending_blocks.is_empty() { + info!( + "Executing {} blocks for full sync. First block hash: {:#?} Last block hash: {:#?}", + pending_blocks.len(), + pending_blocks.first().ok_or(SyncError::NoBlocks)?.hash(), + pending_blocks.last().ok_or(SyncError::NoBlocks)?.hash() + ); + add_blocks_in_batch( + blockchain.clone(), + cancel_token.clone(), + pending_blocks, + true, + store.clone(), + ) + .await?; + } + + store.clear_fullsync_headers().await?; + Ok(()) +} + +async fn add_blocks_in_batch( + blockchain: Arc, + cancel_token: CancellationToken, + blocks: Vec, + final_batch: bool, + store: Store, +) -> Result<(), SyncError> { + let execution_start = Instant::now(); + // Copy some values for later + let blocks_len = blocks.len(); + let numbers_and_hashes = blocks + .iter() + .map(|b| (b.header.number, b.hash())) + .collect::>(); + let (last_block_number, last_block_hash) = numbers_and_hashes + .last() + .cloned() + .ok_or(SyncError::InvalidRangeReceived)?; + let (first_block_number, first_block_hash) = numbers_and_hashes + .first() + .cloned() + .ok_or(SyncError::InvalidRangeReceived)?; + + let blocks_hashes = blocks.iter().map(|block| block.hash()).collect::>(); + // Run the batch + if let Err((err, batch_failure)) = + add_blocks(blockchain.clone(), blocks, final_batch, cancel_token).await + { + if let Some(batch_failure) = batch_failure { + warn!("Failed to add block during FullSync: {err}"); + // Since running the batch failed we set the failing block and its descendants + // with having an invalid ancestor on the following cases. + if let ChainError::InvalidBlock(_) = err { + let mut block_hashes_with_invalid_ancestor: Vec = vec![]; + if let Some(index) = blocks_hashes + .iter() + .position(|x| x == &batch_failure.failed_block_hash) + { + block_hashes_with_invalid_ancestor = blocks_hashes[index..].to_vec(); + } + + for hash in block_hashes_with_invalid_ancestor { + store + .set_latest_valid_ancestor(hash, batch_failure.last_valid_hash) + .await?; + } + } + } + return Err(err.into()); + } + + store + .forkchoice_update( + numbers_and_hashes, + last_block_number, + last_block_hash, + None, + None, + ) + .await?; + + let execution_time: f64 = execution_start.elapsed().as_millis() as f64 / 1000.0; + let blocks_per_second = blocks_len as f64 / execution_time; + + info!( + "[SYNCING] Executed & stored {} blocks in {:.3} seconds.\n\ + Started at block with hash {} (number {}).\n\ + Finished at block with hash {} (number {}).\n\ + Blocks per second: {:.3}", + blocks_len, + execution_time, + first_block_hash, + first_block_number, + last_block_hash, + last_block_number, + blocks_per_second + ); + Ok(()) +} + +/// Executes the given blocks and stores them +/// If sync_head_found is true, they will be executed one by one +/// If sync_head_found is false, they will be executed in a single batch +async fn add_blocks( + blockchain: Arc, + blocks: Vec, + sync_head_found: bool, + cancel_token: CancellationToken, +) -> Result<(), (ChainError, Option)> { + // If we found the sync head, run the blocks sequentially to store all the blocks's state + if sync_head_found { + tokio::task::spawn_blocking(move || { + let mut last_valid_hash = H256::default(); + for block in blocks { + let block_hash = block.hash(); + blockchain.add_block_pipeline(block).map_err(|e| { + ( + e, + Some(BatchBlockProcessingFailure { + last_valid_hash, + failed_block_hash: block_hash, + }), + ) + })?; + last_valid_hash = block_hash; + } + Ok(()) + }) + .await + .map_err(|e| (ChainError::Custom(e.to_string()), None))? + } else { + blockchain.add_blocks_in_batch(blocks, cancel_token).await + } +} diff --git a/crates/networking/p2p/sync/healing/mod.rs b/crates/networking/p2p/sync/healing/mod.rs new file mode 100644 index 00000000000..3a4981ba5ee --- /dev/null +++ b/crates/networking/p2p/sync/healing/mod.rs @@ -0,0 +1,15 @@ +//! Trie Healing Module +//! +//! Heals state and storage tries during snap sync by downloading +//! missing nodes and reconciling inconsistencies from multi-pivot downloads. + +pub mod state; +pub mod storage; +mod types; + +pub use state::heal_state_trie_wrap; +pub use storage::heal_storage_trie; + +// Re-export shared types for external use +#[allow(unused_imports)] +pub use types::{HealingQueueEntry, StateHealingQueue}; diff --git a/crates/networking/p2p/sync/state_healing.rs b/crates/networking/p2p/sync/healing/state.rs similarity index 88% rename from crates/networking/p2p/sync/state_healing.rs rename to crates/networking/p2p/sync/healing/state.rs index bb03253ea37..3c6e0766440 100644 --- a/crates/networking/p2p/sync/state_healing.rs +++ b/crates/networking/p2p/sync/healing/state.rs @@ -23,27 +23,18 @@ use tracing::{debug, trace}; use crate::{ metrics::{CurrentStepValue, METRICS}, - peer_handler::{PeerHandler, RequestMetadata, RequestStateTrieNodesError}, + peer_handler::{PeerHandler, RequestMetadata}, rlpx::p2p::SUPPORTED_SNAP_CAPABILITIES, - sync::{AccountStorageRoots, code_collector::CodeHashCollector}, + snap::{ + SnapError, + constants::{NODE_BATCH_SIZE, SHOW_PROGRESS_INTERVAL_DURATION}, + request_state_trienodes, + }, + sync::{AccountStorageRoots, SyncError, code_collector::CodeHashCollector}, utils::current_unix_time, }; -/// Max size of a bach to start a storage fetch request in queues -pub const STORAGE_BATCH_SIZE: usize = 300; -/// Max size of a bach to start a node fetch request in queues -pub const NODE_BATCH_SIZE: usize = 500; -/// Pace at which progress is shown via info tracing -pub const SHOW_PROGRESS_INTERVAL_DURATION: Duration = Duration::from_secs(2); - -use super::SyncError; - -#[derive(Debug)] -pub struct MembatchEntryValue { - node: Node, - children_not_in_storage_count: u64, - parent_path: Nibbles, -} +use super::types::{HealingQueueEntry, StateHealingQueue}; pub async fn heal_state_trie_wrap( state_root: H256, @@ -89,7 +80,7 @@ async fn heal_state_trie( mut peers: PeerHandler, staleness_timestamp: u64, global_leafs_healed: &mut u64, - mut membatch: HashMap, + mut healing_queue: StateHealingQueue, storage_accounts: &mut AccountStorageRoots, code_hash_collector: &mut CodeHashCollector, ) -> Result { @@ -112,11 +103,10 @@ async fn heal_state_trie( let mut db_joinset = tokio::task::JoinSet::new(); // channel to send the tasks to the peers - let (task_sender, mut task_receiver) = tokio::sync::mpsc::channel::<( - H256, - Result, RequestStateTrieNodesError>, - Vec, - )>(1000); + let (task_sender, mut task_receiver) = + tokio::sync::mpsc::channel::<(H256, Result, SnapError>, Vec)>( + 1000, + ); // Contains both nodes and their corresponding paths to heal let mut nodes_to_heal = Vec::new(); @@ -148,7 +138,7 @@ async fn heal_state_trie( global_leafs_healed, downloads_rate, paths_to_go = paths.len(), - pending_nodes = membatch.len(), + pending_nodes = healing_queue.len(), heals_per_cycle, "State Healing", ); @@ -254,7 +244,7 @@ async fn heal_state_trie( let peer_table = peers.peer_table.clone(); tokio::spawn(async move { // TODO: check errors to determine whether the current block is stale - let response = PeerHandler::request_state_trienodes( + let response = request_state_trienodes( peer_id, connection, peer_table, @@ -278,7 +268,7 @@ async fn heal_state_trie( batch, nodes, store.clone(), - &mut membatch, + &mut healing_queue, &mut nodes_to_write, ) .inspect_err(|err| { @@ -332,7 +322,7 @@ async fn heal_state_trie( } if is_stale && nodes_to_heal.is_empty() && inflight_tasks == 0 { - debug!("Finisehd inflight tasks"); + debug!("Finished inflight tasks"); db_joinset.join_all().await; break; } @@ -351,30 +341,30 @@ fn heal_state_batch( mut batch: Vec, nodes: Vec, store: Store, - membatch: &mut HashMap, + healing_queue: &mut StateHealingQueue, nodes_to_write: &mut Vec<(Nibbles, Node)>, // TODO: change tuple to struct ) -> Result, SyncError> { let trie = store.open_direct_state_trie(*EMPTY_TRIE_HASH)?; for node in nodes.into_iter() { let path = batch.remove(0); - let (missing_children_count, missing_children) = - node_missing_children(&node, &path.path, trie.db())?; - batch.extend(missing_children); - if missing_children_count == 0 { + let (pending_children_count, pending_children) = + node_pending_children(&node, &path.path, trie.db())?; + batch.extend(pending_children); + if pending_children_count == 0 { commit_node( node, &path.path, &path.parent_path, - membatch, + healing_queue, nodes_to_write, ); } else { - let entry = MembatchEntryValue { + let entry = HealingQueueEntry { node: node.clone(), - children_not_in_storage_count: missing_children_count, + pending_children_count, parent_path: path.parent_path.clone(), }; - membatch.insert(path.path.clone(), entry); + healing_queue.insert(path.path.clone(), entry); } } Ok(batch) @@ -384,7 +374,7 @@ fn commit_node( node: Node, path: &Nibbles, parent_path: &Nibbles, - membatch: &mut HashMap, + healing_queue: &mut StateHealingQueue, nodes_to_write: &mut Vec<(Nibbles, Node)>, ) { nodes_to_write.push((path.clone(), node)); @@ -393,32 +383,32 @@ fn commit_node( return; // Case where we're saving the root } - let mut membatch_entry = membatch.remove(parent_path).unwrap_or_else(|| { + let mut healing_queue_entry = healing_queue.remove(parent_path).unwrap_or_else(|| { panic!("The parent should exist. Parent: {parent_path:?}, path: {path:?}") }); - membatch_entry.children_not_in_storage_count -= 1; - if membatch_entry.children_not_in_storage_count == 0 { + healing_queue_entry.pending_children_count -= 1; + if healing_queue_entry.pending_children_count == 0 { commit_node( - membatch_entry.node, + healing_queue_entry.node, parent_path, - &membatch_entry.parent_path, - membatch, + &healing_queue_entry.parent_path, + healing_queue, nodes_to_write, ); } else { - membatch.insert(parent_path.clone(), membatch_entry); + healing_queue.insert(parent_path.clone(), healing_queue_entry); } } /// Returns the partial paths to the node's children if they are not already part of the trie state -pub fn node_missing_children( +pub fn node_pending_children( node: &Node, path: &Nibbles, trie_state: &dyn TrieDB, -) -> Result<(u64, Vec), TrieError> { +) -> Result<(usize, Vec), TrieError> { let mut paths: Vec = Vec::new(); - let mut missing_children_count = 0_u64; + let mut pending_children_count: usize = 0; match &node { Node::Branch(node) => { for (index, child) in node.choices.iter().enumerate() { @@ -436,7 +426,7 @@ pub fn node_missing_children( continue; } - missing_children_count += 1; + pending_children_count += 1; paths.extend(vec![RequestMetadata { hash: child.compute_hash().finalize(), path: child_path, @@ -457,7 +447,7 @@ pub fn node_missing_children( if validity { return Ok((0, vec![])); } - missing_children_count += 1; + pending_children_count += 1; paths.extend(vec![RequestMetadata { hash: node.child.compute_hash().finalize(), @@ -467,5 +457,5 @@ pub fn node_missing_children( } _ => {} } - Ok((missing_children_count, paths)) + Ok((pending_children_count, paths)) } diff --git a/crates/networking/p2p/sync/storage_healing.rs b/crates/networking/p2p/sync/healing/storage.rs similarity index 89% rename from crates/networking/p2p/sync/storage_healing.rs rename to crates/networking/p2p/sync/healing/storage.rs index 2551831b72e..4882ea01c7e 100644 --- a/crates/networking/p2p/sync/storage_healing.rs +++ b/crates/networking/p2p/sync/healing/storage.rs @@ -1,14 +1,19 @@ use crate::{ metrics::{CurrentStepValue, METRICS}, - peer_handler::{MAX_RESPONSE_BYTES, PeerHandler, RequestStorageTrieNodes}, + peer_handler::PeerHandler, rlpx::{ p2p::SUPPORTED_SNAP_CAPABILITIES, snap::{GetTrieNodes, TrieNodes}, }, - sync::{ - AccountStorageRoots, SyncError, - state_healing::{SHOW_PROGRESS_INTERVAL_DURATION, STORAGE_BATCH_SIZE}, + snap::{ + RequestStorageTrieNodesError, + constants::{ + MAX_IN_FLIGHT_REQUESTS, MAX_RESPONSE_BYTES, SHOW_PROGRESS_INTERVAL_DURATION, + STORAGE_BATCH_SIZE, + }, + request_storage_trienodes, }, + sync::{AccountStorageRoots, SyncError}, utils::current_unix_time, }; @@ -31,8 +36,6 @@ use tokio::{ }; use tracing::{debug, trace}; -const MAX_IN_FLIGHT_REQUESTS: u32 = 77; - /// This struct stores the metadata we need when we request a node #[derive(Debug, Clone)] pub struct NodeResponse { @@ -44,18 +47,18 @@ pub struct NodeResponse { /// This struct stores the metadata we need when we store a node in the memory bank before storing #[derive(Debug, Clone)] -pub struct MembatchEntry { +pub struct StorageHealingQueueEntry { /// What this node is node_response: NodeResponse, /// How many missing children this node has /// if this number is 0, it should be flushed to the db, not stored in memory - missing_children_count: usize, + pending_children_count: usize, } -/// The membatch key represents the account path and the storage path -type MembatchKey = (Nibbles, Nibbles); +/// The healing queue key represents the account path and the storage path +type StorageHealingQueueKey = (Nibbles, Nibbles); -type Membatch = HashMap; +pub type StorageHealingQueue = HashMap; #[derive(Debug, Clone)] pub struct InflightRequest { @@ -74,7 +77,7 @@ pub struct StorageHealer { /// Arc to the db, clone freely store: Store, /// Memory of everything stored - membatch: Membatch, + healing_queue: StorageHealingQueue, /// With this we track how many requests are inflight to our peer /// This allows us to know if one is wildly out of time requests: HashMap, @@ -112,17 +115,17 @@ pub struct NodeRequest { /// We receive a list of the counts that we want to save, we heal by chunks of accounts. /// We assume these accounts are not empty hash tries, but may or may not have their /// Algorithmic rules: -/// - If a nodehash is present in the db, it and all of it's children are present in the db +/// - If a nodehash is present in the db, it and all of its children are present in the db /// - If we are missing a node, we queue to download them. /// - When a node is downloaded: /// - if it has no missing children, we store it in the db -/// - if the node has missing childre, we store it in our membatch, wchich is preserved between calls +/// - if the node has missing children, we store it in our healing_queue, which is preserved between calls pub async fn heal_storage_trie( state_root: H256, storage_accounts: &AccountStorageRoots, peers: &mut PeerHandler, store: Store, - membatch: Membatch, + healing_queue: StorageHealingQueue, staleness_timestamp: u64, global_leafs_healed: &mut u64, ) -> Result { @@ -136,7 +139,7 @@ pub async fn heal_storage_trie( last_update: Instant::now(), download_queue, store, - membatch, + healing_queue, requests: HashMap::new(), staleness_timestamp, state_root, @@ -154,7 +157,7 @@ pub async fn heal_storage_trie( // TODO: think if this is a better way to receiver the data // Not in the state because it's not clonable let mut requests_task_joinset: JoinSet< - Result>>, + Result>>, > = JoinSet::new(); let mut nodes_to_write: HashMap> = HashMap::new(); @@ -162,7 +165,7 @@ pub async fn heal_storage_trie( // channel to send the tasks to the peers let (task_sender, mut task_receiver) = - tokio::sync::mpsc::channel::>(1000); + tokio::sync::mpsc::channel::>(1000); let mut logged_no_free_peers_count = 0; @@ -238,7 +241,7 @@ pub async fn heal_storage_trie( if is_stale { db_joinset.join_all().await; - state.membatch = HashMap::new(); + state.healing_queue = HashMap::new(); return Ok(false); } @@ -286,7 +289,7 @@ pub async fn heal_storage_trie( &mut nodes_from_peer, &mut state.download_queue, &state.store, - &mut state.membatch, + &mut state.healing_queue, &mut state.leafs_healed, global_leafs_healed, &mut state.roots_healed, @@ -295,8 +298,14 @@ pub async fn heal_storage_trie( ) .expect("We shouldn't be getting store errors"); // TODO: if we have a store error we should stop } - Err(RequestStorageTrieNodes::RequestError(id, _err)) => { - let inflight_request = state.requests.remove(&id).expect("request disappeared"); + Err(RequestStorageTrieNodesError { + request_id, + source: _err, + }) => { + let inflight_request = state + .requests + .remove(&request_id) + .expect("request disappeared"); state.failed_downloads += 1; state .download_queue @@ -315,11 +324,11 @@ async fn ask_peers_for_nodes( download_queue: &mut VecDeque, requests: &mut HashMap, requests_task_joinset: &mut JoinSet< - Result>>, + Result>>, >, peers: &mut PeerHandler, state_root: H256, - task_sender: &Sender>, + task_sender: &Sender>, logged_no_free_peers_count: &mut u32, ) { if (requests.len() as u32) < MAX_IN_FLIGHT_REQUESTS && !download_queue.is_empty() { @@ -364,8 +373,7 @@ async fn ask_peers_for_nodes( requests_task_joinset.spawn(async move { let req_id = gtn.id; - let response = - PeerHandler::request_storage_trienodes(peer_id, connection, peer_table, gtn).await; + let response = request_storage_trienodes(peer_id, connection, peer_table, gtn).await; // TODO: add error handling tx.try_send(response).inspect_err( |err| debug!(error=?err, "Failed to send state trie nodes response"), @@ -501,7 +509,7 @@ fn process_node_responses( node_processing_queue: &mut Vec, download_queue: &mut VecDeque, store: &Store, - membatch: &mut Membatch, + healing_queue: &mut StorageHealingQueue, leafs_healed: &mut usize, global_leafs_healed: &mut u64, roots_healed: &mut usize, @@ -520,37 +528,39 @@ fn process_node_responses( node_response.node_request.storage_path.len(), ); - let (missing_children_nibbles, missing_children_count) = - determine_missing_children(&node_response, store).inspect_err(|err| { + let (pending_children_nibbles, pending_children_count) = + determine_pending_children(&node_response, store).inspect_err(|err| { debug!( error=?err, ?node_response, - "Error in determine_missing_children" + "Error in determine_pending_children" ) })?; - if missing_children_count == 0 { + if pending_children_count == 0 { // We flush to the database this node - commit_node(&node_response, membatch, roots_healed, to_write).inspect_err(|err| { - debug!( - error=?err, - ?node_response, - "Error in commit_node" - ) - })?; + commit_node(&node_response, healing_queue, roots_healed, to_write).inspect_err( + |err| { + debug!( + error=?err, + ?node_response, + "Error in commit_node" + ) + }, + )?; } else { let key = ( node_response.node_request.acc_path.clone(), node_response.node_request.storage_path.clone(), ); - membatch.insert( + healing_queue.insert( key, - MembatchEntry { + StorageHealingQueueEntry { node_response: node_response.clone(), - missing_children_count, + pending_children_count, }, ); - download_queue.extend(missing_children_nibbles); + download_queue.extend(pending_children_nibbles); } } @@ -596,7 +606,7 @@ fn get_initial_downloads( /// Returns the full paths to the node's missing children and grandchildren /// and the number of direct missing children -pub fn determine_missing_children( +pub fn determine_pending_children( node_response: &NodeResponse, store: &Store, ) -> Result<(Vec, usize), StoreError> { @@ -673,7 +683,7 @@ pub fn determine_missing_children( fn commit_node( node: &NodeResponse, - membatch: &mut Membatch, + healing_queue: &mut StorageHealingQueue, roots_healed: &mut usize, to_write: &mut HashMap>, ) -> Result<(), StoreError> { @@ -698,21 +708,21 @@ fn commit_node( node.node_request.parent.clone(), ); - let mut parent_entry = membatch + let mut parent_entry = healing_queue .remove(&parent_key) - .expect("We are missing the parent from the membatch!"); + .expect("We are missing the parent from the healing_queue!"); - parent_entry.missing_children_count -= 1; + parent_entry.pending_children_count -= 1; - if parent_entry.missing_children_count == 0 { + if parent_entry.pending_children_count == 0 { commit_node( &parent_entry.node_response, - membatch, + healing_queue, roots_healed, to_write, ) } else { - membatch.insert(parent_key, parent_entry); + healing_queue.insert(parent_key, parent_entry); Ok(()) } } diff --git a/crates/networking/p2p/sync/healing/types.rs b/crates/networking/p2p/sync/healing/types.rs new file mode 100644 index 00000000000..6920e479505 --- /dev/null +++ b/crates/networking/p2p/sync/healing/types.rs @@ -0,0 +1,14 @@ +use std::collections::HashMap; + +use ethrex_trie::{Nibbles, Node}; + +/// Entry in the healing queue tracking nodes waiting for children +#[derive(Debug, Clone)] +pub struct HealingQueueEntry { + pub node: Node, + pub pending_children_count: usize, + pub parent_path: Nibbles, +} + +/// Type alias for state healing queue +pub type StateHealingQueue = HashMap; diff --git a/crates/networking/p2p/sync/snap_sync.rs b/crates/networking/p2p/sync/snap_sync.rs new file mode 100644 index 00000000000..5d47db58f89 --- /dev/null +++ b/crates/networking/p2p/sync/snap_sync.rs @@ -0,0 +1,1140 @@ +//! Snap sync implementation +//! +//! This module contains the logic for snap synchronization mode where state is +//! fetched via snap p2p requests while blocks and receipts are fetched in parallel. + +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::path::Path; +#[cfg(feature = "rocksdb")] +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::{Duration, SystemTime}; + +use ethrex_blockchain::Blockchain; +use ethrex_common::types::{AccountState, BlockHeader, Code}; +use ethrex_common::{ + H256, + constants::{EMPTY_KECCACK_HASH, EMPTY_TRIE_HASH}, +}; +use ethrex_rlp::decode::RLPDecode; +use ethrex_storage::Store; +#[cfg(feature = "rocksdb")] +use ethrex_trie::Trie; +use rayon::iter::{ParallelBridge, ParallelIterator}; +use tracing::{debug, error, info, warn}; + +use crate::metrics::{CurrentStepValue, METRICS}; +use crate::peer_handler::PeerHandler; +use crate::rlpx::p2p::SUPPORTED_ETH_CAPABILITIES; +use crate::snap::{ + constants::{ + BYTECODE_CHUNK_SIZE, MAX_HEADER_FETCH_ATTEMPTS, MIN_FULL_BLOCKS, MISSING_SLOTS_PERCENTAGE, + SECONDS_PER_BLOCK, SNAP_LIMIT, + }, + request_account_range, request_bytecodes, request_storage_ranges, +}; +use crate::sync::code_collector::CodeHashCollector; +use crate::sync::healing::{heal_state_trie_wrap, heal_storage_trie}; +use crate::utils::{ + current_unix_time, get_account_state_snapshots_dir, get_account_storages_snapshots_dir, + get_code_hashes_snapshots_dir, +}; + +use super::{AccountStorageRoots, SyncError}; + +#[cfg(not(feature = "rocksdb"))] +use ethrex_common::U256; +#[cfg(not(feature = "rocksdb"))] +use ethrex_rlp::encode::RLPEncode; + +/// Persisted State during the Block Sync phase for SnapSync +#[derive(Clone)] +pub struct SnapBlockSyncState { + pub block_hashes: Vec, + store: Store, +} + +impl SnapBlockSyncState { + pub fn new(store: Store) -> Self { + Self { + block_hashes: Vec::new(), + store, + } + } + + /// Obtain the current head from where to start or resume block sync + pub async fn get_current_head(&self) -> Result { + if let Some(head) = self.store.get_header_download_checkpoint().await? { + Ok(head) + } else { + self.store + .get_latest_canonical_block_hash() + .await? + .ok_or(SyncError::NoLatestCanonical) + } + } + + /// Stores incoming headers to the Store and saves their hashes + pub async fn process_incoming_headers( + &mut self, + block_headers: impl Iterator, + ) -> Result<(), SyncError> { + let mut block_headers_vec = Vec::with_capacity(block_headers.size_hint().1.unwrap_or(0)); + let mut block_hashes = Vec::with_capacity(block_headers.size_hint().1.unwrap_or(0)); + for header in block_headers { + block_hashes.push(header.hash()); + block_headers_vec.push(header); + } + self.store + .set_header_download_checkpoint( + *block_hashes.last().ok_or(SyncError::InvalidRangeReceived)?, + ) + .await?; + self.block_hashes.extend_from_slice(&block_hashes); + self.store.add_block_headers(block_headers_vec).await?; + Ok(()) + } +} + +/// Performs snap sync cycle - fetches state via snap protocol while downloading blocks in parallel +pub async fn sync_cycle_snap( + peers: &mut PeerHandler, + blockchain: Arc, + snap_enabled: &std::sync::atomic::AtomicBool, + sync_head: H256, + store: Store, + datadir: &Path, +) -> Result<(), SyncError> { + // Request all block headers between the current head and the sync head + // We will begin from the current head so that we download the earliest state first + // This step is not parallelized + let mut block_sync_state = SnapBlockSyncState::new(store.clone()); + // Check if we have some blocks downloaded from a previous sync attempt + // This applies only to snap sync—full sync always starts fetching headers + // from the canonical block, which updates as new block headers are fetched. + let mut current_head = block_sync_state.get_current_head().await?; + let mut current_head_number = store + .get_block_number(current_head) + .await? + .ok_or(SyncError::BlockNumber(current_head))?; + info!( + "Syncing from current head {:?} to sync_head {:?}", + current_head, sync_head + ); + let pending_block = match store.get_pending_block(sync_head).await { + Ok(res) => res, + Err(e) => return Err(e.into()), + }; + + let mut attempts = 0; + + loop { + debug!("Requesting Block Headers from {current_head}"); + + let Some(mut block_headers) = peers + .request_block_headers(current_head_number, sync_head) + .await? + else { + if attempts > MAX_HEADER_FETCH_ATTEMPTS { + warn!("Sync failed to find target block header, aborting"); + return Ok(()); + } + attempts += 1; + tokio::time::sleep(Duration::from_millis(1.1_f64.powf(attempts as f64) as u64)).await; + continue; + }; + + debug!("Sync Log 1: In snap sync"); + debug!( + "Sync Log 2: State block hashes len {}", + block_sync_state.block_hashes.len() + ); + + let (first_block_hash, first_block_number, first_block_parent_hash) = + match block_headers.first() { + Some(header) => (header.hash(), header.number, header.parent_hash), + None => continue, + }; + let (last_block_hash, last_block_number) = match block_headers.last() { + Some(header) => (header.hash(), header.number), + None => continue, + }; + // TODO(#2126): This is just a temporary solution to avoid a bug where the sync would get stuck + // on a loop when the target head is not found, i.e. on a reorg with a side-chain. + if first_block_hash == last_block_hash + && first_block_hash == current_head + && current_head != sync_head + { + // There is no path to the sync head this goes back until it find a common ancerstor + warn!("Sync failed to find target block header, going back to the previous parent"); + current_head = first_block_parent_hash; + continue; + } + + debug!( + "Received {} block headers| First Number: {} Last Number: {}", + block_headers.len(), + first_block_number, + last_block_number + ); + + // If we have a pending block from new_payload request + // attach it to the end if it matches the parent_hash of the latest received header + if let Some(ref block) = pending_block + && block.header.parent_hash == last_block_hash + { + block_headers.push(block.header.clone()); + } + + // Filter out everything after the sync_head + let mut sync_head_found = false; + if let Some(index) = block_headers + .iter() + .position(|header| header.hash() == sync_head) + { + sync_head_found = true; + block_headers.drain(index + 1..); + } + + // Update current fetch head + current_head = last_block_hash; + current_head_number = last_block_number; + + // If the sync head is not 0 we search to fullsync + let head_found = sync_head_found && store.get_latest_block_number().await? > 0; + // Or the head is very close to 0 + let head_close_to_0 = last_block_number < MIN_FULL_BLOCKS; + + if head_found || head_close_to_0 { + // Too few blocks for a snap sync, switching to full sync + info!("Sync head is found, switching to FullSync"); + snap_enabled.store(false, Ordering::Relaxed); + return super::full::sync_cycle_full( + peers, + blockchain, + tokio_util::sync::CancellationToken::new(), + sync_head, + store.clone(), + ) + .await; + } + + // Discard the first header as we already have it + if block_headers.len() > 1 { + let block_headers_iter = block_headers.into_iter().skip(1); + + block_sync_state + .process_incoming_headers(block_headers_iter) + .await?; + } + + if sync_head_found { + break; + }; + } + + snap_sync(peers, &store, &mut block_sync_state, datadir).await?; + + store.clear_snap_state().await?; + snap_enabled.store(false, Ordering::Relaxed); + + Ok(()) +} + +/// Main snap sync logic - downloads state via snap protocol +pub async fn snap_sync( + peers: &mut PeerHandler, + store: &Store, + block_sync_state: &mut SnapBlockSyncState, + datadir: &Path, +) -> Result<(), SyncError> { + // snap-sync: launch tasks to fetch blocks and state in parallel + // - Fetch each block's body and its receipt via eth p2p requests + // - Fetch the pivot block's state via snap p2p requests + // - Execute blocks after the pivot (like in full-sync) + let pivot_hash = block_sync_state + .block_hashes + .last() + .ok_or(SyncError::NoBlockHeaders)?; + let mut pivot_header = store + .get_block_header_by_hash(*pivot_hash)? + .ok_or(SyncError::CorruptDB)?; + + while block_is_stale(&pivot_header) { + pivot_header = update_pivot( + pivot_header.number, + pivot_header.timestamp, + peers, + block_sync_state, + ) + .await?; + } + debug!( + "Selected block {} as pivot for snap sync", + pivot_header.number + ); + + let state_root = pivot_header.state_root; + let account_state_snapshots_dir = get_account_state_snapshots_dir(datadir); + let account_storages_snapshots_dir = get_account_storages_snapshots_dir(datadir); + + let code_hashes_snapshot_dir = get_code_hashes_snapshots_dir(datadir); + std::fs::create_dir_all(&code_hashes_snapshot_dir).map_err(|_| SyncError::CorruptPath)?; + + // Create collector to store code hashes in files + let mut code_hash_collector: CodeHashCollector = + CodeHashCollector::new(code_hashes_snapshot_dir.clone()); + + let mut storage_accounts = AccountStorageRoots::default(); + if !std::env::var("SKIP_START_SNAP_SYNC").is_ok_and(|var| !var.is_empty()) { + // We start by downloading all of the leafs of the trie of accounts + // The function request_account_range writes the leafs into files in + // account_state_snapshots_dir + + info!("Starting to download account ranges from peers"); + request_account_range( + peers, + H256::zero(), + H256::repeat_byte(0xff), + account_state_snapshots_dir.as_ref(), + &mut pivot_header, + block_sync_state, + ) + .await?; + info!("Finish downloading account ranges from peers"); + + *METRICS.account_tries_insert_start_time.lock().await = Some(SystemTime::now()); + METRICS + .current_step + .set(CurrentStepValue::InsertingAccountRanges); + // We read the account leafs from the files in account_state_snapshots_dir, write it into + // the trie to compute the nodes and stores the accounts with storages for later use + + // Variable `accounts_with_storage` unused if not in rocksdb + #[allow(unused_variables)] + let (computed_state_root, accounts_with_storage) = insert_accounts( + store.clone(), + &mut storage_accounts, + &account_state_snapshots_dir, + datadir, + &mut code_hash_collector, + ) + .await?; + info!( + "Finished inserting account ranges, total storage accounts: {}", + storage_accounts.accounts_with_storage_root.len() + ); + *METRICS.account_tries_insert_end_time.lock().await = Some(SystemTime::now()); + + info!("Original state root: {state_root:?}"); + info!("Computed state root after request_account_rages: {computed_state_root:?}"); + + *METRICS.storage_tries_download_start_time.lock().await = Some(SystemTime::now()); + // We start downloading the storage leafs. To do so, we need to be sure that the storage root + // is correct. To do so, we always heal the state trie before requesting storage rates + let mut chunk_index = 0_u64; + let mut state_leafs_healed = 0_u64; + let mut storage_range_request_attempts = 0; + loop { + while block_is_stale(&pivot_header) { + pivot_header = update_pivot( + pivot_header.number, + pivot_header.timestamp, + peers, + block_sync_state, + ) + .await?; + } + // heal_state_trie_wrap returns false if we ran out of time before fully healing the trie + // We just need to update the pivot and start again + if !heal_state_trie_wrap( + pivot_header.state_root, + store.clone(), + peers, + calculate_staleness_timestamp(pivot_header.timestamp), + &mut state_leafs_healed, + &mut storage_accounts, + &mut code_hash_collector, + ) + .await? + { + continue; + }; + + info!( + "Started request_storage_ranges with {} accounts with storage root unchanged", + storage_accounts.accounts_with_storage_root.len() + ); + storage_range_request_attempts += 1; + if storage_range_request_attempts < 5 { + chunk_index = request_storage_ranges( + peers, + &mut storage_accounts, + account_storages_snapshots_dir.as_ref(), + chunk_index, + &mut pivot_header, + store.clone(), + ) + .await?; + } else { + for (acc_hash, (maybe_root, old_intervals)) in + storage_accounts.accounts_with_storage_root.iter() + { + // When we fall into this case what happened is there are certain accounts for which + // the storage root went back to a previous value we already had, and thus could not download + // their storage leaves because we were using an old value for their storage root. + // The fallback is to ensure we mark it for storage healing. + storage_accounts.healed_accounts.insert(*acc_hash); + debug!( + "We couldn't download these accounts on request_storage_ranges. Falling back to storage healing for it. + Account hash: {:x?}, {:x?}. Number of intervals {}", + acc_hash, + maybe_root, + old_intervals.len() + ); + } + + warn!( + "Storage could not be downloaded after multiple attempts. Marking for healing. + This could impact snap sync time (healing may take a while)." + ); + + storage_accounts.accounts_with_storage_root.clear(); + } + + info!( + "Ended request_storage_ranges with {} accounts with storage root unchanged and not downloaded yet and with {} big/healed accounts", + storage_accounts.accounts_with_storage_root.len(), + // These accounts are marked as heals if they're a big account. This is + // because we don't know if the storage root is still valid + storage_accounts.healed_accounts.len(), + ); + if !block_is_stale(&pivot_header) { + break; + } + info!("We stopped because of staleness, restarting loop"); + } + info!("Finished request_storage_ranges"); + *METRICS.storage_tries_download_end_time.lock().await = Some(SystemTime::now()); + + *METRICS.storage_tries_insert_start_time.lock().await = Some(SystemTime::now()); + METRICS + .current_step + .set(CurrentStepValue::InsertingStorageRanges); + let account_storages_snapshots_dir = get_account_storages_snapshots_dir(datadir); + + insert_storages( + store.clone(), + accounts_with_storage, + &account_storages_snapshots_dir, + datadir, + ) + .await?; + + *METRICS.storage_tries_insert_end_time.lock().await = Some(SystemTime::now()); + + info!("Finished storing storage tries"); + } + + *METRICS.heal_start_time.lock().await = Some(SystemTime::now()); + info!("Starting Healing Process"); + let mut global_state_leafs_healed: u64 = 0; + let mut global_storage_leafs_healed: u64 = 0; + let mut healing_done = false; + while !healing_done { + // This if is an edge case for the skip snap sync scenario + if block_is_stale(&pivot_header) { + pivot_header = update_pivot( + pivot_header.number, + pivot_header.timestamp, + peers, + block_sync_state, + ) + .await?; + } + healing_done = heal_state_trie_wrap( + pivot_header.state_root, + store.clone(), + peers, + calculate_staleness_timestamp(pivot_header.timestamp), + &mut global_state_leafs_healed, + &mut storage_accounts, + &mut code_hash_collector, + ) + .await?; + if !healing_done { + continue; + } + healing_done = heal_storage_trie( + pivot_header.state_root, + &storage_accounts, + peers, + store.clone(), + HashMap::new(), + calculate_staleness_timestamp(pivot_header.timestamp), + &mut global_storage_leafs_healed, + ) + .await?; + } + *METRICS.heal_end_time.lock().await = Some(SystemTime::now()); + + store.generate_flatkeyvalue()?; + + debug_assert!(validate_state_root(store.clone(), pivot_header.state_root).await); + debug_assert!(validate_storage_root(store.clone(), pivot_header.state_root).await); + + info!("Finished healing"); + + // Finish code hash collection + code_hash_collector.finish().await?; + + *METRICS.bytecode_download_start_time.lock().await = Some(SystemTime::now()); + + let code_hashes_dir = get_code_hashes_snapshots_dir(datadir); + let mut seen_code_hashes = HashSet::new(); + let mut code_hashes_to_download = Vec::new(); + + info!("Starting download code hashes from peers"); + for entry in std::fs::read_dir(&code_hashes_dir) + .map_err(|_| SyncError::CodeHashesSnapshotsDirNotFound)? + { + let entry = entry.map_err(|_| SyncError::CorruptPath)?; + let snapshot_contents = std::fs::read(entry.path()) + .map_err(|err| SyncError::SnapshotReadError(entry.path(), err))?; + let code_hashes: Vec = RLPDecode::decode(&snapshot_contents) + .map_err(|_| SyncError::CodeHashesSnapshotDecodeError(entry.path()))?; + + for hash in code_hashes { + // If we haven't seen the code hash yet, add it to the list of hashes to download + if seen_code_hashes.insert(hash) { + code_hashes_to_download.push(hash); + + if code_hashes_to_download.len() >= BYTECODE_CHUNK_SIZE { + info!( + "Starting bytecode download of {} hashes", + code_hashes_to_download.len() + ); + let bytecodes = request_bytecodes(peers, &code_hashes_to_download) + .await? + .ok_or(SyncError::BytecodesNotFound)?; + + store + .write_account_code_batch( + code_hashes_to_download + .drain(..) + .zip(bytecodes) + // SAFETY: hash already checked by the download worker + .map(|(hash, code)| { + (hash, Code::from_bytecode_unchecked(code, hash)) + }) + .collect(), + ) + .await?; + } + } + } + } + + // Download remaining bytecodes if any + if !code_hashes_to_download.is_empty() { + let bytecodes = request_bytecodes(peers, &code_hashes_to_download) + .await? + .ok_or(SyncError::BytecodesNotFound)?; + store + .write_account_code_batch( + code_hashes_to_download + .drain(..) + .zip(bytecodes) + // SAFETY: hash already checked by the download worker + .map(|(hash, code)| (hash, Code::from_bytecode_unchecked(code, hash))) + .collect(), + ) + .await?; + } + + std::fs::remove_dir_all(code_hashes_dir) + .map_err(|_| SyncError::CodeHashesSnapshotsDirNotFound)?; + + *METRICS.bytecode_download_end_time.lock().await = Some(SystemTime::now()); + + debug_assert!(validate_bytecodes(store.clone(), pivot_header.state_root)); + + store_block_bodies(vec![pivot_header.clone()], peers.clone(), store.clone()).await?; + + let block = store + .get_block_by_hash(pivot_header.hash()) + .await? + .ok_or(SyncError::CorruptDB)?; + + store.add_block(block).await?; + + let numbers_and_hashes = block_sync_state + .block_hashes + .iter() + .rev() + .enumerate() + .map(|(i, hash)| (pivot_header.number - i as u64, *hash)) + .collect::>(); + + store + .forkchoice_update( + numbers_and_hashes, + pivot_header.number, + pivot_header.hash(), + None, + None, + ) + .await?; + Ok(()) +} + +/// Fetches all block bodies for the given block headers via p2p and stores them +pub async fn store_block_bodies( + mut block_headers: Vec, + mut peers: PeerHandler, + store: Store, +) -> Result<(), SyncError> { + loop { + debug!("Requesting Block Bodies "); + if let Some(block_bodies) = peers.request_block_bodies(&block_headers).await? { + debug!(" Received {} Block Bodies", block_bodies.len()); + // Track which bodies we have already fetched + let current_block_headers = block_headers.drain(..block_bodies.len()); + // Add bodies to storage + for (hash, body) in current_block_headers + .map(|h| h.hash()) + .zip(block_bodies.into_iter()) + { + store.add_block_body(hash, body).await?; + } + + // Check if we need to ask for another batch + if block_headers.is_empty() { + break; + } + } + } + Ok(()) +} + +pub async fn update_pivot( + block_number: u64, + block_timestamp: u64, + peers: &mut PeerHandler, + block_sync_state: &mut SnapBlockSyncState, +) -> Result { + // We multiply the estimation by 0.9 in order to account for missing slots (~9% in tesnets) + let new_pivot_block_number = block_number + + ((current_unix_time().saturating_sub(block_timestamp) / SECONDS_PER_BLOCK) as f64 + * MISSING_SLOTS_PERCENTAGE) as u64; + debug!( + "Current pivot is stale (number: {}, timestamp: {}). New pivot number: {}", + block_number, block_timestamp, new_pivot_block_number + ); + loop { + let Some((peer_id, mut connection)) = peers + .peer_table + .get_best_peer(&SUPPORTED_ETH_CAPABILITIES) + .await? + else { + // When we come here, we may be waiting for requests to timeout. + // Because we're waiting for a timeout, we sleep so the rest of the code + // can get to them + debug!("We tried to get peers during update_pivot, but we found no free peers"); + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + }; + + let peer_score = peers.peer_table.get_score(&peer_id).await?; + info!( + "Trying to update pivot to {new_pivot_block_number} with peer {peer_id} (score: {peer_score})" + ); + let Some(pivot) = peers + .get_block_header(peer_id, &mut connection, new_pivot_block_number) + .await + .map_err(SyncError::PeerHandler)? + else { + // Penalize peer + peers.peer_table.record_failure(&peer_id).await?; + let peer_score = peers.peer_table.get_score(&peer_id).await?; + warn!( + "Received None pivot from peer {peer_id} (score after penalizing: {peer_score}). Retrying" + ); + continue; + }; + + // Reward peer + peers.peer_table.record_success(&peer_id).await?; + info!("Succesfully updated pivot"); + let block_headers = peers + .request_block_headers(block_number + 1, pivot.hash()) + .await? + .ok_or(SyncError::NoBlockHeaders)?; + block_sync_state + .process_incoming_headers(block_headers.into_iter()) + .await?; + *METRICS.sync_head_hash.lock().await = pivot.hash(); + return Ok(pivot.clone()); + } +} + +pub fn block_is_stale(block_header: &BlockHeader) -> bool { + calculate_staleness_timestamp(block_header.timestamp) < current_unix_time() +} + +pub fn calculate_staleness_timestamp(timestamp: u64) -> u64 { + timestamp + (SNAP_LIMIT as u64 * 12) +} + +pub async fn validate_state_root(store: Store, state_root: H256) -> bool { + info!("Starting validate_state_root"); + let validated = tokio::task::spawn_blocking(move || { + store + .open_locked_state_trie(state_root) + .expect("couldn't open trie") + .validate() + }) + .await + .expect("We should be able to create threads"); + + if validated.is_ok() { + info!("Succesfully validated tree, {state_root} found"); + } else { + error!("We have failed the validation of the state tree"); + std::process::exit(1); + } + validated.is_ok() +} + +pub async fn validate_storage_root(store: Store, state_root: H256) -> bool { + info!("Starting validate_storage_root"); + let is_valid = tokio::task::spawn_blocking(move || { + store + .iter_accounts(state_root) + .expect("couldn't iterate accounts") + .par_bridge() + .try_for_each(|(hashed_address, account_state)| { + let store_clone = store.clone(); + store_clone + .open_locked_storage_trie( + hashed_address, + state_root, + account_state.storage_root, + ) + .expect("couldn't open storage trie") + .validate() + }) + }) + .await + .expect("We should be able to create threads"); + info!("Finished validate_storage_root"); + if is_valid.is_err() { + std::process::exit(1); + } + is_valid.is_ok() +} + +pub fn validate_bytecodes(store: Store, state_root: H256) -> bool { + info!("Starting validate_bytecodes"); + let mut is_valid = true; + for (account_hash, account_state) in store + .iter_accounts(state_root) + .expect("we couldn't iterate over accounts") + { + if account_state.code_hash != *EMPTY_KECCACK_HASH + && !store + .get_account_code(account_state.code_hash) + .is_ok_and(|code| code.is_some()) + { + error!( + "Missing code hash {:x} for account {:x}", + account_state.code_hash, account_hash + ); + is_valid = false + } + } + if !is_valid { + std::process::exit(1); + } + is_valid +} + +// ============================================================================ +// Account and Storage Insertion (non-rocksdb) +// ============================================================================ + +#[cfg(not(feature = "rocksdb"))] +type StorageRoots = (H256, Vec<(ethrex_trie::Nibbles, Vec)>); + +#[cfg(not(feature = "rocksdb"))] +fn compute_storage_roots( + store: Store, + account_hash: H256, + key_value_pairs: &[(H256, U256)], +) -> Result { + use ethrex_trie::{Nibbles, Node}; + + let storage_trie = store.open_direct_storage_trie(account_hash, *EMPTY_TRIE_HASH)?; + let trie_hash = match storage_trie.db().get(Nibbles::default())? { + Some(noderlp) => Node::decode(&noderlp)?.compute_hash().finalize(), + None => *EMPTY_TRIE_HASH, + }; + let mut storage_trie = store.open_direct_storage_trie(account_hash, trie_hash)?; + + for (hashed_key, value) in key_value_pairs { + if let Err(err) = storage_trie.insert(hashed_key.0.to_vec(), value.encode_to_vec()) { + warn!( + "Failed to insert hashed key {hashed_key:?} in account hash: {account_hash:?}, err={err:?}" + ); + }; + METRICS.storage_leaves_inserted.inc(); + } + + let (_, changes) = storage_trie.collect_changes_since_last_hash(); + + Ok((account_hash, changes)) +} + +#[cfg(not(feature = "rocksdb"))] +async fn insert_accounts( + store: Store, + storage_accounts: &mut AccountStorageRoots, + account_state_snapshots_dir: &Path, + _: &Path, + code_hash_collector: &mut CodeHashCollector, +) -> Result<(H256, BTreeSet), SyncError> { + let mut computed_state_root = *EMPTY_TRIE_HASH; + for entry in std::fs::read_dir(account_state_snapshots_dir) + .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? + { + let entry = entry + .map_err(|err| SyncError::SnapshotReadError(account_state_snapshots_dir.into(), err))?; + info!("Reading account file from entry {entry:?}"); + let snapshot_path = entry.path(); + let snapshot_contents = std::fs::read(&snapshot_path) + .map_err(|err| SyncError::SnapshotReadError(snapshot_path.clone(), err))?; + let account_states_snapshot: Vec<(H256, AccountState)> = + RLPDecode::decode(&snapshot_contents) + .map_err(|_| SyncError::SnapshotDecodeError(snapshot_path.clone()))?; + + storage_accounts.accounts_with_storage_root.extend( + account_states_snapshot.iter().filter_map(|(hash, state)| { + (state.storage_root != *EMPTY_TRIE_HASH) + .then_some((*hash, (Some(state.storage_root), Vec::new()))) + }), + ); + + // Collect valid code hashes from current account snapshot + let code_hashes_from_snapshot: Vec = account_states_snapshot + .iter() + .filter_map(|(_, state)| { + (state.code_hash != *EMPTY_KECCACK_HASH).then_some(state.code_hash) + }) + .collect(); + + code_hash_collector.extend(code_hashes_from_snapshot); + code_hash_collector.flush_if_needed().await?; + + info!("Inserting accounts into the state trie"); + + let store_clone = store.clone(); + let current_state_root: Result = + tokio::task::spawn_blocking(move || -> Result { + let mut trie = store_clone.open_direct_state_trie(computed_state_root)?; + + for (account_hash, account) in account_states_snapshot { + trie.insert(account_hash.0.to_vec(), account.encode_to_vec())?; + } + info!("Comitting to disk"); + let current_state_root = trie.hash()?; + Ok(current_state_root) + }) + .await?; + + computed_state_root = current_state_root?; + } + std::fs::remove_dir_all(account_state_snapshots_dir) + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; + info!("computed_state_root {computed_state_root}"); + Ok((computed_state_root, BTreeSet::new())) +} + +#[cfg(not(feature = "rocksdb"))] +async fn insert_storages( + store: Store, + _: BTreeSet, + account_storages_snapshots_dir: &Path, + _: &Path, +) -> Result<(), SyncError> { + use rayon::iter::IntoParallelIterator; + + for entry in std::fs::read_dir(account_storages_snapshots_dir) + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? + { + use crate::utils::AccountsWithStorage; + + let entry = entry.map_err(|err| { + SyncError::SnapshotReadError(account_storages_snapshots_dir.into(), err) + })?; + info!("Reading account storage file from entry {entry:?}"); + + let snapshot_path = entry.path(); + + let snapshot_contents = std::fs::read(&snapshot_path) + .map_err(|err| SyncError::SnapshotReadError(snapshot_path.clone(), err))?; + + #[expect(clippy::type_complexity)] + let account_storages_snapshot: Vec = + RLPDecode::decode(&snapshot_contents) + .map(|all_accounts: Vec<(Vec, Vec<(H256, U256)>)>| { + all_accounts + .into_iter() + .map(|(accounts, storages)| AccountsWithStorage { accounts, storages }) + .collect() + }) + .map_err(|_| SyncError::SnapshotDecodeError(snapshot_path.clone()))?; + + let store_clone = store.clone(); + info!("Starting compute of account_storages_snapshot"); + let storage_trie_node_changes = tokio::task::spawn_blocking(move || { + let store: Store = store_clone; + + account_storages_snapshot + .into_par_iter() + .flat_map(|account_storages| { + let storages: Arc<[_]> = account_storages.storages.into(); + account_storages + .accounts + .into_par_iter() + // FIXME: we probably want to make storages an Arc + .map(move |account| (account, storages.clone())) + }) + .map(|(account, storages)| compute_storage_roots(store.clone(), account, &storages)) + .collect::, SyncError>>() + }) + .await??; + info!("Writing to db"); + + store + .write_storage_trie_nodes_batch(storage_trie_node_changes) + .await?; + } + + std::fs::remove_dir_all(account_storages_snapshots_dir) + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; + + Ok(()) +} + +// ============================================================================ +// Account and Storage Insertion (rocksdb) +// ============================================================================ + +#[cfg(feature = "rocksdb")] +async fn insert_accounts( + store: Store, + storage_accounts: &mut AccountStorageRoots, + account_state_snapshots_dir: &Path, + datadir: &Path, + code_hash_collector: &mut CodeHashCollector, +) -> Result<(H256, BTreeSet), SyncError> { + use crate::utils::get_rocksdb_temp_accounts_dir; + use ethrex_trie::trie_sorted::trie_from_sorted_accounts_wrap; + + let trie = store.open_direct_state_trie(*EMPTY_TRIE_HASH)?; + let mut db_options = rocksdb::Options::default(); + db_options.create_if_missing(true); + let db = rocksdb::DB::open(&db_options, get_rocksdb_temp_accounts_dir(datadir)) + .map_err(|e| SyncError::AccountTempDBDirNotFound(e.to_string()))?; + let file_paths: Vec = std::fs::read_dir(account_state_snapshots_dir) + .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? + .collect::, _>>() + .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)? + .into_iter() + .map(|res| res.path()) + .collect(); + db.ingest_external_file(file_paths) + .map_err(|err| SyncError::RocksDBError(err.into_string()))?; + let iter = db.full_iterator(rocksdb::IteratorMode::Start); + for account in iter { + let account = account.map_err(|err| SyncError::RocksDBError(err.into_string()))?; + let account_state = AccountState::decode(&account.1).map_err(SyncError::Rlp)?; + if account_state.code_hash != *EMPTY_KECCACK_HASH { + code_hash_collector.add(account_state.code_hash); + code_hash_collector.flush_if_needed().await?; + } + } + + let iter = db.full_iterator(rocksdb::IteratorMode::Start); + let compute_state_root = trie_from_sorted_accounts_wrap( + trie.db(), + &mut iter + .map(|k| k.expect("We shouldn't have a rocksdb error here")) // TODO: remove unwrap + .inspect(|(k, v)| { + METRICS + .account_tries_inserted + .fetch_add(1, Ordering::Relaxed); + let account_state = AccountState::decode(v).expect("We should have accounts here"); + if account_state.storage_root != *EMPTY_TRIE_HASH { + storage_accounts.accounts_with_storage_root.insert( + H256::from_slice(k), + (Some(account_state.storage_root), Vec::new()), + ); + } + }) + .map(|(k, v)| (H256::from_slice(&k), v.to_vec())), + ) + .map_err(SyncError::TrieGenerationError)?; + + drop(db); // close db before removing directory + + std::fs::remove_dir_all(account_state_snapshots_dir) + .map_err(|_| SyncError::AccountStateSnapshotsDirNotFound)?; + std::fs::remove_dir_all(get_rocksdb_temp_accounts_dir(datadir)) + .map_err(|e| SyncError::AccountTempDBDirNotFound(e.to_string()))?; + + let accounts_with_storage = + BTreeSet::from_iter(storage_accounts.accounts_with_storage_root.keys().copied()); + Ok((compute_state_root, accounts_with_storage)) +} + +#[cfg(feature = "rocksdb")] +async fn insert_storages( + store: Store, + accounts_with_storage: BTreeSet, + account_storages_snapshots_dir: &Path, + datadir: &Path, +) -> Result<(), SyncError> { + use crate::utils::get_rocksdb_temp_storage_dir; + use crossbeam::channel::{bounded, unbounded}; + use ethrex_trie::{ + Nibbles, Node, ThreadPool, + trie_sorted::{BUFFER_COUNT, SIZE_TO_WRITE_DB, trie_from_sorted_accounts}, + }; + use std::thread::scope; + + struct RocksDBIterator<'a> { + iter: rocksdb::DBRawIterator<'a>, + limit: H256, + } + + impl<'a> Iterator for RocksDBIterator<'a> { + type Item = (H256, Vec); + + fn next(&mut self) -> Option { + if !self.iter.valid() { + return None; + } + let return_value = { + let key = self.iter.key(); + let value = self.iter.value(); + match (key, value) { + (Some(key), Some(value)) => { + let hash = H256::from_slice(&key[0..32]); + let key = H256::from_slice(&key[32..]); + let value = value.to_vec(); + if hash != self.limit { + None + } else { + Some((key, value)) + } + } + _ => None, + } + }; + self.iter.next(); + return_value + } + } + + let mut db_options = rocksdb::Options::default(); + db_options.create_if_missing(true); + let db = rocksdb::DB::open(&db_options, get_rocksdb_temp_storage_dir(datadir)) + .map_err(|err: rocksdb::Error| SyncError::RocksDBError(err.into_string()))?; + let file_paths: Vec = std::fs::read_dir(account_storages_snapshots_dir) + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? + .collect::, _>>() + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)? + .into_iter() + .map(|res| res.path()) + .collect(); + db.ingest_external_file(file_paths) + .map_err(|err| SyncError::RocksDBError(err.into_string()))?; + let snapshot = db.snapshot(); + + let account_with_storage_and_tries = accounts_with_storage + .into_iter() + .map(|account_hash| { + ( + account_hash, + store + .open_direct_storage_trie(account_hash, *EMPTY_TRIE_HASH) + .expect("Should be able to open trie"), + ) + }) + .collect::>(); + + let (sender, receiver) = unbounded::<()>(); + let mut counter = 0; + let thread_count = std::thread::available_parallelism() + .map(|num| num.into()) + .unwrap_or(8); + + let (buffer_sender, buffer_receiver) = bounded::>(BUFFER_COUNT as usize); + for _ in 0..BUFFER_COUNT { + let _ = buffer_sender.send(Vec::with_capacity(SIZE_TO_WRITE_DB as usize)); + } + + scope(|scope| { + let pool: Arc> = Arc::new(ThreadPool::new(thread_count, scope)); + for (account_hash, trie) in account_with_storage_and_tries.iter() { + let sender = sender.clone(); + let buffer_sender = buffer_sender.clone(); + let buffer_receiver = buffer_receiver.clone(); + if counter >= thread_count - 1 { + let _ = receiver.recv(); + counter -= 1; + } + counter += 1; + let pool_clone = pool.clone(); + let mut iter = snapshot.raw_iterator(); + let task = Box::new(move || { + let mut buffer: [u8; 64] = [0_u8; 64]; + buffer[..32].copy_from_slice(&account_hash.0); + iter.seek(buffer); + let iter = RocksDBIterator { + iter, + limit: *account_hash, + }; + + let _ = trie_from_sorted_accounts( + trie.db(), + &mut iter.inspect(|_| METRICS.storage_leaves_inserted.inc()), + pool_clone, + buffer_sender, + buffer_receiver, + ) + .inspect_err(|err: ðrex_trie::trie_sorted::TrieGenerationError| { + error!( + "we found an error while inserting the storage trie for the account {account_hash:x}, err {err}" + ); + }) + .map_err(SyncError::TrieGenerationError); + let _ = sender.send(()); + }); + pool.execute(task); + } + }); + + // close db before removing directory + drop(snapshot); + drop(db); + + std::fs::remove_dir_all(account_storages_snapshots_dir) + .map_err(|_| SyncError::AccountStoragesSnapshotsDirNotFound)?; + std::fs::remove_dir_all(get_rocksdb_temp_storage_dir(datadir)) + .map_err(|e| SyncError::StorageTempDBDirNotFound(e.to_string()))?; + + Ok(()) +} diff --git a/crates/networking/p2p/utils.rs b/crates/networking/p2p/utils.rs index 1209dcc0ff8..73d4a3d7b53 100644 --- a/crates/networking/p2p/utils.rs +++ b/crates/networking/p2p/utils.rs @@ -150,7 +150,6 @@ pub fn dump_to_file(path: &Path, contents: Vec) -> Result<(), DumpError> { .inspect_err(|err| error!(%err, ?path, "Failed to dump snapshot to file")) .map_err(|err| DumpError { path: path.to_path_buf(), - contents, error: err.kind(), }) } @@ -164,7 +163,6 @@ pub fn dump_accounts_to_file( .inspect_err(|err| error!("Rocksdb writing stt error {err:?}")) .map_err(|_| DumpError { path: path.to_path_buf(), - contents: Vec::new(), error: std::io::ErrorKind::Other, }); #[cfg(not(feature = "rocksdb"))] @@ -207,7 +205,6 @@ pub fn dump_storages_to_file( .inspect_err(|err| error!("Rocksdb writing stt error {err:?}")) .map_err(|_| DumpError { path: path.to_path_buf(), - contents: Vec::new(), error: std::io::ErrorKind::Other, }); diff --git a/test/Cargo.toml b/test/Cargo.toml index e44cbc50941..a69581d5d71 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -35,6 +35,7 @@ proptest = "1.0.0" cita_trie = "4.0.0" hasher = "0.1.4" tokio = { workspace = true, features = ["full", "test-util"] } +lazy_static.workspace = true bytes.workspace = true serde_json.workspace = true ethrex.workspace = true diff --git a/test/tests/p2p/mod.rs b/test/tests/p2p/mod.rs index 63b0e242492..8a95e5893c9 100644 --- a/test/tests/p2p/mod.rs +++ b/test/tests/p2p/mod.rs @@ -1 +1,2 @@ mod rlpx; +mod snap_server_tests; diff --git a/test/tests/p2p/snap_server_tests.rs b/test/tests/p2p/snap_server_tests.rs new file mode 100644 index 00000000000..6c6d3fde443 --- /dev/null +++ b/test/tests/p2p/snap_server_tests.rs @@ -0,0 +1,824 @@ +//! Snap protocol server tests +//! +//! Hive `AccountRange` tests based on go-ethereum's test suite: +//! https://github.com/ethereum/go-ethereum/blob/3e567b8b2901611f004b5a6070a9b6d286be128d/cmd/devp2p/internal/ethtest/snap.go#L69 + +use std::str::FromStr; + +use ethrex_common::{BigEndianHash, H256, types::AccountStateSlimCodec}; +use ethrex_p2p::rlpx::snap::GetAccountRange; +use ethrex_p2p::snap::{SnapError, process_account_range_request}; +use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode}; +use ethrex_storage::{EngineType, Store}; +use ethrex_trie::EMPTY_TRIE_HASH; + +use lazy_static::lazy_static; + +lazy_static! { + // Constant values for hive `AccountRange` tests + static ref HASH_MIN: H256 = H256::zero(); + static ref HASH_MAX: H256 = + H256::from_str("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",) + .unwrap(); + static ref HASH_FIRST: H256 = + H256::from_str("0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6") + .unwrap(); + static ref HASH_SECOND: H256 = + H256::from_str("0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f") + .unwrap(); + static ref HASH_FIRST_MINUS_500: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 500)); + static ref HASH_FIRST_MINUS_450: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 450)); + static ref HASH_FIRST_MINUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() - 1)); + static ref HASH_FIRST_PLUS_ONE: H256 = H256::from_uint(&((*HASH_FIRST).into_uint() + 1)); +} + +#[tokio::test] +async fn hive_account_range_a() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") + .unwrap() + ); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_b() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 3000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 65); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6") + .unwrap() + ); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_c() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 2000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 44); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595") + .unwrap() + ); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_d() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 1, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_e() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MAX, + response_bytes: 0, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_f() -> Result<(), SnapError> { + // In this test, we request a range where startingHash is before the first available + // account key, and limitHash is after. The server should return the first and second + // account of the state (because the second account is the 'next available'). + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_MINUS_500, + limit_hash: *HASH_FIRST_PLUS_ONE, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 2); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_SECOND); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_g() -> Result<(), SnapError> { + // Here we request range where both bounds are before the first available account key. + // This should return the first account (even though it's out of bounds). + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_MINUS_500, + limit_hash: *HASH_FIRST_MINUS_450, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_h() -> Result<(), SnapError> { + // In this test, both startingHash and limitHash are zero. + // The server should return the first available account. + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_MIN, + limit_hash: *HASH_MIN, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_i() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099") + .unwrap() + ); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_j() -> Result<(), SnapError> { + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST_PLUS_ONE, + limit_hash: *HASH_MAX, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 86); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_SECOND); + assert_eq!( + res.accounts.last().unwrap().hash, + H256::from_str("0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa") + .unwrap() + ); + Ok(()) +} + +// Tests for different roots skipped (we don't have other state's data loaded) + +// Non-sensical requests + +#[tokio::test] +async fn hive_account_range_k() -> Result<(), SnapError> { + // In this test, the startingHash is the first available key, and limitHash is + // a key before startingHash (wrong order). The server should return the first available key. + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_FIRST_MINUS_ONE, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +#[tokio::test] +async fn hive_account_range_m() -> Result<(), SnapError> { + // In this test, the startingHash is the first available key and limitHash is zero. + // (wrong order). The server should return the first available key. + let (store, root) = setup_initial_state()?; + let request = GetAccountRange { + id: 0, + root_hash: root, + starting_hash: *HASH_FIRST, + limit_hash: *HASH_MIN, + response_bytes: 4000, + }; + let res = process_account_range_request(request, store).await.unwrap(); + // Check test invariants + assert_eq!(res.accounts.len(), 1); + assert_eq!(res.accounts.first().unwrap().hash, *HASH_FIRST); + assert_eq!(res.accounts.last().unwrap().hash, *HASH_FIRST); + Ok(()) +} + +// Initial state setup for hive snap tests + +fn setup_initial_state() -> Result<(Store, H256), SnapError> { + // We cannot process the old blocks that hive uses for the devp2p snap tests + // So I copied the state from a geth execution of the test suite + + // State was trimmed to only the first 100 accounts (as the furthest account used by the tests is account 87) + // If the full 408 account state is needed check out previous commits the PR that added this code + + let accounts: Vec<(&str, Vec)> = vec![ + ( + "0x005e94bf632e80cde11add7d3447cd4ca93a5f2205d9874261484ae180718bd6", + vec![ + 228_u8, 1, 128, 160, 223, 151, 249, 75, 196, 116, 113, 135, 6, 6, 246, 38, 251, + 122, 11, 66, 238, 210, 212, 95, 204, 132, 220, 18, 0, 206, 98, 247, 131, 29, 169, + 144, 128, + ], + ), + ( + "0x00748bacab20da9ae19dd26a33bd10bbf825e28b3de84fc8fe1d15a21645067f", + vec![196, 128, 1, 128, 128], + ), + ( + "0x00aa781aff39a8284ef43790e3a511b2caa50803613c5096bc782e8de08fa4c5", + vec![ + 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, 128, + 128, + ], + ), + ( + "0x016d92531f4754834b0502de5b0342ceff21cde5bef386a83d2292f4445782c2", + vec![196, 128, 1, 128, 128], + ), + ( + "0x02547b56492bfe767f3d18be2aab96441c449cd945770ef7ef8555acc505b2e4", + vec![196, 128, 1, 128, 128], + ), + ( + "0x025f478d53bf78add6fa3708d9e061d59bfe14b21329b2a4cf1156d4f81b3d2d", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0267c643f67b47cac9efacf6fcf0e4f4e1b273a727ded155db60eb9907939eb6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0304d8eaccf0b942c468074250cbcb625ec5c4688b6b5d17d2a9bdd8dd565d5a", + vec![ + 228, 1, 128, 160, 224, 12, 73, 166, 88, 73, 208, 92, 191, 39, 164, 215, 120, 138, + 104, 188, 123, 96, 19, 174, 51, 65, 29, 64, 188, 137, 40, 47, 192, 100, 243, 61, + 128, + ], + ), + ( + "0x0463e52cda557221b0b66bd7285b043071df4c2ab146260f4e010970f3a0cccf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x04d9aa4f67f8b24d70a0ffd757e82456d9184113106b7d9e8eb6c3e8a8df27ee", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x053df2c3b574026812b154a99b13b626220af85cd01bb1693b1d42591054bce6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0579e46a5ed8a88504ac7d579b12eb346fbe4fd7e281bdd226b891f8abed4789", + vec![ + 228, 1, 128, 160, 61, 14, 43, 165, 55, 243, 89, 65, 6, 135, 9, 69, 15, 37, 254, + 228, 90, 175, 77, 198, 174, 46, 210, 42, 209, 46, 7, 67, 172, 124, 84, 167, 128, + ], + ), + ( + "0x05f6de281d8c2b5d98e8e01cd529bd76416b248caf11e0552047c5f1d516aab6", + vec![196, 128, 1, 128, 128], + ), + ( + "0x07b49045c401bcc408f983d91a199c908cdf0d646049b5b83629a70b0117e295", + vec![ + 228, 1, 128, 160, 134, 154, 203, 146, 159, 89, 28, 84, 203, 133, 132, 42, 81, 242, + 150, 99, 94, 125, 137, 87, 152, 197, 71, 162, 147, 175, 228, 62, 123, 247, 244, 23, + 128, + ], + ), + ( + "0x0993fd5b750fe4414f93c7880b89744abb96f7af1171ed5f47026bdf01df1874", + vec![196, 128, 1, 128, 128], + ), + ( + "0x099d5081762b8b265e8ba4cd8e43f08be4715d903a0b1d96b3d9c4e811cbfb33", + vec![196, 128, 1, 128, 128], + ), + ( + "0x09d6e6745d272389182a510994e2b54d14b731fac96b9c9ef434bc1924315371", + vec![196, 128, 128, 128, 128], + ), + ( + "0x0a93a7231976ad485379a3b66c2d8983ba0b2ca87abaf0ca44836b2a06a2b102", + vec![196, 128, 1, 128, 128], + ), + ( + "0x0b564e4a0203cbcec8301709a7449e2e7371910778df64c89f48507390f2d129", + vec![196, 1, 128, 128, 128], + ), + ( + "0x0cd2a7c53c76f228ed3aa7a29644b1915fde9ec22e0433808bf5467d914e7c7a", + vec![ + 228, 1, 128, 160, 7, 84, 3, 90, 164, 7, 51, 129, 162, 17, 52, 43, 80, 125, 232, + 231, 117, 201, 124, 150, 16, 150, 230, 226, 39, 93, 240, 191, 203, 179, 160, 28, + 128, + ], + ), + ( + "0x0e0e4646090b881949ec9991e48dec768ccd1980896aefd0d51fd56fd5689790", + vec![ + 228, 1, 128, 160, 96, 252, 105, 16, 13, 142, 99, 38, 103, 200, 11, 148, 212, 52, 0, + 136, 35, 237, 117, 65, 107, 113, 203, 209, 18, 180, 208, 176, 47, 86, 48, 39, 128, + ], + ), + ( + "0x0e27113c09de0a0cb0ff268c677aba17d39a3190fe15aec0ff7f54184955cba4", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x0e57ffa6cc6cbd96c1400150417dd9b30d958c58f63c36230a90a02b076f78b5", + vec![196, 128, 1, 128, 128], + ), + ( + "0x0f30822f90f33f1d1ba6d1521a00935630d2c81ab12fa03d4a0f4915033134f3", + vec![ + 228, 1, 128, 160, 128, 120, 243, 37, 157, 129, 153, 183, 202, 57, 213, 30, 53, 213, + 181, 141, 113, 255, 20, 134, 6, 115, 16, 96, 56, 109, 50, 60, 93, 25, 24, 44, 128, + ], + ), + ( + "0x1017b10a7cc3732d729fe1f71ced25e5b7bc73dc62ca61309a8c7e5ac0af2f72", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1098f06082dc467088ecedb143f9464ebb02f19dc10bd7491b03ba68d751ce45", + vec![196, 1, 128, 128, 128], + ), + ( + "0x11eb0304c1baa92e67239f6947cb93e485a7db05e2b477e1167a8960458fa8cc", + vec![196, 1, 128, 128, 128], + ), + ( + "0x12be3bf1f9b1dab5f908ca964115bee3bcff5371f84ede45bc60591b21117c51", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x12c1bb3dddf0f06f62d70ed5b7f7db7d89b591b3f23a838062631c4809c37196", + vec![196, 128, 1, 128, 128], + ), + ( + "0x12e394ad62e51261b4b95c431496e46a39055d7ada7dbf243f938b6d79054630", + vec![196, 1, 128, 128, 128], + ), + ( + "0x13cfc46f6bdb7a1c30448d41880d061c3b8d36c55a29f1c0c8d95a8e882b8c25", + vec![ + 228, 1, 128, 160, 148, 79, 9, 90, 251, 209, 56, 62, 93, 15, 145, 239, 2, 137, 93, + 57, 143, 79, 118, 253, 182, 216, 106, 223, 71, 101, 242, 91, 220, 48, 79, 95, 128, + ], + ), + ( + "0x15293aec87177f6c88f58bc51274ba75f1331f5cb94f0c973b1deab8b3524dfe", + vec![196, 128, 1, 128, 128], + ), + ( + "0x170c927130fe8f1db3ae682c22b57f33f54eb987a7902ec251fe5dba358a2b25", + vec![196, 128, 1, 128, 128], + ), + ( + "0x17350c7adae7f08d7bbb8befcc97234462831638443cd6dfea186cbf5a08b7c7", + vec![ + 228, 1, 128, 160, 76, 231, 156, 217, 100, 86, 80, 240, 160, 14, 255, 168, 111, 111, + 234, 115, 60, 236, 234, 158, 162, 105, 100, 130, 143, 242, 92, 240, 87, 123, 201, + 116, 128, + ], + ), + ( + "0x174f1a19ff1d9ef72d0988653f31074cb59e2cf37cd9d2992c7b0dd3d77d84f9", + vec![196, 128, 1, 128, 128], + ), + ( + "0x17984cc4b4aac0492699d37662b53ec2acf8cbe540c968b817061e4ed27026d0", + vec![196, 128, 1, 128, 128], + ), + ( + "0x181abdd5e212171007e085fdc284a84d42d5bfc160960d881ccb6a10005ff089", + vec![196, 1, 128, 128, 128], + ), + ( + "0x188111c233bf6516bb9da8b5c4c31809a42e8604cd0158d933435cfd8e06e413", + vec![196, 1, 128, 128, 128], + ), + ( + "0x18f4256a59e1b2e01e96ac465e1d14a45d789ce49728f42082289fc25cf32b8d", + vec![196, 128, 1, 128, 128], + ), + ( + "0x1960414a11f8896c7fc4243aba7ed8179b0bc6979b7c25da7557b17f5dee7bf7", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1a28912018f78f7e754df6b9fcec33bea25e5a232224db622e0c3343cf079eff", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1bf7626cec5330a127e439e68e6ee1a1537e73b2de1aa6d6f7e06bc0f1e9d763", + vec![196, 128, 1, 128, 128], + ), + ( + "0x1c248f110218eaae2feb51bc82e9dcc2844bf93b88172c52afcb86383d262323", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595", + vec![ + 228, 1, 128, 160, 175, 134, 126, 108, 186, 232, 16, 202, 169, 36, 184, 182, 172, + 61, 140, 8, 145, 131, 20, 145, 166, 144, 109, 208, 190, 122, 211, 36, 220, 209, 83, + 61, 128, + ], + ), + ( + "0x1d38ada74301c31f3fd7d92dd5ce52dc37ae633e82ac29c4ef18dfc141298e26", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x1d6ee979097e29141ad6b97ae19bb592420652b7000003c55eb52d5225c3307d", + vec![ + 228, 1, 128, 160, 247, 53, 145, 231, 145, 175, 76, 124, 95, 160, 57, 195, 61, 217, + 209, 105, 202, 177, 75, 29, 155, 12, 167, 139, 204, 78, 116, 13, 85, 59, 26, 207, + 128, + ], + ), + ( + "0x1dff76635b74ddba16bba3054cc568eed2571ea6becaabd0592b980463f157e2", + vec![196, 1, 128, 128, 128], + ), + ( + "0x1ee7e0292fba90d9733f619f976a2655c484adb30135ef0c5153b5a2f32169df", + vec![196, 1, 128, 128, 128], + ), + ( + "0x209b102e507b8dfc6acfe2cf55f4133b9209357af679a6d507e6ee87112bfe10", + vec![196, 1, 128, 128, 128], + ), + ( + "0x210ce6d692a21d75de3764b6c0356c63a51550ebec2c01f56c154c24b1cf8888", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2116ab29b4cb8547af547fe472b7ce30713f234ed49cb1801ea6d3cf9c796d57", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x2290ea88cc63f09ab5e8c989a67e2e06613311801e39c84aae3badd8bb38409c", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x2369a492b6cddcc0218617a060b40df0e7dda26abe48ba4e4108c532d3f2b84f", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2374954008440ca3d17b1472d34cc52a6493a94fb490d5fb427184d7d5fd1cbf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x23ddaac09188c12e5d88009afa4a34041175c5531f45be53f1560a1cbfec4e8a", + vec![ + 228, 1, 128, 160, 71, 250, 72, 226, 93, 54, 105, 169, 187, 25, 12, 89, 147, 143, + 75, 228, 157, 226, 208, 131, 105, 110, 185, 57, 195, 180, 7, 46, 198, 126, 67, 177, + 128, + ], + ), + ( + "0x246cc8a2b79a30ec71390d829d0cb37cce1b953e89cb14deae4945526714a71c", + vec![196, 128, 1, 128, 128], + ), + ( + "0x255ec86eac03ba59f6dfcaa02128adbb22c561ae0c49e9e62e4fff363750626e", + vec![ + 228, 1, 128, 160, 102, 235, 22, 7, 27, 163, 121, 191, 12, 99, 47, 203, 82, 249, 23, + 90, 101, 107, 239, 98, 173, 240, 190, 245, 52, 154, 127, 90, 106, 173, 93, 136, + 128, + ], + ), + ( + "0x26ce7d83dfb0ab0e7f15c42aeb9e8c0c5dba538b07c8e64b35fb64a37267dd96", + vec![ + 228, 1, 128, 160, 36, 52, 191, 198, 67, 236, 54, 65, 22, 205, 113, 81, 154, 57, + 118, 98, 178, 12, 82, 209, 173, 207, 240, 184, 48, 232, 10, 115, 142, 25, 243, 14, + 128, + ], + ), + ( + "0x2705244734f69af78e16c74784e1dc921cb8b6a98fe76f577cc441c831e973bf", + vec![196, 1, 128, 128, 128], + ), + ( + "0x28f25652ec67d8df6a2e33730e5d0983443e3f759792a0128c06756e8eb6c37f", + vec![ + 211, 128, 143, 192, 151, 206, 123, 201, 7, 21, 179, 75, 159, 16, 0, 0, 0, 0, 128, + 128, + ], + ), + ( + "0x2a248c1755e977920284c8054fceeb20530dc07cd8bbe876f3ce02000818cc3a", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2a39afbe88f572c23c90da2d059af3de125f1da5c3753c530dc5619a4857119f", + vec![ + 228, 1, 128, 160, 130, 137, 181, 88, 134, 95, 44, 161, 245, 76, 152, 181, 255, 93, + 249, 95, 7, 194, 78, 198, 5, 226, 71, 181, 140, 119, 152, 96, 93, 205, 121, 79, + 128, + ], + ), + ( + "0x2b8d12301a8af18405b3c826b6edcc60e8e034810f00716ca48bebb84c4ce7ab", + vec![196, 1, 128, 128, 128], + ), + ( + "0x2baa718b760c0cbd0ec40a3c6df7f2948b40ba096e6e4b116b636f0cca023bde", + vec![196, 128, 1, 128, 128], + ), + ( + "0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6", + vec![ + 228, 128, 128, 128, 160, 142, 3, 136, 236, 246, 76, 250, 118, 179, 166, 175, 21, + 159, 119, 69, 21, 25, 167, 249, 187, 134, 46, 76, 206, 36, 23, 92, 121, 31, 220, + 176, 223, + ], + ), + ( + "0x2fe5767f605b7b821675b223a22e4e5055154f75e7f3041fdffaa02e4787fab8", + vec![196, 128, 1, 128, 128], + ), + ( + "0x303f57a0355c50bf1a0e1cf0fa8f9bdbc8d443b70f2ad93ac1c6b9c1d1fe29a2", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x30ce5b7591126d5464dfb4fc576a970b1368475ce097e244132b06d8cc8ccffe", + vec![196, 128, 1, 128, 128], + ), + ( + "0x315ccc15883d06b4e743f8252c999bf1ee994583ff6114d89c0f3ddee828302b", + vec![196, 1, 128, 128, 128], + ), + ( + "0x3197690074092fe51694bdb96aaab9ae94dac87f129785e498ab171a363d3b40", + vec![196, 128, 1, 128, 128], + ), + ( + "0x34a715e08b77afd68cde30b62e222542f3db90758370400c94d0563959a1d1a0", + vec![ + 228, 1, 128, 160, 79, 68, 99, 41, 181, 238, 61, 19, 212, 246, 181, 229, 242, 16, + 221, 194, 217, 15, 237, 186, 56, 75, 149, 14, 54, 161, 209, 154, 249, 92, 92, 177, + 128, + ], + ), + ( + "0x37310559ceaade42e45b3e3f05925aadca9e60aeeb9dd60d824875d9e9e71e26", + vec![ + 228, 1, 128, 160, 114, 200, 146, 33, 218, 237, 204, 221, 63, 187, 166, 108, 27, 8, + 27, 54, 52, 206, 137, 213, 160, 105, 190, 151, 255, 120, 50, 119, 143, 123, 2, 58, + 128, + ], + ), + ( + "0x37d65eaa92c6bc4c13a5ec45527f0c18ea8932588728769ec7aecfe6d9f32e42", + vec![ + 248, 68, 128, 42, 160, 172, 49, 98, 168, 185, 219, 180, 49, 139, 132, 33, 159, 49, + 64, 231, 169, 236, 53, 18, 98, 52, 18, 2, 151, 221, 225, 15, 81, 178, 95, 106, 38, + 160, 245, 122, 205, 64, 37, 152, 114, 96, 109, 118, 25, 126, 240, 82, 243, 211, 85, + 136, 218, 223, 145, 158, 225, 240, 227, 203, 155, 98, 211, 244, 176, 44, + ], + ), + ( + "0x37ddfcbcb4b2498578f90e0fcfef9965dcde4d4dfabe2f2836d2257faa169947", + vec![ + 228, 1, 128, 160, 82, 214, 210, 145, 58, 228, 75, 202, 17, 181, 161, 22, 2, 29, + 185, 124, 145, 161, 62, 56, 94, 212, 139, 160, 102, 40, 231, 66, 1, 35, 29, 186, + 128, + ], + ), + ( + "0x37e51740ad994839549a56ef8606d71ace79adc5f55c988958d1c450eea5ac2d", + vec![196, 1, 128, 128, 128], + ), + ( + "0x38152bce526b7e1c2bedfc9d297250fcead02818be7806638564377af145103b", + vec![ + 228, 1, 128, 160, 108, 0, 224, 145, 218, 227, 212, 34, 111, 172, 214, 190, 128, 44, + 134, 93, 93, 176, 245, 36, 117, 77, 34, 102, 100, 6, 19, 139, 84, 250, 176, 230, + 128, + ], + ), + ( + "0x3848b7da914222540b71e398081d04e3849d2ee0d328168a3cc173a1cd4e783b", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x389093badcaa24c3a8cbb4461f262fba44c4f178a162664087924e85f3d55710", + vec![196, 1, 128, 128, 128], + ), + ( + "0x3897cb9b6f68765022f3c74f84a9f2833132858f661f4bc91ccd7a98f4e5b1ee", + vec![196, 1, 128, 128, 128], + ), + ( + "0x395b92f75f8e06b5378a84ba03379f025d785d8b626b2b6a1c84b718244b9a91", + vec![ + 228, 1, 128, 160, 84, 70, 184, 24, 244, 198, 105, 102, 156, 211, 49, 71, 38, 255, + 19, 76, 241, 140, 88, 169, 165, 54, 223, 19, 199, 0, 97, 7, 5, 168, 183, 200, 128, + ], + ), + ( + "0x3be526914a7d688e00adca06a0c47c580cb7aa934115ca26006a1ed5455dd2ce", + vec![196, 128, 1, 128, 128], + ), + ( + "0x3e57e37bc3f588c244ffe4da1f48a360fa540b77c92f0c76919ec4ee22b63599", + vec![196, 128, 1, 128, 128], + ), + ( + "0x415ded122ff7b6fe5862f5c443ea0375e372862b9001c5fe527d276a3a420280", + vec![196, 1, 128, 128, 128], + ), + ( + "0x419809ad1512ed1ab3fb570f98ceb2f1d1b5dea39578583cd2b03e9378bbe418", + vec![196, 1, 128, 128, 128], + ), + ( + "0x4363d332a0d4df8582a84932729892387c623fe1ec42e2cfcbe85c183ed98e0e", + vec![ + 213, 130, 1, 146, 143, 192, 151, 206, 123, 201, 7, 21, 179, 73, 233, 122, 138, 101, + 46, 31, 128, 128, + ], + ), + ( + "0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099", + vec![ + 228, 1, 1, 160, 190, 61, 117, 161, 114, 155, 225, 87, 231, 156, 59, 119, 240, 2, 6, + 219, 77, 84, 227, 234, 20, 55, 90, 1, 84, 81, 200, 142, 192, 103, 199, 144, 128, + ], + ), + ( + "0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa", + vec![196, 1, 128, 128, 128], + ), + ( + "0x465311df0bf146d43750ed7d11b0451b5f6d5bfc69b8a216ef2f1c79c93cd848", + vec![196, 128, 1, 128, 128], + ), + ( + "0x47450e5beefbd5e3a3f80cbbac474bb3db98d5e609aa8d15485c3f0d733dea3a", + vec![ + 228, 1, 128, 160, 84, 66, 224, 39, 157, 63, 17, 73, 222, 76, 232, 217, 226, 211, + 240, 29, 24, 84, 117, 80, 56, 172, 26, 15, 174, 92, 72, 116, 155, 247, 31, 32, 128, + ], + ), + ( + "0x482814ea8f103c39dcf6ba7e75df37145bde813964d82e81e5d7e3747b95303d", + vec![196, 128, 1, 128, 128], + ), + ( + "0x4845aac9f26fcd628b39b83d1ccb5c554450b9666b66f83aa93a1523f4db0ab6", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x48e291f8a256ab15da8401c8cae555d5417a992dff3848926fa5b71655740059", + vec![ + 228, 1, 128, 160, 162, 231, 8, 75, 169, 206, 193, 121, 81, 156, 126, 137, 80, 198, + 106, 211, 203, 168, 88, 106, 96, 207, 249, 244, 214, 12, 24, 141, 214, 33, 82, 42, + 128, + ], + ), + ( + "0x4973f6aa8cf5b1190fc95379aa01cff99570ee6b670725880217237fb49e4b24", + vec![ + 228, 1, 128, 160, 174, 46, 127, 28, 147, 60, 108, 168, 76, 232, 190, 129, 30, 244, + 17, 222, 231, 115, 251, 105, 80, 128, 86, 215, 36, 72, 4, 142, 161, 219, 92, 71, + 128, + ], + ), + ( + "0x4b238e08b80378d0815e109f350a08e5d41ec4094df2cfce7bc8b9e3115bda70", + vec![ + 228, 1, 128, 160, 17, 245, 211, 153, 202, 143, 183, 169, 175, 90, 212, 129, 190, + 96, 207, 97, 212, 84, 147, 205, 32, 32, 108, 157, 10, 35, 124, 231, 215, 87, 30, + 95, 128, + ], + ), + ( + "0x4b9f335ce0bdffdd77fdb9830961c5bc7090ae94703d0392d3f0ff10e6a4fbab", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x4bd8ef9873a5e85d4805dbcb0dbf6810e558ea175167549ef80545a9cafbb0e1", + vec![ + 228, 1, 128, 160, 161, 73, 19, 213, 72, 172, 29, 63, 153, 98, 162, 26, 86, 159, + 229, 47, 20, 54, 182, 210, 245, 234, 78, 54, 222, 19, 234, 133, 94, 222, 84, 224, + 128, + ], + ), + ( + "0x4c2765139cace1d217e238cc7ccfbb751ef200e0eae7ec244e77f37e92dfaee5", + vec![196, 1, 128, 128, 128], + ), + ( + "0x4c310e1f5d2f2e03562c4a5c473ae044b9ee19411f07097ced41e85bd99c3364", + vec![196, 128, 1, 128, 128], + ), + ( + "0x4ccd31891378d2025ef58980481608f11f5b35a988e877652e7cbb0a6127287c", + vec![201, 128, 133, 23, 72, 118, 232, 0, 128, 128], + ), + ( + "0x4ceaf2371fcfb54a4d8bc1c804d90b06b3c32c9f17112b57c29b30a25cf8ca12", + vec![196, 128, 1, 128, 128], + ), + ]; + + // Create a store and load it up with the accounts + let store = Store::new("null", EngineType::InMemory).unwrap(); + let mut state_trie = store.open_direct_state_trie(*EMPTY_TRIE_HASH)?; + for (address, account) in accounts { + let hashed_address = H256::from_str(address).unwrap().as_bytes().to_vec(); + let AccountStateSlimCodec(account) = RLPDecode::decode(&account).unwrap(); + state_trie + .insert(hashed_address, account.encode_to_vec()) + .unwrap(); + } + Ok((store, state_trie.hash().unwrap())) +} diff --git a/tooling/sync/docker_monitor.py b/tooling/sync/docker_monitor.py index 331b26d3ab0..0ab1c4254c0 100644 --- a/tooling/sync/docker_monitor.py +++ b/tooling/sync/docker_monitor.py @@ -48,6 +48,18 @@ "block_processing": "📦", "success": "🎉", "failed": "❌" } +# Phase completion patterns for parsing sync logs +PHASE_COMPLETION_PATTERNS = { + "Block Headers": r"✓ BLOCK HEADERS complete: ([\d,]+) headers in (\d+:\d{2}:\d{2})", + "Account Ranges": r"✓ ACCOUNT RANGES complete: ([\d,]+) accounts in (\d+:\d{2}:\d{2})", + "Account Insertion": r"✓ ACCOUNT INSERTION complete: ([\d,]+) accounts inserted in (\d+:\d{2}:\d{2})", + "Storage Ranges": r"✓ STORAGE RANGES complete: ([\d,]+) storage slots in (\d+:\d{2}:\d{2})", + "Storage Insertion": r"✓ STORAGE INSERTION complete: ([\d,]+) storage slots inserted in (\d+:\d{2}:\d{2})", + "State Healing": r"✓ STATE HEALING complete: ([\d,]+) state paths healed in (\d+:\d{2}:\d{2})", + "Storage Healing": r"✓ STORAGE HEALING complete: ([\d,]+) storage accounts healed in (\d+:\d{2}:\d{2})", + "Bytecodes": r"✓ BYTECODES complete: ([\d,]+) bytecodes in (\d+:\d{2}:\d{2})", +} + @dataclass class Instance: @@ -262,6 +274,30 @@ def rpc_call(url: str, method: str) -> Optional[Any]: return None +def parse_phase_timings(run_id: str, container: str) -> list[tuple[str, str, str]]: + """Parse phase completion times from saved container logs. + + Returns list of (phase_name, count, duration) tuples. + """ + log_file = LOGS_DIR / f"run_{run_id}" / f"{container}.log" + if not log_file.exists(): + return [] + + try: + logs = log_file.read_text() + except Exception: + return [] + + phases = [] + for phase_name, pattern in PHASE_COMPLETION_PATTERNS.items(): + match = re.search(pattern, logs) + if match: + count = match.group(1) + duration = match.group(2) + phases.append((phase_name, count, duration)) + return phases + + def slack_notify(run_id: str, run_count: int, instances: list, hostname: str, branch: str, commit: str, build_profile: str = ""): """Send a single summary Slack message for the run.""" all_success = all(i.status == "success" for i in instances) @@ -319,6 +355,21 @@ def slack_notify(run_id: str, run_count: int, instances: list, hostname: str, br if i.error: line += f"\n Error: {i.error}" blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": line}}) + + # Add phase breakdown for each instance + for i in instances: + phases = parse_phase_timings(run_id, i.container) + if phases: + phase_lines = [f"📊 *Phase Breakdown — {i.name}*", "```"] + max_name_len = max(len(name) for name, _, _ in phases) + for name, count, duration in phases: + phase_lines.append(f"{name:<{max_name_len}} {duration} ({count})") + phase_lines.append("```") + blocks.append({ + "type": "section", + "text": {"type": "mrkdwn", "text": "\n".join(phase_lines)} + }) + try: requests.post(url, json={"blocks": blocks}, timeout=10) except Exception: @@ -417,6 +468,15 @@ def log_run_result(run_id: str, run_count: int, instances: list[Instance], hostn if inst.error: line += f"\n Error: {inst.error}" lines.append(line) + + # Add phase breakdown + phases = parse_phase_timings(run_id, inst.container) + if phases: + lines.append(f" Phase Breakdown:") + max_name_len = max(len(name) for name, _, _ in phases) + for name, count, duration in phases: + lines.append(f" {name:<{max_name_len}} {duration} ({count})") + lines.append("") # Append to log file with open(RUN_LOG_FILE, "a") as f: