From eebff62ebba8e5c95bf258e31823e621fccc25d4 Mon Sep 17 00:00:00 2001 From: Yasir Date: Thu, 30 Jan 2025 22:13:00 +0300 Subject: [PATCH 1/2] EPROD-1119 Check evm state before importing blocks --- Cargo.lock | 12 +- Cargo.toml | 6 +- bin/reth/src/commands/bitfinity_import.rs | 6 +- .../src/commands/bitfinity_reset_evm_state.rs | 10 +- .../tests/commands/bitfinity_import_it.rs | 172 ++++++++++++ bin/reth/tests/commands/bitfinity_node_it.rs | 245 +++++++++++++++++- .../commands/bitfinity_reset_evm_state_it.rs | 6 +- bin/reth/tests/commands/utils.rs | 14 +- .../downloaders/src/bitfinity_evm_client.rs | 159 +++++++++--- crates/node/core/src/args/bitfinity_args.rs | 9 + 10 files changed, 567 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b570cf2cd3..bee3c24489a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2725,8 +2725,8 @@ dependencies = [ [[package]] name = "did" -version = "0.40.0" -source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.40.x#34f302e396964c9b0ff3a7fc22543c045975147a" +version = "0.42.0" +source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.42.x#df8b9ffac634e5a9441ff92887ca613c1adcdb02" dependencies = [ "alloy", "bincode", @@ -3080,8 +3080,8 @@ dependencies = [ [[package]] name = "ethereum-json-rpc-client" -version = "0.40.0" -source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.40.x#34f302e396964c9b0ff3a7fc22543c045975147a" +version = "0.42.0" +source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.42.x#df8b9ffac634e5a9441ff92887ca613c1adcdb02" dependencies = [ "alloy", "anyhow", @@ -3166,8 +3166,8 @@ dependencies = [ [[package]] name = "evm-canister-client" -version = "0.40.0" -source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.40.x#34f302e396964c9b0ff3a7fc22543c045975147a" +version = "0.42.0" +source = "git+https://github.com/bitfinity-network/bitfinity-evm-sdk?tag=v0.42.x#df8b9ffac634e5a9441ff92887ca613c1adcdb02" dependencies = [ "candid", "did", diff --git a/Cargo.toml b/Cargo.toml index d12157c0cfd..d9f2843d182 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -650,14 +650,14 @@ tracy-client = "0.17.3" # Bitfinity Deps async-channel = "2" candid = "0.10" -did = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "did", tag = "v0.40.x" } +did = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "did", tag = "v0.42.x" } dirs = "5.0.1" -ethereum-json-rpc-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "ethereum-json-rpc-client", tag = "v0.40.x", features = [ +ethereum-json-rpc-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "ethereum-json-rpc-client", tag = "v0.42.x", features = [ "reqwest", ] } evm-canister-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "evm-canister-client", features = [ "ic-agent-client", -], tag = "v0.40.x" } +], tag = "v0.42.x" } ic-cbor = "3" ic-certificate-verification = "3" ic-certification = "3" diff --git a/bin/reth/src/commands/bitfinity_import.rs b/bin/reth/src/commands/bitfinity_import.rs index f6a6a714843..9eb220dfb5e 100644 --- a/bin/reth/src/commands/bitfinity_import.rs +++ b/bin/reth/src/commands/bitfinity_import.rs @@ -28,7 +28,9 @@ use reth_provider::{ }; use reth_prune::PruneModes; use reth_stages::{ - prelude::*, stages::{ExecutionStage, SenderRecoveryStage}, ExecutionStageThresholds, Pipeline, StageSet + prelude::*, + stages::{ExecutionStage, SenderRecoveryStage}, + ExecutionStageThresholds, Pipeline, StageSet, }; use reth_static_file::StaticFileProducer; use std::{path::PathBuf, sync::Arc, time::Duration}; @@ -142,6 +144,7 @@ impl BitfinityImportCommand { backup_url: self.bitfinity.backup_rpc_url.clone(), max_retries: self.bitfinity.max_retries, retry_delay: Duration::from_secs(self.bitfinity.retry_delay_secs), + max_block_age_secs: Duration::from_secs(self.bitfinity.max_block_age_secs), }; let remote_client = Arc::new( @@ -155,6 +158,7 @@ impl BitfinityImportCommand { evmc_principal: self.bitfinity.evmc_principal.clone(), ic_root_key: self.bitfinity.ic_root_key.clone(), }), + self.bitfinity.check_evm_state_before_importing, ) .await?, ); diff --git a/bin/reth/src/commands/bitfinity_reset_evm_state.rs b/bin/reth/src/commands/bitfinity_reset_evm_state.rs index 7cb8d94db41..5831926bcb0 100644 --- a/bin/reth/src/commands/bitfinity_reset_evm_state.rs +++ b/bin/reth/src/commands/bitfinity_reset_evm_state.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use alloy_rlp::{Decodable, Encodable}; use clap::Parser; -use did::evm_reset_state::EvmResetState; +use did::evm_state::EvmResetState; use did::{AccountInfoMap, RawAccountInfo, H160, H256}; use evm_canister_client::{CanisterClient, EvmCanisterClient, IcAgentClient}; use reth_db::cursor::DbCursorRO; @@ -107,7 +107,13 @@ impl BitfinityResetEvmStateCommand { max_request_bytes: usize, max_account_request_bytes: usize, ) -> Self { - Self { provider_factory, executor, parallel_requests: parallel_requests.max(1), max_request_bytes, max_account_request_bytes } + Self { + provider_factory, + executor, + parallel_requests: parallel_requests.max(1), + max_request_bytes, + max_account_request_bytes, + } } /// Execute the command diff --git a/bin/reth/tests/commands/bitfinity_import_it.rs b/bin/reth/tests/commands/bitfinity_import_it.rs index 9a173787a3e..50e9a692038 100644 --- a/bin/reth/tests/commands/bitfinity_import_it.rs +++ b/bin/reth/tests/commands/bitfinity_import_it.rs @@ -3,10 +3,18 @@ //! These tests requires a running EVM node or EVM block extractor node at the specified URL. //! +use super::bitfinity_node_it::{ + eth_server::{EthImpl, EthServer}, + mock_eth_server_start, +}; use super::utils::*; use alloy_eips::BlockNumberOrTag; + use ethereum_json_rpc_client::{reqwest::ReqwestClient, EthJsonRpcClient}; + +use reth::commands::bitfinity_import::BitfinityImportCommand; use reth_provider::{BlockNumReader, BlockReader, BlockReaderIdExt}; + use std::time::Duration; #[tokio::test] @@ -134,3 +142,167 @@ async fn bitfinity_test_should_import_data_from_evm_with_backup_rpc_url() { assert_eq!(remote_block.state_root.0, local_block.state_root.0); } } + +#[tokio::test] +async fn bitfinity_test_should_not_import_blocks_when_evm_is_disabled_and_check_evm_state_before_importing_is_true( +) { + // Arrange + let _log = init_logs(); + + let eth_server = EthImpl::with_evm_state(did::evm_state::EvmGlobalState::Disabled); + let (_server, eth_server_address) = + mock_eth_server_start(EthServer::into_rpc(eth_server)).await; + let evm_datasource_url = format!("http://{}", eth_server_address); + + let (_temp_dir, mut import_data) = + bitfinity_import_config_data(&evm_datasource_url, None, None).await.unwrap(); + import_data.bitfinity_args.end_block = Some(10); + import_data.bitfinity_args.batch_size = 5; + import_data.bitfinity_args.max_fetch_blocks = 10; + import_data.bitfinity_args.check_evm_state_before_importing = true; + + // Act - Try to import blocks + let import = BitfinityImportCommand::new( + None, + import_data.data_dir, + import_data.chain, + import_data.bitfinity_args, + import_data.provider_factory.clone(), + import_data.blockchain_db, + ); + let (job_executor, _import_handle) = import.schedule_execution().await.unwrap(); + + // Allow some time for potential imports + tokio::time::sleep(Duration::from_secs(2)).await; + job_executor.stop(true).await.unwrap(); + + // Assert - No blocks should have been imported + let provider = import_data.provider_factory.provider().unwrap(); + let last_block = provider.last_block_number().unwrap(); + assert_eq!(last_block, 0, "Expected no blocks to be imported when staging mode is disabled"); +} + +#[tokio::test] +async fn bitfinity_test_should_import_blocks_when_evm_is_enabled_and_check_evm_state_before_importing_is_true( +) { + // Arrange + let _log = init_logs(); + + // Set up mock ETH server in disabled mode + let eth_server = EthImpl::with_evm_state(did::evm_state::EvmGlobalState::Enabled); + let (_server, eth_server_address) = + mock_eth_server_start(EthServer::into_rpc(eth_server)).await; + let evm_datasource_url = format!("http://{}", eth_server_address); + + let (_temp_dir, mut import_data) = + bitfinity_import_config_data(&evm_datasource_url, None, None).await.unwrap(); + import_data.bitfinity_args.end_block = Some(10); + import_data.bitfinity_args.batch_size = 5; + import_data.bitfinity_args.max_fetch_blocks = 10; + import_data.bitfinity_args.check_evm_state_before_importing = true; + + // Act - Try to import blocks + let import = BitfinityImportCommand::new( + None, + import_data.data_dir, + import_data.chain, + import_data.bitfinity_args, + import_data.provider_factory.clone(), + import_data.blockchain_db, + ); + let (job_executor, _import_handle) = import.schedule_execution().await.unwrap(); + + // Allow some time for potential imports + tokio::time::sleep(Duration::from_secs(2)).await; + job_executor.stop(true).await.unwrap(); + + // Assert + let provider = import_data.provider_factory.provider().unwrap(); + let last_block = provider.last_block_number().unwrap(); + assert_eq!(last_block, 10, "Expected 10 blocks to be imported when EVM is enabled"); +} + +#[tokio::test] +async fn bitfinity_test_should_import_block_when_evm_is_enabled_and_check_evm_state_before_importing_is_false( +) { + // Arrange + let _log = init_logs(); + + // Act + // Set up mock ETH server in disabled mode + let eth_server = EthImpl::with_evm_state(did::evm_state::EvmGlobalState::Enabled); + let (_server, eth_server_address) = + mock_eth_server_start(EthServer::into_rpc(eth_server)).await; + let evm_datasource_url = format!("http://{}", eth_server_address); + + let (_temp_dir, mut import_data) = + bitfinity_import_config_data(&evm_datasource_url, None, None).await.unwrap(); + + import_data.bitfinity_args.end_block = Some(10); + import_data.bitfinity_args.batch_size = 5; + import_data.bitfinity_args.max_fetch_blocks = 10; + import_data.bitfinity_args.check_evm_state_before_importing = false; + + // Act - Try to import blocks + let import = BitfinityImportCommand::new( + None, + import_data.data_dir, + import_data.chain, + import_data.bitfinity_args, + import_data.provider_factory.clone(), + import_data.blockchain_db, + ); + let (job_executor, _import_handle) = import.schedule_execution().await.unwrap(); + + // Allow some time for potential imports + tokio::time::sleep(Duration::from_secs(2)).await; + job_executor.stop(true).await.unwrap(); + + // Assert + let provider = import_data.provider_factory.provider().unwrap(); + let last_block = provider.last_block_number().unwrap(); + assert_eq!(last_block, 10, "Expected 10 blocks to be imported when EVM is enabled"); +} + +#[tokio::test] +async fn bitfinity_test_should_not_import_block_when_evm_is_staging_and_check_evm_state_before_importing_is_false( +) { + // Arrange + let _log = init_logs(); + + // Set up mock ETH server in disabled mode + let eth_server = + EthImpl::with_evm_state(did::evm_state::EvmGlobalState::Staging { max_block_number: None }); + let (_server, eth_server_address) = + mock_eth_server_start(EthServer::into_rpc(eth_server)).await; + let evm_datasource_url = format!("http://{}", eth_server_address); + + // Act + let (_temp_dir, mut import_data) = + bitfinity_import_config_data(&evm_datasource_url, None, None).await.unwrap(); + + import_data.bitfinity_args.end_block = Some(10); + import_data.bitfinity_args.batch_size = 5; + import_data.bitfinity_args.max_fetch_blocks = 10; + import_data.bitfinity_args.check_evm_state_before_importing = true; + + // Act + let import = BitfinityImportCommand::new( + None, + import_data.data_dir, + import_data.chain, + import_data.bitfinity_args, + import_data.provider_factory.clone(), + import_data.blockchain_db, + ); + let (job_executor, _import_handle) = import.schedule_execution().await.unwrap(); + + // Allow some time for potential imports + tokio::time::sleep(Duration::from_secs(2)).await; + job_executor.stop(true).await.unwrap(); + + // Assert + let provider = import_data.provider_factory.provider().unwrap(); + let last_block = provider.last_block_number().unwrap(); + assert_eq!(last_block, 0, "Expected no blocks to be imported when EVM is staging"); +} diff --git a/bin/reth/tests/commands/bitfinity_node_it.rs b/bin/reth/tests/commands/bitfinity_node_it.rs index 8c9de89f861..62eb65c6a3d 100644 --- a/bin/reth/tests/commands/bitfinity_node_it.rs +++ b/bin/reth/tests/commands/bitfinity_node_it.rs @@ -122,7 +122,11 @@ async fn bitfinity_test_node_forward_eth_get_genesis_balances() { // Arrange let _log = init_logs(); - let eth_server = EthImpl::new(); + let eth_server = EthImpl::with_genesis_balances(vec![ + (Address::from_slice(&[1u8; 20]), U256::from(10)), + (Address::from_slice(&[2u8; 20]), U256::from(20)), + (Address::from_slice(&[3u8; 20]), U256::from(30)), + ]); let (_server, eth_server_address) = mock_eth_server_start(EthServer::into_rpc(eth_server)).await; let (reth_client, _reth_node) = @@ -156,7 +160,11 @@ async fn bitfinity_test_node_forward_ic_get_genesis_balances() { // Arrange let _log = init_logs(); - let eth_server = EthImpl::new(); + let eth_server = EthImpl::with_genesis_balances(vec![ + (Address::from_slice(&[1u8; 20]), U256::from(10)), + (Address::from_slice(&[2u8; 20]), U256::from(20)), + (Address::from_slice(&[3u8; 20]), U256::from(30)), + ]); let (_server, eth_server_address) = mock_eth_server_start(EthServer::into_rpc(eth_server)).await; let (reth_client, _reth_node) = @@ -202,12 +210,93 @@ async fn bitfinity_test_node_forward_send_raw_transaction_requests() { } /// Start a local reth node -async fn start_reth_node( +pub async fn start_reth_node( bitfinity_evm_url: Option, import_data: Option, ) -> ( EthJsonRpcClient, - NodeHandle, BlockchainProvider>>>, Components, BlockchainProvider>>>, reth_network::EthNetworkPrimitives, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider, Arc>>, RpcAddOns, BlockchainProvider>>>, Components, BlockchainProvider>>>, reth_network::EthNetworkPrimitives, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider, Arc>>, EthApi>>, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, NetworkHandle, EthEvmConfig>, EthereumEngineValidatorBuilder>>, + NodeHandle< + NodeAdapter< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + Components< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + reth_network::EthNetworkPrimitives, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + EthEvmConfig, + BasicBlockExecutorProvider, + Arc, + >, + >, + RpcAddOns< + NodeAdapter< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + Components< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + reth_network::EthNetworkPrimitives, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + EthEvmConfig, + BasicBlockExecutorProvider, + Arc, + >, + >, + EthApi< + BlockchainProvider>>, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + NetworkHandle, + EthEvmConfig, + >, + EthereumEngineValidatorBuilder, + >, + >, ) { let tasks = TaskManager::current(); @@ -257,7 +346,7 @@ async fn start_reth_node( /// Start a local Eth server. /// Reth requests will be forwarded to this server -async fn mock_eth_server_start(methods: impl Into) -> (ServerHandle, SocketAddr) { +pub async fn mock_eth_server_start(methods: impl Into) -> (ServerHandle, SocketAddr) { let addr = SocketAddr::from(([127, 0, 0, 1], 0)); let server = Server::builder().build(addr).await.unwrap(); @@ -273,10 +362,14 @@ async fn mock_eth_server_start(methods: impl Into) -> (ServerHandle, So /// Eth server mock for local testing pub mod eth_server { + use std::sync::Arc; + + use alloy_consensus::constants::{EMPTY_RECEIPTS, EMPTY_TRANSACTIONS}; use alloy_rlp::Bytes; - use did::keccak; + use did::{keccak, BlockNumber, H256, U64}; use ethereum_json_rpc_client::CertifiedResult; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + use reth_trie::EMPTY_ROOT_HASH; use revm_primitives::{Address, B256, U256}; #[rpc(server, namespace = "eth")] @@ -302,6 +395,25 @@ pub mod eth_server { async fn get_last_certified_block( &self, ) -> RpcResult>>; + + /// Returns a block by block number + #[method(name = "getBlockByNumber")] + async fn get_block_by_number( + &self, + number: BlockNumber, + ) -> RpcResult>; + + /// Returns the chain ID + #[method(name = "chainId")] + async fn chain_id(&self) -> RpcResult; + + /// Returns the last block number + #[method(name = "blockNumber")] + async fn block_number(&self) -> RpcResult; + + /// Returns the EVM global state + #[method(name = "getEvmGlobalState", aliases = ["ic_getEvmGlobalState"])] + async fn get_evm_global_state(&self) -> RpcResult; } /// Eth server implementation for local testing @@ -311,12 +423,66 @@ pub mod eth_server { pub gas_price: u128, /// Current max priority fee per gas pub max_priority_fee_per_gas: u128, + /// Current block number (atomic for thread safety) + pub current_block: Arc, + /// Chain ID + pub chain_id: u64, + /// EVM staging mode + pub state: did::evm_state::EvmGlobalState, + /// Block production task handle + #[allow(dead_code)] + block_task: Option>, + /// Genesis Balances + pub genesis_balances: Vec<(Address, U256)>, } impl EthImpl { /// Create a new Eth server implementation pub fn new() -> Self { - Self { gas_price: rand::random(), max_priority_fee_per_gas: rand::random() } + // Fake block counter + let current_block = Arc::new(std::sync::atomic::AtomicU64::new(1)); + let block_counter = current_block.clone(); + + let block_task = Some(tokio::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); + loop { + interval.tick().await; + block_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + })); + + Self { + gas_price: rand::random(), + max_priority_fee_per_gas: rand::random(), + current_block, + chain_id: 1, + state: did::evm_state::EvmGlobalState::Staging { max_block_number: None }, + block_task, + genesis_balances: vec![], + } + } + + /// Create a new instance with custom state + pub fn with_evm_state(state: did::evm_state::EvmGlobalState) -> Self { + let mut instance = Self::new(); + instance.state = state; + instance + } + + /// Set the genesis balances + pub fn with_genesis_balances(balances: Vec<(Address, U256)>) -> Self { + let mut instance = Self::new(); + instance.genesis_balances = balances; + instance + } + } + + impl Drop for EthImpl { + fn drop(&mut self) { + // Abort the background task when the EthImpl is dropped + if let Some(task) = self.block_task.take() { + task.abort(); + } } } @@ -328,6 +494,55 @@ pub mod eth_server { #[async_trait::async_trait] impl EthServer for EthImpl { + async fn get_block_by_number( + &self, + number: BlockNumber, + ) -> RpcResult> { + let block_num = match number { + BlockNumber::Latest | BlockNumber::Finalized | BlockNumber::Safe => { + self.current_block.load(std::sync::atomic::Ordering::Relaxed) + } + BlockNumber::Earliest => 0, + BlockNumber::Pending => { + self.current_block.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + BlockNumber::Number(num) => num.as_u64(), + }; + + let mut block = did::Block { + number: block_num.into(), + timestamp: did::U256::from(1234567890_u64 + block_num), + gas_limit: did::U256::from(30_000_000_u64), + base_fee_per_gas: Some(did::U256::from(7_u64)), + state_root: EMPTY_ROOT_HASH.into(), + receipts_root: EMPTY_RECEIPTS.into(), + transactions_root: EMPTY_TRANSACTIONS.into(), + parent_hash: if block_num == 0 { + H256::zero() + } else { + self.get_block_by_number(BlockNumber::Number(U64::from(block_num - 1))) + .await? + .hash + }, + ..Default::default() + }; + + block.hash = keccak::keccak_hash(&block.header_rlp_encoded()); + Ok(block) + } + + async fn chain_id(&self) -> RpcResult { + Ok(U256::from(self.chain_id)) + } + + async fn block_number(&self) -> RpcResult { + Ok(U256::from(self.current_block.load(std::sync::atomic::Ordering::Relaxed))) + } + + async fn get_evm_global_state(&self) -> RpcResult { + Ok(self.state.clone()) + } + async fn gas_price(&self) -> RpcResult { Ok(U256::from(self.gas_price)) } @@ -342,18 +557,22 @@ pub mod eth_server { } async fn get_genesis_balances(&self) -> RpcResult> { - Ok(vec![ - (Address::from_slice(&[1u8; 20]), U256::from(10)), - (Address::from_slice(&[2u8; 20]), U256::from(20)), - (Address::from_slice(&[3u8; 20]), U256::from(30)), - ]) + Ok(self.genesis_balances.clone()) } async fn get_last_certified_block( &self, ) -> RpcResult>> { Ok(CertifiedResult { - data: Default::default(), + data: did::Block { + number: 20_u64.into(), + hash: H256::from_slice(&[20; 32]), + parent_hash: H256::from_slice(&[19; 32]), + timestamp: did::U256::from(1234567890_u64 + 20), + state_root: H256::from_slice(&[20; 32]), + transactions: vec![], + ..Default::default() + }, witness: vec![], certificate: vec![1u8, 3, 11], }) diff --git a/bin/reth/tests/commands/bitfinity_reset_evm_state_it.rs b/bin/reth/tests/commands/bitfinity_reset_evm_state_it.rs index 8beb8ce6d02..b52618e5c12 100644 --- a/bin/reth/tests/commands/bitfinity_reset_evm_state_it.rs +++ b/bin/reth/tests/commands/bitfinity_reset_evm_state_it.rs @@ -8,6 +8,7 @@ use std::{ time::Duration, }; +use did::evm_state::EvmGlobalState; use did::{block::BlockResult, AccountInfoMap, H256}; use evm_canister_client::{EvmCanisterClient, IcAgentClient}; use reth::{ @@ -53,7 +54,8 @@ async fn bitfinity_manual_test_should_reset_evm_state() { evm_datasource_url, ) .await; - let _ = evm_client.admin_disable_evm(true).await.unwrap(); + + let _ = evm_client.admin_set_evm_global_state(EvmGlobalState::Disabled).await.unwrap(); // Act { @@ -85,7 +87,7 @@ async fn bitfinity_manual_test_should_reset_evm_state() { assert_eq!(evm_block.state_root.0 .0, reth_block.state_root.0); } - let _ = evm_client.admin_disable_evm(false).await.unwrap(); + let _ = evm_client.admin_set_evm_global_state(EvmGlobalState::Enabled).await.unwrap(); } #[tokio::test] diff --git a/bin/reth/tests/commands/utils.rs b/bin/reth/tests/commands/utils.rs index 3f2b102f22a..935f9b2d34c 100644 --- a/bin/reth/tests/commands/utils.rs +++ b/bin/reth/tests/commands/utils.rs @@ -1,6 +1,6 @@ //! //! Utils for bitfinity integration tests -//! +//! use std::{ fmt::{Debug, Display, Formatter}, path::PathBuf, @@ -24,9 +24,7 @@ use reth_chainspec::ChainSpec; use reth_db::{init_db, DatabaseEnv}; use reth_downloaders::bitfinity_evm_client::BitfinityEvmClient; use reth_errors::BlockExecutionError; -use reth_evm::execute::{ - BatchExecutor, BlockExecutionOutput, BlockExecutorProvider, Executor, -}; +use reth_evm::execute::{BatchExecutor, BlockExecutionOutput, BlockExecutorProvider, Executor}; use reth_node_api::NodeTypesWithDBAdapter; use reth_node_ethereum::EthereumNode; use reth_primitives::{BlockWithSenders, EthPrimitives, Receipt}; @@ -42,6 +40,7 @@ use tracing::{debug, info}; /// Local EVM canister ID for testing. pub const LOCAL_EVM_CANISTER_ID: &str = "bkyz2-fmaaa-aaaaa-qaaaq-cai"; + /// EVM block extractor for devnet running on Digital Ocean. pub const DEFAULT_EVM_DATASOURCE_URL: &str = "https://block-extractor-testnet-1052151659755.europe-west9.run.app"; @@ -51,7 +50,7 @@ pub fn init_logs() -> eyre::Result> { let mut tracer = RethTracer::new(); let stdout = LayerInfo::new( LogFormat::Terminal, - "info".to_string(), + "trace".to_string(), String::new(), Some("always".to_string()), ); @@ -64,7 +63,6 @@ pub fn init_logs() -> eyre::Result> { /// Type alias for the node types. pub type NodeTypes = NodeTypesWithDBAdapter>; - #[derive(Clone)] /// Data needed for the import tests. pub struct ImportData { @@ -172,6 +170,8 @@ pub async fn bitfinity_import_config_data( backup_rpc_url: backup_evm_datasource_url, max_retries: 3, retry_delay_secs: 3, + check_evm_state_before_importing: false, + max_block_age_secs: 600, }; Ok(( @@ -215,7 +215,7 @@ pub fn get_dfx_local_port() -> u16 { /// A [`BlockExecutorProvider`] that returns mocked execution results. /// Original code taken from ./`crates/evm/src/test_utils.rs` #[derive(Clone, Debug, Default)] -struct MockExecutorProvider { +pub struct MockExecutorProvider { exec_results: Arc>>, } diff --git a/crates/net/downloaders/src/bitfinity_evm_client.rs b/crates/net/downloaders/src/bitfinity_evm_client.rs index 4c529c71920..04b567d8747 100644 --- a/crates/net/downloaders/src/bitfinity_evm_client.rs +++ b/crates/net/downloaders/src/bitfinity_evm_client.rs @@ -46,6 +46,8 @@ pub struct RpcClientConfig { pub max_retries: u32, /// Delay between retries pub retry_delay: Duration, + /// Maximum age of the latest block to consider the EVM as active + pub max_block_age_secs: Duration, } /// Front-end API for fetching chain data from remote sources. @@ -98,6 +100,7 @@ impl BitfinityEvmClient { batch_size: usize, max_blocks: u64, certificate_settings: Option, + check_evm_state_before_importing: bool, ) -> Result { let mut headers = HashMap::new(); let mut hash_to_number = HashMap::new(); @@ -107,6 +110,21 @@ impl BitfinityEvmClient { .await .map_err(|e| RemoteClientError::ProviderError(e.to_string()))?; + if check_evm_state_before_importing { + match Self::is_evm_enabled(&provider).await { + Ok(true) => { + info!(target: "downloaders::bitfinity_evm_client", "EVM is enabled, proceeding with import"); + } + Ok(false) => { + info!(target: "downloaders::bitfinity_evm_client", "Skipping block import: EVM is disabled"); + return Ok(Self { headers, hash_to_number, bodies }); + } + Err(e) => { + warn!(target: "downloaders::bitfinity_evm_client", "Failed to check EVM state: {}. Proceeding with import", e); + } + } + } + let block_checker = match certificate_settings { None => None, Some(settings) => Some(BlockCertificateChecker::new(&provider, settings).await?), @@ -317,7 +335,11 @@ impl BitfinityEvmClient { (EthereumHardfork::London.boxed(), ForkCondition::Block(0)), ( EthereumHardfork::Paris.boxed(), - ForkCondition::TTD { activation_block_number: 0, fork_block: Some(0), total_difficulty: U256::from(0) }, + ForkCondition::TTD { + activation_block_number: 0, + fork_block: Some(0), + total_difficulty: U256::from(0), + }, ), ]), deposit_contract: None, @@ -347,54 +369,100 @@ impl BitfinityEvmClient { self } + /// Check if the RPC endpoint is producing new blocks + async fn is_producing_blocks( + client: &EthJsonRpcClient, + max_block_age_secs: Duration, + ) -> Result { + debug!(target: "downloaders::bitfinity_evm_client", "Checking if the EVM is producing blocks"); + + let last_block = client + .get_block_by_number(did::BlockNumber::Latest) + .await + .map_err(|e| eyre::eyre!("error getting block number: {}", e))?; + + let block_ts = last_block.timestamp.0.to::(); + + let current_ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Should be able to get the time") + .as_secs(); + + Ok(current_ts - block_ts <= max_block_age_secs.as_secs()) + } + /// Creates a new JSON-RPC client with retry functionality /// - /// Tries the primary URL first, falls back to backup URL if provided + /// Tries the primary URL first, falls back to backup URL if provided or if primary is not producing blocks pub async fn client(config: RpcClientConfig) -> Result> { + // Create retry configuration let backoff = ExponentialBuilder::default() .with_max_times(config.max_retries as _) .with_max_delay(config.retry_delay) .with_jitter(); - // Helper closure to create and test a client connection - let try_connect = move |url: String| { - let backoff = backoff; - let client = EthJsonRpcClient::new(ReqwestClient::new(url)); - - async move { - // Test connection by getting chain ID - (|| async { client.get_chain_id().await }) - .retry(&backoff) - .notify(|e, dur| { - error!( - target: "downloaders::bitfinity_evm_client", - "Failed to connect after {}s and {} retries: {}", - dur.as_secs(), - config.max_retries, - e - ); - }) - .await - .map(|_| client) - } - }; - // Try primary URL first - if let Ok(client) = try_connect(config.primary_url).await { - debug!(target: "downloaders::bitfinity_evm_client", "Connected to primary RPC endpoint"); - return Ok(client); + if let Ok(primary) = Self::connect_and_verify(&config.primary_url, &backoff).await { + if Self::is_producing_blocks(&primary, config.max_block_age_secs).await? { + debug!(target: "downloaders::bitfinity_evm_client", "Using primary RPC endpoint - producing blocks"); + + return Ok(primary); + } } - // Fall back to backup URL if available + // Try backup URL if available if let Some(backup_url) = config.backup_url { - if let Ok(client) = try_connect(backup_url).await { - debug!(target: "downloaders::bitfinity_evm_client", "Connected to backup RPC endpoint"); - return Ok(client); + if let Ok(backup) = Self::connect_and_verify(&backup_url, &backoff).await { + if Self::is_producing_blocks(&backup, config.max_block_age_secs).await? { + warn!(target: "downloaders::bitfinity_evm_client", "Primary RPC endpoint not producing blocks, using backup"); + + return Ok(backup); + } } } + // Fall back to primary if it connected but wasn't producing blocks + if let Ok(primary) = Self::connect_and_verify(&config.primary_url, &backoff).await { + warn!(target: "downloaders::bitfinity_evm_client", "No endpoints producing blocks, using primary as fallback"); + + return Ok(primary); + } + Err(eyre::eyre!("Failed to connect to any RPC endpoint")) } + + /// Helper function to connect to an RPC endpoint and verify the connection + async fn connect_and_verify( + url: &str, + backoff: &ExponentialBuilder, + ) -> Result> { + let client = EthJsonRpcClient::new(ReqwestClient::new(url.to_string())); + + // Test connection by getting chain ID + Ok((|| async { client.get_chain_id().await }) + .retry(backoff) + .notify(|e, dur| { + error!( + target: "downloaders::bitfinity_evm_client", + "Failed to connect to {} after {}s: {}", + url, + dur.as_secs(), + e + ); + }) + .await + .map(|_| client) + .map_err(|e| RemoteClientError::ProviderError(e.to_string()))?) + } + + /// Check if the node is enabled + pub async fn is_evm_enabled(client: &EthJsonRpcClient) -> Result { + let state = client.get_evm_global_state().await.map_err(|e| { + RemoteClientError::ProviderError(format!("failed to get evm global state: {e}")) + })?; + + Ok(state.is_enabled()) + } } struct BlockCertificateChecker { @@ -451,14 +519,21 @@ impl BlockCertificateChecker { })?; let current_time_ns = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("Should be able to get the time") - .as_nanos(); + .duration_since(std::time::UNIX_EPOCH) + .expect("Should be able to get the time") + .as_nanos(); let allowed_certificate_time_offset = Duration::from_secs(120).as_nanos(); - certificate.verify(self.evmc_principal.as_ref(), &self.ic_root_key, ¤t_time_ns, &allowed_certificate_time_offset).map_err(|e| { - RemoteClientError::CertificateError(format!("certificate validation error: {e}")) - })?; + certificate + .verify( + self.evmc_principal.as_ref(), + &self.ic_root_key, + ¤t_time_ns, + &allowed_certificate_time_offset, + ) + .map_err(|e| { + RemoteClientError::CertificateError(format!("certificate validation error: {e}")) + })?; let tree = HashTree::from_cbor(&self.certified_data.witness).map_err(|e| { RemoteClientError::CertificateError(format!("failed to parse witness: {e}")) @@ -591,12 +666,14 @@ mod tests { backup_url: None, max_retries: 3, retry_delay: Duration::from_secs(1), + max_block_age_secs: Duration::from_secs(600), }, 0, Some(5), 5, 1000, None, + false, ) .await .unwrap(); @@ -611,12 +688,14 @@ mod tests { backup_url: Some("https://cloudflare-eth.com".to_string()), max_retries: 3, retry_delay: Duration::from_secs(1), + max_block_age_secs: Duration::from_secs(600), }, 0, Some(5), 5, 1000, None, + false, ) .await .unwrap(); @@ -631,12 +710,14 @@ mod tests { backup_url: None, max_retries: 3, retry_delay: Duration::from_secs(1), + max_block_age_secs: Duration::from_secs(600), }, 0, Some(5), 5, 1000, None, + false, ) .await .unwrap(); @@ -662,12 +743,14 @@ mod tests { backup_url: None, max_retries: 3, retry_delay: Duration::from_secs(1), + max_block_age_secs: Duration::from_secs(600), }, 0, Some(5), 5, 1000, None, + false, ) .await .unwrap(); diff --git a/crates/node/core/src/args/bitfinity_args.rs b/crates/node/core/src/args/bitfinity_args.rs index 029d2079aab..52201a6cb4a 100644 --- a/crates/node/core/src/args/bitfinity_args.rs +++ b/crates/node/core/src/args/bitfinity_args.rs @@ -48,6 +48,11 @@ pub struct BitfinityImportArgs { #[arg(long, value_name = "MAX_FETCH_BLOCKS", default_value = "10000")] pub max_fetch_blocks: u64, + /// Maximum age (in seconds) of the latest block to consider the EVM as active + /// Default: 600 seconds (10 minutes) + #[arg(long, value_name = "MAX_BLOCK_AGE_SECS", default_value = "600")] + pub max_block_age_secs: u64, + /// Canister principal /// Default value corresponds to testnet #[arg(long, value_name = "EVMC_PRINCIPAL", default_value = "4fe7g-7iaaa-aaaak-aegcq-cai")] @@ -56,6 +61,10 @@ pub struct BitfinityImportArgs { /// Root key for the IC network #[arg(long, value_name = "IC_ROOT_KEY", default_value = IC_MAINNET_KEY)] pub ic_root_key: String, + + /// A flag to check the EVM state before importing blocks + #[arg(long, default_value = "true")] + pub check_evm_state_before_importing: bool, } /// Bitfinity Related Args From ce84e339df49ab845ea2583e69a6f40c31e161ec Mon Sep 17 00:00:00 2001 From: Yasir Date: Thu, 30 Jan 2025 22:21:15 +0300 Subject: [PATCH 2/2] Change log level from 'trace' to 'info' in init_logs function --- bin/reth/tests/commands/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/reth/tests/commands/utils.rs b/bin/reth/tests/commands/utils.rs index 935f9b2d34c..94de57b61a1 100644 --- a/bin/reth/tests/commands/utils.rs +++ b/bin/reth/tests/commands/utils.rs @@ -50,7 +50,7 @@ pub fn init_logs() -> eyre::Result> { let mut tracer = RethTracer::new(); let stdout = LayerInfo::new( LogFormat::Terminal, - "trace".to_string(), + "info".to_string(), String::new(), Some("always".to_string()), );