1use crate::{
4 eth_payload_attributes,
5 setup_builder::{launch_test_node, open_test_database, test_node_config, LaunchArgs},
6 wallet::Wallet,
7 NodeHelperType,
8};
9use eyre::WrapErr;
10use reth_chainspec::ChainSpec;
11use reth_cli_commands::import_core::{import_blocks_from_file, ImportConfig, ImportResult};
12use reth_config::Config;
13use reth_db::DatabaseEnv;
14use reth_node_api::{NodeTypesWithDBAdapter, TreeConfig};
15use reth_node_builder::sync::PipelineBackfill;
16use reth_node_core::args::StorageArgs;
17use reth_node_ethereum::EthereumNode;
18use reth_provider::{
19 providers::{RocksDBProvider, StaticFileProvider},
20 DatabaseProviderFactory, ProviderFactory, StageCheckpointReader, StorageSettings,
21};
22use reth_stages_types::StageId;
23use reth_tasks::Runtime;
24use std::{path::Path, sync::Arc};
25use tempfile::TempDir;
26use tracing::{debug, info, span, Instrument, Level};
27
28pub struct ChainImportResult {
30 pub nodes: Vec<NodeHelperType<EthereumNode>>,
32 pub wallet: Wallet,
34 pub _temp_dirs: Vec<TempDir>,
36}
37
38impl std::fmt::Debug for ChainImportResult {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("ChainImportResult")
41 .field("nodes", &self.nodes.len())
42 .field("wallet", &self.wallet)
43 .field("temp_dirs", &self._temp_dirs.len())
44 .finish()
45 }
46}
47
48pub async fn setup_engine_with_chain_import(
67 num_nodes: usize,
68 chain_spec: Arc<ChainSpec>,
69 is_dev: bool,
70 storage_v2: bool,
71 tree_config: TreeConfig,
72 rlp_path: &Path,
73) -> eyre::Result<ChainImportResult> {
74 let runtime = Runtime::test();
75 let attributes_generator = {
76 let chain_spec = chain_spec.clone();
77 Arc::new(move |timestamp| eth_payload_attributes(&chain_spec, timestamp))
78 };
79
80 let mut nodes = Vec::with_capacity(num_nodes);
82 let mut temp_dirs = Vec::with_capacity(num_nodes);
84
85 for idx in 0..num_nodes {
86 let temp_dir = TempDir::new()?;
88 let datadir = temp_dir.path().to_path_buf();
89 debug!(target: "e2e::import", "Node {idx} datadir: {datadir:?}");
90
91 let span = span!(Level::INFO, "node", idx);
92 let node_config = test_node_config(chain_spec.clone())
93 .set_dev(is_dev)
94 .with_storage(StorageArgs { v2: storage_v2 });
95
96 import_chain(
98 &datadir,
99 chain_spec.clone(),
100 node_config.storage_settings(),
101 rlp_path,
102 runtime.clone(),
103 )
104 .instrument(span.clone())
105 .await
106 .wrap_err_with(|| format!("chain import failed for node {idx}"))?;
107
108 debug!(target: "e2e::import", "Launching node with datadir: {:?}", datadir);
110
111 let database = open_test_database(&datadir)?;
112 let node = launch_test_node(
113 LaunchArgs {
114 idx,
115 node_factory: Arc::new(|_| EthereumNode::default()),
116 node_builder_modifiers: Vec::new(),
117 node_config,
118 runtime: Some(runtime.clone()),
119 tree_config: tree_config.clone(),
120 datadir,
121 attributes_generator: attributes_generator.clone(),
122 dev_payload_attributes: None,
123 },
124 database,
125 PipelineBackfill,
126 )
127 .instrument(span)
128 .await?;
129
130 nodes.push(node);
131 temp_dirs.push(temp_dir);
132 }
133
134 Ok(ChainImportResult {
135 nodes,
136 wallet: Wallet::default().with_chain_id(chain_spec.chain.id()),
137 _temp_dirs: temp_dirs,
138 })
139}
140
141pub fn load_forkchoice_state(path: &Path) -> eyre::Result<alloy_rpc_types_engine::ForkchoiceState> {
143 let json_str = std::fs::read_to_string(path)?;
144 let fcu_data: serde_json::Value = serde_json::from_str(&json_str)?;
145
146 let state = &fcu_data["params"][0];
148 Ok(alloy_rpc_types_engine::ForkchoiceState {
149 head_block_hash: state["headBlockHash"]
150 .as_str()
151 .ok_or_else(|| eyre::eyre!("missing headBlockHash"))?
152 .parse()?,
153 safe_block_hash: state["safeBlockHash"]
154 .as_str()
155 .ok_or_else(|| eyre::eyre!("missing safeBlockHash"))?
156 .parse()?,
157 finalized_block_hash: state["finalizedBlockHash"]
158 .as_str()
159 .ok_or_else(|| eyre::eyre!("missing finalizedBlockHash"))?
160 .parse()?,
161 })
162}
163
164async fn import_chain(
170 datadir: &Path,
171 chain_spec: Arc<ChainSpec>,
172 storage_settings: StorageSettings,
173 rlp_path: &Path,
174 runtime: Runtime,
175) -> eyre::Result<ImportResult> {
176 info!(target: "test", "Importing chain data from {:?} into {:?}", rlp_path, datadir);
177
178 let db_args = reth_node_core::args::DatabaseArgs::default().database_args();
180 let db = reth_db::init_db(datadir.join("db"), db_args)?;
181
182 let provider_factory =
184 ProviderFactory::<NodeTypesWithDBAdapter<EthereumNode, DatabaseEnv>>::new(
185 db.clone(),
186 chain_spec.clone(),
187 StaticFileProvider::read_write(datadir.join("static_files"))?,
188 RocksDBProvider::builder(datadir.join("rocksdb")).with_default_tables().build()?,
189 runtime.clone(),
190 )?;
191
192 reth_db_common::init::init_genesis_with_settings(&provider_factory, storage_settings)?;
194
195 let result = import_blocks_from_file(
197 rlp_path,
198 ImportConfig::default(),
199 provider_factory.clone(),
200 &Config::default(),
201 reth_node_ethereum::EthEvmConfig::new(chain_spec),
202 reth_consensus::noop::NoopConsensus::arc(),
203 runtime,
204 )
205 .await?;
206
207 info!(
208 target: "test",
209 "Imported {} blocks and {} transactions",
210 result.total_imported_blocks,
211 result.total_imported_txns,
212 );
213
214 debug!(target: "e2e::import",
215 "Import result: decoded {} blocks, imported {} blocks, complete: {}",
216 result.total_decoded_blocks,
217 result.total_imported_blocks,
218 result.is_complete()
219 );
220
221 eyre::ensure!(
222 result.total_decoded_blocks == result.total_imported_blocks,
223 "block count mismatch: decoded {} != imported {}",
224 result.total_decoded_blocks,
225 result.total_imported_blocks
226 );
227 eyre::ensure!(
228 result.total_decoded_txns == result.total_imported_txns,
229 "transaction count mismatch: decoded {} != imported {}",
230 result.total_decoded_txns,
231 result.total_imported_txns
232 );
233
234 let headers_checkpoint =
236 provider_factory.database_provider_ro()?.get_stage_checkpoint(StageId::Headers)?;
237 eyre::ensure!(headers_checkpoint.is_some(), "Headers stage checkpoint is missing after import");
238 debug!(target: "e2e::import", "Headers stage checkpoint after import: {headers_checkpoint:?}");
239
240 drop(provider_factory);
242 drop(db);
243
244 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
248
249 Ok(result)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::{
256 test_chain_spec,
257 test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp},
258 };
259 use reth_chainspec::EthereumHardfork;
260 use reth_db::mdbx::DatabaseArguments;
261 use reth_ethereum_primitives::Block;
262 use reth_primitives_traits::SealedBlock;
263 use reth_provider::{BlockHashReader, BlockNumReader, BlockReaderIdExt, MetadataProvider};
264 use std::path::PathBuf;
265
266 fn setup_test_blocks_and_rlp(
268 chain_spec: &ChainSpec,
269 block_count: u64,
270 temp_dir: &Path,
271 ) -> (Vec<SealedBlock<Block>>, PathBuf) {
272 let test_blocks = generate_test_blocks(chain_spec, block_count);
273 assert_eq!(
274 test_blocks.len(),
275 block_count as usize,
276 "Should have generated expected blocks"
277 );
278
279 let rlp_path = temp_dir.join("test_chain.rlp");
280 write_blocks_to_rlp(&test_blocks, &rlp_path).expect("Failed to write RLP data");
281
282 let rlp_size = std::fs::metadata(&rlp_path).expect("RLP file should exist").len();
283 debug!(target: "e2e::import", "Wrote RLP file with size: {rlp_size} bytes");
284
285 (test_blocks, rlp_path)
286 }
287
288 fn reopen_provider_factory(
290 datadir: &Path,
291 chain_spec: Arc<ChainSpec>,
292 runtime: Runtime,
293 ) -> ProviderFactory<NodeTypesWithDBAdapter<EthereumNode, DatabaseEnv>> {
294 let db = reth_db::init_db(datadir.join("db"), DatabaseArguments::default()).unwrap();
295 ProviderFactory::new(
296 db,
297 chain_spec,
298 StaticFileProvider::read_only(datadir.join("static_files")).unwrap(),
299 RocksDBProvider::builder(datadir.join("rocksdb"))
300 .with_default_tables()
301 .build()
302 .unwrap(),
303 runtime,
304 )
305 .expect("failed to create provider factory")
306 }
307
308 #[tokio::test]
309 async fn test_import_blocks_and_reopen() {
310 reth_tracing::init_test_tracing();
313
314 let runtime = Runtime::test();
315 let chain_spec = test_chain_spec(EthereumHardfork::Shanghai);
316 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
317 let (test_blocks, rlp_path) = setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path());
318
319 let datadir = temp_dir.path().join("datadir");
320 std::fs::create_dir_all(&datadir).unwrap();
321 let storage_settings = StorageSettings { storage_v2: StorageArgs::default().v2 };
322 let result = import_chain(
323 &datadir,
324 chain_spec.clone(),
325 storage_settings,
326 &rlp_path,
327 runtime.clone(),
328 )
329 .await
330 .unwrap();
331 assert_eq!(result.total_decoded_blocks, 10);
332 assert_eq!(result.total_imported_blocks, 10);
333 assert_eq!(result.total_decoded_txns, 0);
334 assert_eq!(result.total_imported_txns, 0);
335
336 let provider_factory = reopen_provider_factory(&datadir, chain_spec, runtime);
337 let provider = provider_factory.database_provider_ro().unwrap();
338 assert_eq!(provider.last_block_number().unwrap(), 10);
339 assert_eq!(provider.block_hash(10).unwrap(), Some(test_blocks[9].hash()));
340 let headers_checkpoint = provider.get_stage_checkpoint(StageId::Headers).unwrap();
341 assert_eq!(headers_checkpoint.map(|checkpoint| checkpoint.block_number), Some(10));
342 }
343
344 #[tokio::test]
345 async fn test_import_with_node_integration() {
346 reth_tracing::init_test_tracing();
348
349 let chain_spec = test_chain_spec(EthereumHardfork::Shanghai);
350 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
351 let (test_blocks, rlp_path) = setup_test_blocks_and_rlp(&chain_spec, 10, temp_dir.path());
352
353 let tip = test_blocks.last().expect("Should have generated blocks");
355 let fcu_path = temp_dir.path().join("test_fcu.json");
356 std::fs::write(&fcu_path, create_fcu_json(tip).to_string())
357 .expect("Failed to write FCU data");
358
359 for storage_v2 in [false, true] {
360 let result = setup_engine_with_chain_import(
362 1,
363 chain_spec.clone(),
364 false,
365 storage_v2,
366 TreeConfig::default(),
367 &rlp_path,
368 )
369 .await
370 .expect("Failed to setup nodes with chain import");
371
372 let fcu_state =
374 load_forkchoice_state(&fcu_path).expect("Failed to load forkchoice state");
375
376 let node = &result.nodes[0];
377
378 let settings = node
380 .inner
381 .provider
382 .database_provider_ro()
383 .expect("Failed to open database provider")
384 .storage_settings()
385 .expect("Failed to read storage settings");
386 assert_eq!(settings, Some(StorageSettings { storage_v2 }));
387
388 node.update_forkchoice(fcu_state.finalized_block_hash, fcu_state.head_block_hash)
390 .await
391 .expect("Failed to update forkchoice");
392
393 node.sync_to(fcu_state.head_block_hash).await.expect("Failed to sync to head");
395
396 let latest = node
398 .inner
399 .provider
400 .sealed_header_by_id(alloy_eips::BlockId::latest())
401 .expect("Failed to get latest header")
402 .expect("No latest header found");
403
404 assert_eq!(
405 latest.hash(),
406 fcu_state.head_block_hash,
407 "Chain tip does not match expected head"
408 );
409 }
410 }
411}