Skip to main content

reth_node_builder/launch/
engine.rs

1//! Engine node related functionality.
2
3use crate::{
4    common::{Attached, LaunchContextWith, WithConfigs},
5    hooks::NodeHooks,
6    rpc::{EngineShutdown, EngineValidatorAddOn, EngineValidatorBuilder, RethRpcAddOns, RpcHandle},
7    setup::build_networked_pipeline,
8    AddOns, AddOnsContext, FullNode, LaunchContext, LaunchNode, Node, NodeAdapter,
9    NodeBuilderWithComponents, NodeComponents, NodeComponentsBuilder, NodeHandle, NodeTypesAdapter,
10    RethFullAdapter,
11};
12use alloy_consensus::BlockHeader;
13use futures::{stream::FusedStream, stream_select, FutureExt, StreamExt};
14use reth_chainspec::{EthChainSpec, EthereumHardforks};
15use reth_db::{database_metrics::DatabaseMetrics, Database};
16use reth_engine_tree::{
17    chain::{ChainEvent, FromOrchestrator},
18    engine::{EngineApiKind, EngineApiRequest, EngineRequestHandler},
19    launch::build_engine_orchestrator,
20    tree::TreeConfig,
21};
22use reth_engine_util::EngineMessageStreamExt;
23use reth_exex::ExExManagerHandle;
24use reth_network::{types::BlockRangeUpdate, NetworkSyncUpdater, SyncState};
25use reth_network_api::BlockDownloaderProvider;
26use reth_node_api::{
27    BuiltPayload, ConsensusEngineHandle, FullNodeTypes, NodeTypes, NodeTypesWithDBAdapter,
28};
29use reth_node_core::{
30    args::PruneConfigKind,
31    dirs::{ChainPath, DataDirPath},
32    exit::NodeExitFuture,
33    primitives::Head,
34};
35use reth_node_events::node;
36use reth_provider::{
37    providers::{BlockchainProvider, NodeTypesForProvider},
38    BlockNumReader, StorageSettingsCache,
39};
40use reth_storage_overlay::OverlayManager;
41use reth_tasks::TaskExecutor;
42use reth_tokio_util::EventSender;
43use reth_tracing::tracing::{debug, error, info};
44use std::{future::Future, pin::Pin, sync::Arc};
45use tokio::sync::{mpsc::unbounded_channel, oneshot};
46use tokio_stream::wrappers::UnboundedReceiverStream;
47
48/// The engine node launcher.
49#[derive(Debug)]
50pub struct EngineNodeLauncher {
51    /// The task executor for the node.
52    pub ctx: LaunchContext,
53
54    /// Temporary configuration for engine tree.
55    /// After engine is stabilized, this should be configured through node builder.
56    pub engine_tree_config: TreeConfig,
57}
58
59impl EngineNodeLauncher {
60    /// Create a new instance of the ethereum node launcher.
61    pub const fn new(
62        task_executor: TaskExecutor,
63        data_dir: ChainPath<DataDirPath>,
64        engine_tree_config: TreeConfig,
65    ) -> Self {
66        Self { ctx: LaunchContext::new(task_executor, data_dir), engine_tree_config }
67    }
68
69    async fn launch_node<N, DB, T, CB, AO>(
70        self,
71        target: NodeBuilderWithComponents<T, CB, AO>,
72    ) -> eyre::Result<NodeHandle<NodeAdapter<T, CB::Components>, AO>>
73    where
74        N: Node<RethFullAdapter<DB, N>> + NodeTypesForProvider,
75        DB: Database + DatabaseMetrics + Clone + Unpin + 'static,
76        T: FullNodeTypes<
77            Types = N,
78            Provider = BlockchainProvider<NodeTypesWithDBAdapter<N, DB>>,
79            DB = DB,
80        >,
81        CB: NodeComponentsBuilder<T>,
82        AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
83            + EngineValidatorAddOn<NodeAdapter<T, CB::Components>>,
84    {
85        let Self { ctx, engine_tree_config } = self;
86        let NodeBuilderWithComponents {
87            adapter: NodeTypesAdapter { database },
88            rocksdb_provider,
89            components_builder,
90            add_ons: AddOns { hooks, exexs: installed_exex, add_ons },
91            config,
92        } = target;
93        let NodeHooks { on_component_initialized, on_node_started, .. } = hooks;
94
95        // Create the overlay manager that will be shared across the provider and engine.
96        let overlay_manager = OverlayManager::<N::Primitives>::new(
97            ctx.task_executor.state_trie_overlay_worker_pool(),
98        );
99        let disabled_stages = N::disabled_stages();
100
101        // setup the launch context
102        let ctx = ctx
103            .with_configured_globals(engine_tree_config.reserved_cpu_cores())
104            // load the toml config
105            .with_loaded_toml_config(config)?
106            // add resolved peers
107            .with_resolved_peers()?
108            // attach the database
109            .attach(database.clone())
110            // ensure certain settings take effect
111            .with_adjusted_configs()
112            // Create the provider factory with the shared overlay manager
113            .with_provider_factory::<_, <CB::Components as NodeComponents<T>>::Evm>(
114                overlay_manager.clone(),
115                rocksdb_provider,
116                disabled_stages,
117            )
118            .await?
119            .inspect(|_| {
120                info!(target: "reth::cli", "Database opened");
121            })
122            .with_prometheus_server().await?
123            .inspect(|this| {
124                debug!(target: "reth::cli", chain=%this.chain_id(), genesis=?this.genesis_hash(), "Initializing genesis");
125            })
126            .with_genesis()?
127            .inspect(|this: &LaunchContextWith<Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, _>>| {
128                info!(target: "reth::cli", "\n{}", this.chain_spec().display_hardforks());
129                let settings = this.provider_factory().cached_storage_settings();
130                let pruning_mode =
131                    PruneConfigKind::from_config(&this.prune_config(), this.chain_spec().as_ref()).as_str();
132                info!(target: "reth::cli", ?settings, ?pruning_mode, "Loaded storage settings");
133            })
134            .with_metrics_task()
135            // passing FullNodeTypes as type parameter here so that we can build
136            // later the components.
137            .with_blockchain_db::<T, _>(move |provider_factory| {
138                Ok(BlockchainProvider::new(provider_factory)?)
139            })?
140            .with_components(components_builder, on_component_initialized).await?;
141
142        // spawn exexs if any
143        let maybe_exex_manager_handle = ctx.launch_exex(installed_exex).await?;
144
145        // create pipeline
146        let network_handle = ctx.components().network().clone();
147        let network_client = network_handle.fetch_client().await?;
148        let (consensus_engine_tx, consensus_engine_rx) = unbounded_channel();
149
150        let node_config = ctx.node_config();
151
152        // We always assume that node is syncing after a restart
153        network_handle.update_sync_state(SyncState::Syncing);
154
155        let max_block = ctx.max_block(network_client.clone()).await?;
156
157        let static_file_producer = ctx.static_file_producer();
158        let static_file_producer_events = static_file_producer.lock().events();
159        info!(target: "reth::cli", "StaticFileProducer initialized");
160
161        let consensus = Arc::new(ctx.components().consensus().clone());
162
163        let pipeline = build_networked_pipeline(
164            &ctx.toml_config().stages,
165            network_client.clone(),
166            consensus.clone(),
167            ctx.provider_factory().clone(),
168            ctx.task_executor(),
169            ctx.sync_metrics_tx(),
170            ctx.prune_config(),
171            max_block,
172            static_file_producer,
173            ctx.components().evm_config().clone(),
174            maybe_exex_manager_handle.clone().unwrap_or_else(ExExManagerHandle::empty),
175            ctx.era_import_source(),
176            disabled_stages,
177        )?;
178
179        // The new engine writes directly to static files. This ensures that they're up to the tip.
180        pipeline.move_to_static_files()?;
181
182        let pipeline_events = pipeline.events();
183
184        let mut pruner_builder = ctx.pruner_builder();
185        if let Some(exex_manager_handle) = &maybe_exex_manager_handle {
186            pruner_builder =
187                pruner_builder.finished_exex_height(exex_manager_handle.finished_height());
188        }
189        let pruner = pruner_builder.build_with_provider_factory(ctx.provider_factory().clone());
190        let pruner_events = pruner.events();
191        info!(target: "reth::cli", prune_config=?ctx.prune_config(), "Pruner initialized");
192
193        let event_sender = EventSender::default();
194
195        let beacon_engine_handle = ConsensusEngineHandle::new(consensus_engine_tx.clone());
196
197        // extract the jwt secret from the args if possible
198        let jwt_secret = ctx.auth_jwt_secret()?;
199
200        let add_ons_ctx = AddOnsContext {
201            node: ctx.node_adapter().clone(),
202            config: ctx.node_config(),
203            beacon_engine_handle: beacon_engine_handle.clone(),
204            jwt_secret,
205            engine_events: event_sender.clone(),
206        };
207        let validator_builder = add_ons.engine_validator_builder();
208
209        // Build the engine validator with all required components
210        let engine_validator = validator_builder
211            .clone()
212            .build_tree_validator(&add_ons_ctx, engine_tree_config.clone(), overlay_manager.clone())
213            .await?;
214
215        // Create the consensus engine stream with optional reorg
216        let reorg_overlay_manager = overlay_manager.clone();
217        let consensus_engine_stream = UnboundedReceiverStream::from(consensus_engine_rx)
218            .maybe_skip_fcu(node_config.debug.skip_fcu)
219            .maybe_skip_new_payload(node_config.debug.skip_new_payload)
220            .maybe_reorg(
221                ctx.blockchain_db().clone(),
222                ctx.components().evm_config().clone(),
223                || async {
224                    validator_builder
225                        .build_tree_validator(
226                            &add_ons_ctx,
227                            engine_tree_config.clone(),
228                            reorg_overlay_manager.clone(),
229                        )
230                        .await
231                },
232                node_config.debug.reorg_frequency,
233                node_config.debug.reorg_depth,
234            )
235            .await?
236            // Store messages _after_ skipping so that `replay-engine` command
237            // would replay only the messages that were observed by the engine
238            // during this run.
239            .maybe_store_messages(node_config.debug.engine_api_store.clone());
240
241        let engine_kind = if ctx.chain_spec().is_optimism() {
242            EngineApiKind::OpStack
243        } else {
244            EngineApiKind::Ethereum
245        };
246
247        let mut orchestrator = build_engine_orchestrator(
248            engine_kind,
249            consensus.clone(),
250            network_client.clone(),
251            Box::pin(consensus_engine_stream),
252            pipeline,
253            ctx.task_executor().clone(),
254            ctx.provider_factory().clone(),
255            ctx.blockchain_db().clone(),
256            pruner,
257            ctx.components().payload_builder_handle().clone(),
258            engine_validator,
259            overlay_manager,
260            engine_tree_config,
261            ctx.sync_metrics_tx(),
262            ctx.components().evm_config().clone(),
263            ctx.task_executor().clone(),
264        );
265
266        info!(target: "reth::cli", "Consensus engine initialized");
267
268        #[expect(clippy::needless_continue)]
269        let events = stream_select!(
270            event_sender.new_listener().map(Into::into),
271            pipeline_events.map(Into::into),
272            ctx.consensus_layer_events(),
273            pruner_events.map(Into::into),
274            static_file_producer_events.map(Into::into),
275        );
276
277        ctx.task_executor().spawn_critical_task(
278            "events task",
279            node::handle_events(
280                Some(Box::new(ctx.components().network().clone())),
281                Some(ctx.head().number),
282                events,
283            ),
284        );
285
286        let RpcHandle {
287            rpc_server_handles,
288            rpc_registry,
289            engine_events,
290            beacon_engine_handle,
291            engine_shutdown: _,
292        } = add_ons.launch_add_ons(add_ons_ctx).await?;
293
294        // Create engine shutdown handle
295        let (engine_shutdown, shutdown_rx) = EngineShutdown::new();
296
297        // Run consensus engine to completion
298        let initial_target = ctx.initial_backfill_target(disabled_stages)?;
299        let mut built_payloads = ctx
300            .components()
301            .payload_builder_handle()
302            .subscribe()
303            .await
304            .map_err(|e| eyre::eyre!("Failed to subscribe to payload builder events: {:?}", e))?
305            .into_built_payload_stream()
306            .fuse();
307
308        let chainspec = ctx.chain_spec();
309        let provider = ctx.blockchain_db().clone();
310        let (exit, rx) = oneshot::channel();
311        let terminate_after_backfill = ctx.terminate_after_initial_backfill();
312        let startup_sync_state_idle = ctx.node_config().debug.startup_sync_state_idle;
313
314        info!(target: "reth::cli", "Starting consensus engine");
315        let consensus_engine = move |mut on_graceful_shutdown| async move {
316            if let Some(initial_target) = initial_target {
317                debug!(target: "reth::cli", %initial_target,  "start backfill sync");
318                // network_handle's sync state is already initialized at Syncing
319                orchestrator.start_backfill_sync(initial_target);
320            } else if startup_sync_state_idle {
321                network_handle.update_sync_state(SyncState::Idle);
322            }
323
324            let mut res = Ok(());
325            let mut shutdown_rx = shutdown_rx.fuse();
326
327            // advance the chain and await payloads built locally to add into the engine api
328            // tree handler to prevent re-execution if that block is received as payload from
329            // the CL
330            loop {
331                tokio::select! {
332                    event = orchestrator.next() => {
333                        let Some(event) = event else { break };
334                        debug!(target: "reth::cli", "Event: {event}");
335                        match event {
336                            ChainEvent::BackfillSyncFinished => {
337                                if terminate_after_backfill {
338                                    debug!(target: "reth::cli", "Terminating after initial backfill");
339                                    break
340                                }
341                                if startup_sync_state_idle {
342                                    network_handle.update_sync_state(SyncState::Idle);
343                                }
344                            }
345                            ChainEvent::BackfillSyncStarted => {
346                                network_handle.update_sync_state(SyncState::Syncing);
347                            }
348                            ChainEvent::FatalError => {
349                                error!(target: "reth::cli", "Fatal error in consensus engine");
350                                res = Err(eyre::eyre!("Fatal error in consensus engine"));
351                                break
352                            }
353                            ChainEvent::Handler(ev) => {
354                                if let Some(head) = ev.canonical_header() {
355                                    // Once we're progressing via live sync, we can consider the node is not syncing anymore
356                                    network_handle.update_sync_state(SyncState::Idle);
357                                    let head_block = Head {
358                                        number: head.number(),
359                                        hash: head.hash(),
360                                        difficulty: head.difficulty(),
361                                        timestamp: head.timestamp(),
362                                        total_difficulty: chainspec.final_paris_total_difficulty()
363                                            .filter(|_| chainspec.is_paris_active_at_block(head.number()))
364                                            .unwrap_or_default(),
365                                    };
366                                    network_handle.update_status(head_block);
367
368                                    let updated = BlockRangeUpdate {
369                                        earliest: provider.earliest_block_number().unwrap_or_default(),
370                                        latest: head.number(),
371                                        latest_hash: head.hash(),
372                                    };
373                                    network_handle.update_block_range(updated);
374                                }
375                                event_sender.notify(ev);
376                            }
377                        }
378                    }
379                    payload = built_payloads.select_next_some(), if !built_payloads.is_terminated() => {
380                        if let Some(executed_block) = payload.executed_block() {
381                            debug!(target: "reth::cli", block=?executed_block.recovered_block.num_hash(),  "inserting built payload");
382                            orchestrator.handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block).into());
383                        }
384                    }
385                    shutdown_req = &mut shutdown_rx => {
386                        if let Ok(req) = shutdown_req {
387                            debug!(target: "reth::cli", "received engine shutdown request");
388                            orchestrator.handler_mut().handler_mut().on_event(
389                                FromOrchestrator::Terminate { tx: req.done_tx }.into()
390                            );
391                        }
392                    }
393                    _guard = &mut on_graceful_shutdown => {
394                        // Shutdown signal received.
395                        // Send Terminate so the engine OS thread can exit cleanly before we
396                        // drop the orchestrator.
397                        debug!(target: "reth::cli", "shutdown signal received, terminating engine");
398                        let (done_tx, done_rx) = oneshot::channel();
399                        orchestrator.handler_mut().handler_mut().on_event(
400                            FromOrchestrator::Terminate { tx: done_tx }.into()
401                        );
402                        let _ = done_rx.await;
403                        break;
404                    }
405                }
406            }
407
408            let _ = exit.send(res);
409        };
410        ctx.task_executor()
411            .spawn_critical_with_graceful_shutdown_signal("consensus engine", consensus_engine);
412
413        let engine_events_for_ethstats = engine_events.new_listener();
414
415        let full_node = FullNode {
416            evm_config: ctx.components().evm_config().clone(),
417            pool: ctx.components().pool().clone(),
418            network: ctx.components().network().clone(),
419            provider: ctx.node_adapter().provider.clone(),
420            payload_builder_handle: ctx.components().payload_builder_handle().clone(),
421            task_executor: ctx.task_executor().clone(),
422            config: ctx.node_config().clone(),
423            data_dir: ctx.data_dir().clone(),
424            add_ons_handle: RpcHandle {
425                rpc_server_handles,
426                rpc_registry,
427                engine_events,
428                beacon_engine_handle,
429                engine_shutdown,
430            },
431        };
432        // Notify on node started
433        on_node_started.on_event(FullNode::clone(&full_node))?;
434
435        ctx.spawn_ethstats(engine_events_for_ethstats).await?;
436
437        let handle = NodeHandle {
438            node_exit_future: NodeExitFuture::new(async { rx.await? }),
439            node: full_node,
440        };
441
442        Ok(handle)
443    }
444}
445
446impl<N, DB, T, CB, AO> LaunchNode<NodeBuilderWithComponents<T, CB, AO>> for EngineNodeLauncher
447where
448    T: FullNodeTypes<
449        Types = N,
450        DB = DB,
451        Provider = BlockchainProvider<NodeTypesWithDBAdapter<N, DB>>,
452    >,
453    N: Node<RethFullAdapter<DB, N>> + NodeTypesForProvider,
454    DB: Database + DatabaseMetrics + Clone + Unpin + 'static,
455    CB: NodeComponentsBuilder<T> + 'static,
456    AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
457        + EngineValidatorAddOn<NodeAdapter<T, CB::Components>>
458        + 'static,
459{
460    type Node = NodeHandle<NodeAdapter<T, CB::Components>, AO>;
461    type Future = Pin<Box<dyn Future<Output = eyre::Result<Self::Node>> + Send>>;
462
463    fn launch_node(self, target: NodeBuilderWithComponents<T, CB, AO>) -> Self::Future {
464        Box::pin(self.launch_node(target))
465    }
466}