1use crate::{node::NodeTestContext, NodeHelperType, Wallet};
4use reth_chainspec::ChainSpec;
5use reth_cli_commands::import_core::{import_blocks_from_file, ImportConfig};
6use reth_config::Config;
7use reth_db::DatabaseEnv;
8use reth_node_api::{NodeTypesWithDBAdapter, TreeConfig};
9use reth_node_builder::{EngineNodeLauncher, Node, NodeBuilder, NodeConfig, NodeHandle};
10use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs};
11use reth_node_ethereum::EthereumNode;
12use reth_provider::{
13 providers::BlockchainProvider, DatabaseProviderFactory, ProviderFactory, StageCheckpointReader,
14 StaticFileProviderFactory,
15};
16use reth_rpc_server_types::RpcModuleSelection;
17use reth_stages_types::StageId;
18use reth_tasks::TaskManager;
19use std::{path::Path, sync::Arc};
20use tempfile::TempDir;
21use tracing::{debug, info, span, Level};
22
23pub struct ChainImportResult {
25 pub nodes: Vec<NodeHelperType<EthereumNode>>,
27 pub task_manager: TaskManager,
29 pub wallet: Wallet,
31 pub _temp_dirs: Vec<TempDir>,
33}
34
35impl std::fmt::Debug for ChainImportResult {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("ChainImportResult")
38 .field("nodes", &self.nodes.len())
39 .field("wallet", &self.wallet)
40 .field("temp_dirs", &self._temp_dirs.len())
41 .finish()
42 }
43}
44
45pub async fn setup_engine_with_chain_import(
60 num_nodes: usize,
61 chain_spec: Arc<ChainSpec>,
62 is_dev: bool,
63 tree_config: TreeConfig,
64 rlp_path: &Path,
65 attributes_generator: impl Fn(u64) -> reth_payload_builder::EthPayloadBuilderAttributes
66 + Send
67 + Sync
68 + Copy
69 + 'static,
70) -> eyre::Result<ChainImportResult> {
71 let tasks = TaskManager::current();
72 let exec = tasks.executor();
73
74 let network_config = NetworkArgs {
75 discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
76 ..NetworkArgs::default()
77 };
78
79 let mut nodes: Vec<NodeHelperType<EthereumNode>> = Vec::with_capacity(num_nodes);
81 let mut temp_dirs = Vec::with_capacity(num_nodes); for idx in 0..num_nodes {
84 let temp_dir = TempDir::new()?;
86 let datadir = temp_dir.path().to_path_buf();
87
88 let mut node_config = NodeConfig::new(chain_spec.clone())
89 .with_network(network_config.clone())
90 .with_unused_ports()
91 .with_rpc(
92 RpcServerArgs::default()
93 .with_unused_ports()
94 .with_http()
95 .with_http_api(RpcModuleSelection::All),
96 )
97 .set_dev(is_dev);
98
99 node_config.datadir.datadir =
101 reth_node_core::dirs::MaybePlatformPath::from(datadir.clone());
102 debug!(target: "e2e::import", "Node {idx} datadir: {datadir:?}");
103
104 let span = span!(Level::INFO, "node", idx);
105 let _enter = span.enter();
106
107 info!(target: "test", "Importing chain data from {:?} for node {} into {:?}", rlp_path, idx, datadir);
109
110 let db_path = datadir.join("db");
112 let static_files_path = datadir.join("static_files");
113
114 let db_args = reth_node_core::args::DatabaseArgs::default().database_args();
117 let db_env = reth_db::init_db(&db_path, db_args)?;
118 let db = Arc::new(db_env);
119
120 let provider_factory = ProviderFactory::<
123 NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>,
124 >::new(
125 db.clone(),
126 chain_spec.clone(),
127 reth_provider::providers::StaticFileProvider::read_write(static_files_path.clone())?,
128 )?;
129
130 reth_db_common::init::init_genesis(&provider_factory)?;
132
133 let import_config = ImportConfig::default();
136 let config = Config::default();
137
138 let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
140 let consensus = reth_consensus::noop::NoopConsensus::arc();
142
143 let result = import_blocks_from_file(
144 rlp_path,
145 import_config,
146 provider_factory.clone(),
147 &config,
148 evm_config,
149 consensus,
150 )
151 .await?;
152
153 info!(
154 target: "test",
155 "Imported {} blocks and {} transactions for node {}",
156 result.total_imported_blocks,
157 result.total_imported_txns,
158 idx
159 );
160
161 debug!(target: "e2e::import",
162 "Import result for node {}: decoded {} blocks, imported {} blocks, complete: {}",
163 idx,
164 result.total_decoded_blocks,
165 result.total_imported_blocks,
166 result.is_complete()
167 );
168
169 if result.total_decoded_blocks != result.total_imported_blocks {
170 debug!(target: "e2e::import",
171 "Import block count mismatch: decoded {} != imported {}",
172 result.total_decoded_blocks, result.total_imported_blocks
173 );
174 return Err(eyre::eyre!("Chain import block count mismatch for node {}", idx));
175 }
176
177 if result.total_decoded_txns != result.total_imported_txns {
178 debug!(target: "e2e::import",
179 "Import transaction count mismatch: decoded {} != imported {}",
180 result.total_decoded_txns, result.total_imported_txns
181 );
182 return Err(eyre::eyre!("Chain import transaction count mismatch for node {}", idx));
183 }
184
185 {
187 let provider = provider_factory.database_provider_ro()?;
188 let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers)?;
189 if headers_checkpoint.is_none() {
190 return Err(eyre::eyre!("Headers stage checkpoint is missing after import!"));
191 }
192 debug!(target: "e2e::import", "Headers stage checkpoint after import: {headers_checkpoint:?}");
193 drop(provider);
194 }
195
196 {
200 let static_file_provider = provider_factory.static_file_provider();
201 drop(static_file_provider);
203 }
204
205 drop(provider_factory);
207 drop(db);
208
209 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
211
212 debug!(target: "e2e::import", "Launching node with datadir: {:?}", datadir);
214
215 let node = EthereumNode::default();
218
219 let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config.clone())
220 .testing_node_with_datadir(exec.clone(), datadir.clone())
221 .with_types_and_provider::<EthereumNode, BlockchainProvider<_>>()
222 .with_components(node.components_builder())
223 .with_add_ons(node.add_ons())
224 .launch_with_fn(|builder| {
225 let launcher = EngineNodeLauncher::new(
226 builder.task_executor().clone(),
227 builder.config().datadir(),
228 tree_config.clone(),
229 );
230 builder.launch_with(launcher)
231 })
232 .await?;
233
234 let node_ctx = NodeTestContext::new(node, attributes_generator).await?;
235
236 nodes.push(node_ctx);
237 temp_dirs.push(temp_dir); }
239
240 Ok(ChainImportResult {
241 nodes,
242 task_manager: tasks,
243 wallet: crate::Wallet::default().with_chain_id(chain_spec.chain.id()),
244 _temp_dirs: temp_dirs,
245 })
246}
247
248pub fn load_forkchoice_state(path: &Path) -> eyre::Result<alloy_rpc_types_engine::ForkchoiceState> {
250 let json_str = std::fs::read_to_string(path)?;
251 let fcu_data: serde_json::Value = serde_json::from_str(&json_str)?;
252
253 let state = &fcu_data["params"][0];
255 Ok(alloy_rpc_types_engine::ForkchoiceState {
256 head_block_hash: state["headBlockHash"]
257 .as_str()
258 .ok_or_else(|| eyre::eyre!("missing headBlockHash"))?
259 .parse()?,
260 safe_block_hash: state["safeBlockHash"]
261 .as_str()
262 .ok_or_else(|| eyre::eyre!("missing safeBlockHash"))?
263 .parse()?,
264 finalized_block_hash: state["finalizedBlockHash"]
265 .as_str()
266 .ok_or_else(|| eyre::eyre!("missing finalizedBlockHash"))?
267 .parse()?,
268 })
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp};
275 use reth_chainspec::{ChainSpecBuilder, MAINNET};
276 use reth_db::mdbx::DatabaseArguments;
277 use reth_payload_builder::EthPayloadBuilderAttributes;
278 use reth_primitives::SealedBlock;
279 use reth_provider::{
280 test_utils::MockNodeTypesWithDB, BlockHashReader, BlockNumReader, BlockReaderIdExt,
281 };
282 use std::path::PathBuf;
283
284 #[tokio::test]
285 async fn test_stage_checkpoints_persistence() {
286 reth_tracing::init_test_tracing();
289
290 let chain_spec = Arc::new(
291 ChainSpecBuilder::default()
292 .chain(MAINNET.chain)
293 .genesis(
294 serde_json::from_str(include_str!("testsuite/assets/genesis.json")).unwrap(),
295 )
296 .london_activated()
297 .shanghai_activated()
298 .build(),
299 );
300
301 let test_blocks = generate_test_blocks(&chain_spec, 5);
303
304 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
306 let rlp_path = temp_dir.path().join("test_chain.rlp");
307 write_blocks_to_rlp(&test_blocks, &rlp_path).expect("Failed to write RLP data");
308
309 let datadir = temp_dir.path().join("datadir");
311 std::fs::create_dir_all(&datadir).unwrap();
312 let db_path = datadir.join("db");
313 let static_files_path = datadir.join("static_files");
314
315 {
317 let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
318 let db = Arc::new(db_env);
319
320 let provider_factory: ProviderFactory<
321 NodeTypesWithDBAdapter<reth_node_ethereum::EthereumNode, Arc<DatabaseEnv>>,
322 > = ProviderFactory::new(
323 db.clone(),
324 chain_spec.clone(),
325 reth_provider::providers::StaticFileProvider::read_write(static_files_path.clone())
326 .unwrap(),
327 )
328 .expect("failed to create provider factory");
329
330 reth_db_common::init::init_genesis(&provider_factory).unwrap();
332
333 let import_config = ImportConfig::default();
335 let config = Config::default();
336 let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
337 let consensus = reth_consensus::noop::NoopConsensus::arc();
339
340 let result = import_blocks_from_file(
341 &rlp_path,
342 import_config,
343 provider_factory.clone(),
344 &config,
345 evm_config,
346 consensus,
347 )
348 .await
349 .unwrap();
350
351 assert_eq!(result.total_decoded_blocks, 5);
352 assert_eq!(result.total_imported_blocks, 5);
353
354 let provider = provider_factory.database_provider_ro().unwrap();
356 let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers).unwrap();
357 assert!(headers_checkpoint.is_some(), "Headers checkpoint should exist after import");
358 assert_eq!(
359 headers_checkpoint.unwrap().block_number,
360 5,
361 "Headers checkpoint should be at block 5"
362 );
363 drop(provider);
364
365 let static_file_provider = provider_factory.static_file_provider();
367 drop(static_file_provider);
368
369 drop(provider_factory);
370 drop(db);
371 }
372
373 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
375
376 {
378 let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
379 let db = Arc::new(db_env);
380
381 let provider_factory: ProviderFactory<
382 NodeTypesWithDBAdapter<reth_node_ethereum::EthereumNode, Arc<DatabaseEnv>>,
383 > = ProviderFactory::new(
384 db,
385 chain_spec.clone(),
386 reth_provider::providers::StaticFileProvider::read_only(static_files_path, false)
387 .unwrap(),
388 )
389 .expect("failed to create provider factory");
390
391 let provider = provider_factory.database_provider_ro().unwrap();
392
393 let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers).unwrap();
395 assert!(
396 headers_checkpoint.is_some(),
397 "Headers checkpoint should still exist after reopening database"
398 );
399 assert_eq!(
400 headers_checkpoint.unwrap().block_number,
401 5,
402 "Headers checkpoint should still be at block 5"
403 );
404
405 let block_5_hash = provider.block_hash(5).unwrap();
407 assert!(block_5_hash.is_some(), "Block 5 should exist in database");
408 assert_eq!(block_5_hash.unwrap(), test_blocks[4].hash(), "Block 5 hash should match");
409
410 debug!(target: "e2e::import", "All stage checkpoints after reopening:");
412 for stage in StageId::ALL {
413 let checkpoint = provider.get_stage_checkpoint(stage).unwrap();
414 debug!(target: "e2e::import", " Stage {stage:?}: {checkpoint:?}");
415 }
416 }
417 }
418
419 fn create_test_chain_spec() -> Arc<ChainSpec> {
421 Arc::new(
422 ChainSpecBuilder::default()
423 .chain(MAINNET.chain)
424 .genesis(
425 serde_json::from_str(include_str!("testsuite/assets/genesis.json")).unwrap(),
426 )
427 .london_activated()
428 .shanghai_activated()
429 .build(),
430 )
431 }
432
433 async fn setup_test_blocks_and_rlp(
435 chain_spec: &ChainSpec,
436 block_count: u64,
437 temp_dir: &Path,
438 ) -> (Vec<SealedBlock>, PathBuf) {
439 let test_blocks = generate_test_blocks(chain_spec, block_count);
440 assert_eq!(
441 test_blocks.len(),
442 block_count as usize,
443 "Should have generated expected blocks"
444 );
445
446 let rlp_path = temp_dir.join("test_chain.rlp");
447 write_blocks_to_rlp(&test_blocks, &rlp_path).expect("Failed to write RLP data");
448
449 let rlp_size = std::fs::metadata(&rlp_path).expect("RLP file should exist").len();
450 debug!(target: "e2e::import", "Wrote RLP file with size: {rlp_size} bytes");
451
452 (test_blocks, rlp_path)
453 }
454
455 #[tokio::test]
456 async fn test_import_blocks_only() {
457 reth_tracing::init_test_tracing();
459
460 let chain_spec = create_test_chain_spec();
461 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
462 let (test_blocks, rlp_path) =
463 setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path()).await;
464
465 let datadir = temp_dir.path().join("datadir");
467 std::fs::create_dir_all(&datadir).unwrap();
468 let db_path = datadir.join("db");
469 let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
470 let db = Arc::new(reth_db::test_utils::TempDatabase::new(db_env, db_path));
471
472 let static_files_path = datadir.join("static_files");
474
475 let provider_factory: ProviderFactory<MockNodeTypesWithDB> = ProviderFactory::new(
477 db.clone(),
478 chain_spec.clone(),
479 reth_provider::providers::StaticFileProvider::read_write(static_files_path).unwrap(),
480 )
481 .expect("failed to create provider factory");
482
483 reth_db_common::init::init_genesis(&provider_factory).unwrap();
485
486 let import_config = ImportConfig::default();
488 let config = Config::default();
489 let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
490 let consensus = reth_consensus::noop::NoopConsensus::arc();
492
493 let result = import_blocks_from_file(
494 &rlp_path,
495 import_config,
496 provider_factory.clone(),
497 &config,
498 evm_config,
499 consensus,
500 )
501 .await
502 .unwrap();
503
504 debug!(target: "e2e::import",
505 "Import result: decoded {} blocks, imported {} blocks",
506 result.total_decoded_blocks, result.total_imported_blocks
507 );
508
509 assert_eq!(result.total_decoded_blocks, 10);
511 assert_eq!(result.total_imported_blocks, 10);
512 assert_eq!(result.total_decoded_txns, 0);
513 assert_eq!(result.total_imported_txns, 0);
514
515 let provider = provider_factory.database_provider_ro().unwrap();
517 let latest_block = provider.last_block_number().unwrap();
518 assert_eq!(latest_block, 10, "Should have imported up to block 10");
519
520 let block_10_hash = provider.block_hash(10).unwrap().expect("Block 10 should exist");
521 assert_eq!(block_10_hash, test_blocks[9].hash(), "Block 10 hash should match");
522 }
523
524 #[tokio::test]
525 async fn test_import_with_node_integration() {
526 reth_tracing::init_test_tracing();
528
529 let chain_spec = create_test_chain_spec();
530 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
531 let (test_blocks, rlp_path) =
532 setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path()).await;
533
534 let tip = test_blocks.last().expect("Should have generated blocks");
536 let fcu_path = temp_dir.path().join("test_fcu.json");
537 std::fs::write(&fcu_path, create_fcu_json(tip).to_string())
538 .expect("Failed to write FCU data");
539
540 let result = setup_engine_with_chain_import(
542 1,
543 chain_spec,
544 false,
545 TreeConfig::default(),
546 &rlp_path,
547 |_| EthPayloadBuilderAttributes::default(),
548 )
549 .await
550 .expect("Failed to setup nodes with chain import");
551
552 let fcu_state = load_forkchoice_state(&fcu_path).expect("Failed to load forkchoice state");
554
555 let node = &result.nodes[0];
556
557 node.update_forkchoice(fcu_state.finalized_block_hash, fcu_state.head_block_hash)
559 .await
560 .expect("Failed to update forkchoice");
561
562 node.sync_to(fcu_state.head_block_hash).await.expect("Failed to sync to head");
564
565 let latest = node
567 .inner
568 .provider
569 .sealed_header_by_id(alloy_eips::BlockId::latest())
570 .expect("Failed to get latest header")
571 .expect("No latest header found");
572
573 assert_eq!(
574 latest.hash(),
575 fcu_state.head_block_hash,
576 "Chain tip does not match expected head"
577 );
578 }
579}