reth_e2e_test_utils/
setup_import.rs

1//! Setup utilities for importing RLP chain data before starting nodes.
2
3use 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
23/// Setup result containing nodes and temporary directories that must be kept alive
24pub struct ChainImportResult {
25    /// The nodes that were created
26    pub nodes: Vec<NodeHelperType<EthereumNode>>,
27    /// The task manager
28    pub task_manager: TaskManager,
29    /// The wallet for testing
30    pub wallet: Wallet,
31    /// Temporary directories that must be kept alive for the duration of the test
32    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
45/// Creates a test setup with Ethereum nodes that have pre-imported chain data from RLP files.
46///
47/// This function:
48/// 1. Creates a temporary datadir for each node
49/// 2. Imports the specified RLP chain data into the datadir
50/// 3. Starts the nodes with the pre-populated database
51/// 4. Returns the running nodes ready for testing
52///
53/// Note: This function is currently specific to `EthereumNode` because the import process
54/// uses Ethereum-specific consensus and block format. It can be made generic in the future
55/// by abstracting the import process.
56/// It uses `NoopConsensus` during import to bypass validation checks like gas limit constraints,
57/// which allows importing test chains that may not strictly conform to mainnet consensus rules. The
58/// nodes themselves still run with proper consensus when started.
59pub 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    // Create nodes with imported data
80    let mut nodes: Vec<NodeHelperType<EthereumNode>> = Vec::with_capacity(num_nodes);
81    let mut temp_dirs = Vec::with_capacity(num_nodes); // Keep temp dirs alive
82
83    for idx in 0..num_nodes {
84        // Create a temporary datadir for this node
85        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        // Set the datadir
100        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        // First, import the chain data into this datadir
108        info!(target: "test", "Importing chain data from {:?} for node {} into {:?}", rlp_path, idx, datadir);
109
110        // Create database path and static files path
111        let db_path = datadir.join("db");
112        let static_files_path = datadir.join("static_files");
113
114        // Initialize the database using init_db (same as CLI import command)
115        // Use the same database arguments as the node will use
116        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        // Create a provider factory with the initialized database (use regular DB, not
121        // TempDatabase) We need to specify the node types properly for the adapter
122        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        // Initialize genesis if needed
131        reth_db_common::init::init_genesis(&provider_factory)?;
132
133        // Import the chain data
134        // Use no_state to skip state validation for test chains
135        let import_config = ImportConfig::default();
136        let config = Config::default();
137
138        // Create EVM and consensus for Ethereum
139        let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
140        // Use NoopConsensus to skip gas limit validation for test imports
141        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        // Verify the database was properly initialized by checking stage checkpoints
186        {
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        // IMPORTANT: We need to properly flush and close the static files provider
197        // The static files provider may have open file handles that need to be closed
198        // before we can reopen the database in the node launcher
199        {
200            let static_file_provider = provider_factory.static_file_provider();
201            // This will ensure all static file writers are properly closed
202            drop(static_file_provider);
203        }
204
205        // Close all database handles to release locks before launching the node
206        drop(provider_factory);
207        drop(db);
208
209        // Give the OS a moment to release file locks
210        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
211
212        // Now launch the node with the pre-populated datadir
213        debug!(target: "e2e::import", "Launching node with datadir: {:?}", datadir);
214
215        // Use the testing_node_with_datadir method which properly handles opening existing
216        // databases
217        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); // Keep temp dir alive
238    }
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
248/// Helper to load forkchoice state from a JSON file
249pub 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    // The headfcu.json file contains a JSON-RPC request with the forkchoice state in params[0]
254    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        // This test specifically verifies that stage checkpoints are persisted correctly
287        // when reopening the database
288        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        // Generate test blocks
302        let test_blocks = generate_test_blocks(&chain_spec, 5);
303
304        // Create temporary files for RLP data
305        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        // Create a persistent datadir that won't be deleted
310        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        // Import the chain
316        {
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
329            // Initialize genesis
330            reth_db_common::init::init_genesis(&provider_factory).unwrap();
331
332            // Import the chain data
333            let import_config = ImportConfig::default();
334            let config = Config::default();
335            let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
336            // Use NoopConsensus to skip gas limit validation for test imports
337            let consensus = reth_consensus::noop::NoopConsensus::arc();
338
339            let result = import_blocks_from_file(
340                &rlp_path,
341                import_config,
342                provider_factory.clone(),
343                &config,
344                evm_config,
345                consensus,
346            )
347            .await
348            .unwrap();
349
350            assert_eq!(result.total_decoded_blocks, 5);
351            assert_eq!(result.total_imported_blocks, 5);
352
353            // Verify stage checkpoints exist
354            let provider = provider_factory.database_provider_ro().unwrap();
355            let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers).unwrap();
356            assert!(headers_checkpoint.is_some(), "Headers checkpoint should exist after import");
357            assert_eq!(
358                headers_checkpoint.unwrap().block_number,
359                5,
360                "Headers checkpoint should be at block 5"
361            );
362            drop(provider);
363
364            // Properly close static files to release all file handles
365            let static_file_provider = provider_factory.static_file_provider();
366            drop(static_file_provider);
367
368            drop(provider_factory);
369            drop(db);
370        }
371
372        // Give the OS a moment to release file locks
373        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
374
375        // Now reopen the database and verify checkpoints are still there
376        {
377            let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
378            let db = Arc::new(db_env);
379
380            let provider_factory: ProviderFactory<
381                NodeTypesWithDBAdapter<reth_node_ethereum::EthereumNode, Arc<DatabaseEnv>>,
382            > = ProviderFactory::new(
383                db,
384                chain_spec.clone(),
385                reth_provider::providers::StaticFileProvider::read_only(static_files_path, false)
386                    .unwrap(),
387            );
388
389            let provider = provider_factory.database_provider_ro().unwrap();
390
391            // Check that stage checkpoints are still present
392            let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers).unwrap();
393            assert!(
394                headers_checkpoint.is_some(),
395                "Headers checkpoint should still exist after reopening database"
396            );
397            assert_eq!(
398                headers_checkpoint.unwrap().block_number,
399                5,
400                "Headers checkpoint should still be at block 5"
401            );
402
403            // Verify we can read blocks
404            let block_5_hash = provider.block_hash(5).unwrap();
405            assert!(block_5_hash.is_some(), "Block 5 should exist in database");
406            assert_eq!(block_5_hash.unwrap(), test_blocks[4].hash(), "Block 5 hash should match");
407
408            // Check all stage checkpoints
409            debug!(target: "e2e::import", "All stage checkpoints after reopening:");
410            for stage in StageId::ALL {
411                let checkpoint = provider.get_stage_checkpoint(stage).unwrap();
412                debug!(target: "e2e::import", "  Stage {stage:?}: {checkpoint:?}");
413            }
414        }
415    }
416
417    /// Helper to create test chain spec
418    fn create_test_chain_spec() -> Arc<ChainSpec> {
419        Arc::new(
420            ChainSpecBuilder::default()
421                .chain(MAINNET.chain)
422                .genesis(
423                    serde_json::from_str(include_str!("testsuite/assets/genesis.json")).unwrap(),
424                )
425                .london_activated()
426                .shanghai_activated()
427                .build(),
428        )
429    }
430
431    /// Helper to setup test blocks and write to RLP
432    async fn setup_test_blocks_and_rlp(
433        chain_spec: &ChainSpec,
434        block_count: u64,
435        temp_dir: &Path,
436    ) -> (Vec<SealedBlock>, PathBuf) {
437        let test_blocks = generate_test_blocks(chain_spec, block_count);
438        assert_eq!(
439            test_blocks.len(),
440            block_count as usize,
441            "Should have generated expected blocks"
442        );
443
444        let rlp_path = temp_dir.join("test_chain.rlp");
445        write_blocks_to_rlp(&test_blocks, &rlp_path).expect("Failed to write RLP data");
446
447        let rlp_size = std::fs::metadata(&rlp_path).expect("RLP file should exist").len();
448        debug!(target: "e2e::import", "Wrote RLP file with size: {rlp_size} bytes");
449
450        (test_blocks, rlp_path)
451    }
452
453    #[tokio::test]
454    async fn test_import_blocks_only() {
455        // Tests just the block import functionality without full node setup
456        reth_tracing::init_test_tracing();
457
458        let chain_spec = create_test_chain_spec();
459        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
460        let (test_blocks, rlp_path) =
461            setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path()).await;
462
463        // Create a test database
464        let datadir = temp_dir.path().join("datadir");
465        std::fs::create_dir_all(&datadir).unwrap();
466        let db_path = datadir.join("db");
467        let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
468        let db = Arc::new(reth_db::test_utils::TempDatabase::new(db_env, db_path));
469
470        // Create static files path
471        let static_files_path = datadir.join("static_files");
472
473        // Create a provider factory
474        let provider_factory: ProviderFactory<MockNodeTypesWithDB> = ProviderFactory::new(
475            db.clone(),
476            chain_spec.clone(),
477            reth_provider::providers::StaticFileProvider::read_write(static_files_path).unwrap(),
478        );
479
480        // Initialize genesis
481        reth_db_common::init::init_genesis(&provider_factory).unwrap();
482
483        // Import the chain data
484        let import_config = ImportConfig::default();
485        let config = Config::default();
486        let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
487        // Use NoopConsensus to skip gas limit validation for test imports
488        let consensus = reth_consensus::noop::NoopConsensus::arc();
489
490        let result = import_blocks_from_file(
491            &rlp_path,
492            import_config,
493            provider_factory.clone(),
494            &config,
495            evm_config,
496            consensus,
497        )
498        .await
499        .unwrap();
500
501        debug!(target: "e2e::import",
502            "Import result: decoded {} blocks, imported {} blocks",
503            result.total_decoded_blocks, result.total_imported_blocks
504        );
505
506        // Verify the import was successful
507        assert_eq!(result.total_decoded_blocks, 10);
508        assert_eq!(result.total_imported_blocks, 10);
509        assert_eq!(result.total_decoded_txns, 0);
510        assert_eq!(result.total_imported_txns, 0);
511
512        // Verify we can read the imported blocks
513        let provider = provider_factory.database_provider_ro().unwrap();
514        let latest_block = provider.last_block_number().unwrap();
515        assert_eq!(latest_block, 10, "Should have imported up to block 10");
516
517        let block_10_hash = provider.block_hash(10).unwrap().expect("Block 10 should exist");
518        assert_eq!(block_10_hash, test_blocks[9].hash(), "Block 10 hash should match");
519    }
520
521    #[tokio::test]
522    async fn test_import_with_node_integration() {
523        // Tests the full integration with node setup, forkchoice updates, and syncing
524        reth_tracing::init_test_tracing();
525
526        let chain_spec = create_test_chain_spec();
527        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
528        let (test_blocks, rlp_path) =
529            setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path()).await;
530
531        // Create FCU data for the tip
532        let tip = test_blocks.last().expect("Should have generated blocks");
533        let fcu_path = temp_dir.path().join("test_fcu.json");
534        std::fs::write(&fcu_path, create_fcu_json(tip).to_string())
535            .expect("Failed to write FCU data");
536
537        // Setup nodes with imported chain
538        let result = setup_engine_with_chain_import(
539            1,
540            chain_spec,
541            false,
542            TreeConfig::default(),
543            &rlp_path,
544            |_| EthPayloadBuilderAttributes::default(),
545        )
546        .await
547        .expect("Failed to setup nodes with chain import");
548
549        // Load and apply forkchoice state
550        let fcu_state = load_forkchoice_state(&fcu_path).expect("Failed to load forkchoice state");
551
552        let node = &result.nodes[0];
553
554        // Send forkchoice update to make the imported chain canonical
555        node.update_forkchoice(fcu_state.finalized_block_hash, fcu_state.head_block_hash)
556            .await
557            .expect("Failed to update forkchoice");
558
559        // Wait for the node to sync to the head
560        node.sync_to(fcu_state.head_block_hash).await.expect("Failed to sync to head");
561
562        // Verify the chain tip
563        let latest = node
564            .inner
565            .provider
566            .sealed_header_by_id(alloy_eips::BlockId::latest())
567            .expect("Failed to get latest header")
568            .expect("No latest header found");
569
570        assert_eq!(
571            latest.hash(),
572            fcu_state.head_block_hash,
573            "Chain tip does not match expected head"
574        );
575    }
576}