arb_node/
launcher.rs

1//! Custom engine node launcher for Arbitrum.
2//!
3//! Extends reth's standard `EngineNodeLauncher` by capturing the engine tree
4//! sender during orchestrator construction. This sender allows the block
5//! producer to inject `InsertExecutedBlock` directly into reth's engine tree
6//! for persistence via `PersistenceService::save_blocks(Full)`.
7//!
8//! This is the reth SDK-native approach: implement `LaunchNode` with custom
9//! orchestrator wiring while reusing all other engine infrastructure.
10
11use std::{
12    future::Future,
13    pin::Pin,
14    sync::{Arc, OnceLock},
15};
16
17use alloy_consensus::BlockHeader;
18use arb_payload::ArbEngineTypes;
19use arb_primitives::ArbPrimitives;
20use futures::{FutureExt, StreamExt, stream::FusedStream, stream_select};
21use reth_chainspec::{EthChainSpec, EthereumHardforks};
22use reth_engine_tree::{
23    chain::{ChainEvent, FromOrchestrator},
24    engine::{EngineApiKind, EngineApiRequest, EngineRequestHandler},
25    tree::TreeConfig,
26};
27use reth_engine_util::EngineMessageStreamExt;
28use reth_exex::ExExManagerHandle;
29use reth_network::{NetworkSyncUpdater, SyncState, types::BlockRangeUpdate};
30use reth_network_api::BlockDownloaderProvider;
31use reth_node_api::{
32    BuiltPayload, ConsensusEngineHandle, FullNodeTypes, NodeTypes, NodeTypesWithDBAdapter,
33};
34use reth_node_builder::{
35    AddOns, AddOnsContext, FullNode, LaunchContext, LaunchNode, NodeAdapter,
36    NodeBuilderWithComponents, NodeComponents, NodeComponentsBuilder, NodeHandle, NodeTypesAdapter,
37    common::{Attached, LaunchContextWith, WithConfigs},
38    hooks::NodeHooks,
39    rpc::{EngineShutdown, EngineValidatorAddOn, EngineValidatorBuilder, RethRpcAddOns, RpcHandle},
40    setup::build_networked_pipeline,
41};
42use reth_node_core::{
43    dirs::{ChainPath, DataDirPath},
44    exit::NodeExitFuture,
45    primitives::Head,
46};
47use reth_node_events::node;
48use reth_provider::{
49    BlockNumReader, StorageSettingsCache,
50    providers::{BlockchainProvider, NodeTypesForProvider},
51};
52use reth_tasks::TaskExecutor;
53use reth_tokio_util::EventSender;
54use reth_tracing::tracing::{debug, error, info};
55use reth_trie_db::ChangesetCache;
56use tokio::sync::{mpsc::unbounded_channel, oneshot};
57use tokio_stream::wrappers::UnboundedReceiverStream;
58
59use crate::{
60    engine::{TreeSender, build_arb_engine_orchestrator},
61    error::LauncherError,
62};
63
64static TREE_SENDER: OnceLock<TreeSender<ArbEngineTypes, ArbPrimitives>> = OnceLock::new();
65static ENGINE_HANDLE: OnceLock<ConsensusEngineHandle<ArbEngineTypes>> = OnceLock::new();
66static FLUSH_HANDLE: OnceLock<FlushHandle> = OnceLock::new();
67static PARALLEL_STATE_ROOT_FN: OnceLock<ParallelStateRootFn> = OnceLock::new();
68
69/// Request sent to the background persistence thread.
70pub enum PersistenceRequest {
71    Flush(FlushRequest),
72    Unwind {
73        target: u64,
74        done: crossbeam_channel::Sender<Result<(), LauncherError>>,
75    },
76}
77
78/// Flush payload: buffered blocks to persist.
79pub struct FlushRequest {
80    pub blocks: Vec<reth_chain_state::ExecutedBlock<ArbPrimitives>>,
81    pub last_num_hash: alloy_eips::BlockNumHash,
82}
83
84/// Result from a completed flush.
85pub struct FlushResult {
86    pub last_num_hash: alloy_eips::BlockNumHash,
87    pub count: usize,
88    pub duration: std::time::Duration,
89}
90
91/// Handle to the background persistence thread.
92struct FlushHandle {
93    sender: std::sync::mpsc::Sender<PersistenceRequest>,
94    result_rx: crossbeam_channel::Receiver<FlushResult>,
95    flush_done: Arc<tokio::sync::Notify>,
96}
97
98/// Type-erased parallel state root function.
99type ParallelStateRootFn = Box<
100    dyn Fn(
101            Arc<reth_trie_common::TrieInputSorted>,
102            reth_trie_common::prefix_set::TriePrefixSets,
103        )
104            -> Result<(alloy_primitives::B256, reth_trie::updates::TrieUpdates), LauncherError>
105        + Send
106        + Sync,
107>;
108
109pub fn tree_sender() -> Option<&'static TreeSender<ArbEngineTypes, ArbPrimitives>> {
110    TREE_SENDER.get()
111}
112
113pub fn engine_handle() -> Option<&'static ConsensusEngineHandle<ArbEngineTypes>> {
114    ENGINE_HANDLE.get()
115}
116
117/// Send blocks to the background persistence thread (non-blocking).
118pub fn start_flush(request: FlushRequest) {
119    if let Some(handle) = FLUSH_HANDLE.get()
120        && let Err(e) = handle.sender.send(PersistenceRequest::Flush(request))
121    {
122        error!(target: "reth::cli", "Failed to send flush request: {e}");
123    }
124}
125
126/// Send an unwind request to the background persistence thread and return a
127/// receiver for the result. Blocks from `target + 1` onward (and their
128/// execution state + trie state) are removed from disk. This runs on the
129/// same thread as flushes to avoid races with in-flight persistence.
130pub fn start_unwind(target: u64) -> Option<crossbeam_channel::Receiver<Result<(), LauncherError>>> {
131    let handle = FLUSH_HANDLE.get()?;
132    let (done_tx, done_rx) = crossbeam_channel::bounded(1);
133    if let Err(e) = handle.sender.send(PersistenceRequest::Unwind {
134        target,
135        done: done_tx,
136    }) {
137        error!(target: "reth::cli", "Failed to send unwind request: {e}");
138        return None;
139    }
140    Some(done_rx)
141}
142
143/// Check if a background flush completed (non-blocking).
144pub fn try_flush_result() -> Option<FlushResult> {
145    FLUSH_HANDLE
146        .get()
147        .and_then(|handle| handle.result_rx.try_recv().ok())
148}
149
150/// Returns the notifier signalled after each successful flush commit.
151pub fn flush_notifier() -> Option<Arc<tokio::sync::Notify>> {
152    FLUSH_HANDLE.get().map(|handle| handle.flush_done.clone())
153}
154
155pub fn compute_parallel_state_root(
156    overlay: Arc<reth_trie_common::TrieInputSorted>,
157    prefix_sets: reth_trie_common::prefix_set::TriePrefixSets,
158) -> Result<(alloy_primitives::B256, reth_trie::updates::TrieUpdates), LauncherError> {
159    let f = PARALLEL_STATE_ROOT_FN
160        .get()
161        .ok_or(LauncherError::ParallelStateRootNotInitialized)?;
162    f(overlay, prefix_sets)
163}
164
165/// Arbitrum engine node launcher.
166///
167/// Identical to reth's `EngineNodeLauncher` but captures the engine tree sender
168/// during orchestrator construction for block injection.
169#[derive(Debug)]
170pub struct ArbEngineLauncher {
171    pub ctx: LaunchContext,
172    pub engine_tree_config: TreeConfig,
173}
174
175impl ArbEngineLauncher {
176    pub const fn new(
177        task_executor: TaskExecutor,
178        data_dir: ChainPath<DataDirPath>,
179        engine_tree_config: TreeConfig,
180    ) -> Self {
181        Self {
182            ctx: LaunchContext::new(task_executor, data_dir),
183            engine_tree_config,
184        }
185    }
186
187    /// Launch the node — mirrors EngineNodeLauncher::launch_node exactly,
188    /// except uses build_arb_engine_orchestrator to capture the tree sender.
189    async fn launch_node<T, CB, AO>(
190        self,
191        target: NodeBuilderWithComponents<T, CB, AO>,
192    ) -> eyre::Result<NodeHandle<NodeAdapter<T, CB::Components>, AO>>
193    where
194        T: FullNodeTypes<
195                Types: NodeTypesForProvider<Payload = ArbEngineTypes, Primitives = ArbPrimitives>,
196                Provider = BlockchainProvider<
197                    NodeTypesWithDBAdapter<<T as FullNodeTypes>::Types, <T as FullNodeTypes>::DB>,
198                >,
199            >,
200        CB: NodeComponentsBuilder<T>,
201        AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
202            + EngineValidatorAddOn<NodeAdapter<T, CB::Components>>,
203    {
204        let Self {
205            ctx,
206            engine_tree_config,
207        } = self;
208        let NodeBuilderWithComponents {
209            adapter: NodeTypesAdapter { database },
210            components_builder,
211            add_ons:
212                AddOns {
213                    hooks,
214                    exexs: installed_exex,
215                    add_ons,
216                },
217            config,
218        } = target;
219        let NodeHooks {
220            on_component_initialized,
221            on_node_started,
222            ..
223        } = hooks;
224
225        let changeset_cache = ChangesetCache::new();
226
227        let ctx = ctx
228            .with_configured_globals(engine_tree_config.reserved_cpu_cores())
229            .with_loaded_toml_config(config)?
230            .with_resolved_peers()?
231            .attach(database.clone())
232            .with_adjusted_configs()
233            .with_provider_factory::<_, <CB::Components as NodeComponents<T>>::Evm>(
234                changeset_cache.clone(),
235            )
236            .await?
237            .inspect(|_| {
238                info!(target: "reth::cli", "Database opened");
239            })
240            .with_prometheus_server()
241            .await?
242            .inspect(|this| {
243                debug!(target: "reth::cli", chain=%this.chain_id(), genesis=?this.genesis_hash(), "Initializing genesis");
244            })
245            .with_genesis()?
246            .inspect(
247                |this: &LaunchContextWith<
248                    Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, _>,
249                >| {
250                    info!(target: "reth::cli", "\n{}", this.chain_spec().display_hardforks());
251                    let settings = this.provider_factory().cached_storage_settings();
252                    info!(target: "reth::cli", ?settings, "Loaded storage settings");
253                },
254            )
255            .with_metrics_task()
256            .with_blockchain_db::<T, _>(move |provider_factory| {
257                Ok(BlockchainProvider::new(provider_factory)?)
258            })?
259            .with_components(components_builder, on_component_initialized)
260            .await?;
261
262        let maybe_exex_manager_handle = ctx.launch_exex(installed_exex).await?;
263
264        let network_handle = ctx.components().network().clone();
265        let network_client = network_handle.fetch_client().await?;
266        let (consensus_engine_tx, consensus_engine_rx) = unbounded_channel();
267
268        let node_config = ctx.node_config();
269
270        network_handle.update_sync_state(SyncState::Syncing);
271
272        let max_block = ctx.max_block(network_client.clone()).await?;
273
274        let static_file_producer = ctx.static_file_producer();
275        let static_file_producer_events = static_file_producer.lock().events();
276        info!(target: "reth::cli", "StaticFileProducer initialized");
277
278        let consensus = Arc::new(ctx.components().consensus().clone());
279
280        let pipeline = build_networked_pipeline(
281            &ctx.toml_config().stages,
282            network_client.clone(),
283            consensus.clone(),
284            ctx.provider_factory().clone(),
285            ctx.task_executor(),
286            ctx.sync_metrics_tx(),
287            ctx.prune_config(),
288            max_block,
289            static_file_producer,
290            ctx.components().evm_config().clone(),
291            maybe_exex_manager_handle
292                .clone()
293                .unwrap_or_else(ExExManagerHandle::empty),
294            ctx.era_import_source(),
295        )?;
296
297        pipeline.move_to_static_files()?;
298
299        let pipeline_events = pipeline.events();
300
301        let mut pruner_builder = ctx.pruner_builder();
302        if let Some(exex_manager_handle) = &maybe_exex_manager_handle {
303            pruner_builder =
304                pruner_builder.finished_exex_height(exex_manager_handle.finished_height());
305        }
306        let pruner = pruner_builder.build_with_provider_factory(ctx.provider_factory().clone());
307        let pruner_events = pruner.events();
308        info!(target: "reth::cli", prune_config=?ctx.prune_config(), "Pruner initialized");
309
310        let event_sender = EventSender::default();
311
312        let beacon_engine_handle = ConsensusEngineHandle::new(consensus_engine_tx.clone());
313
314        let jwt_secret = ctx.auth_jwt_secret()?;
315
316        let add_ons_ctx = AddOnsContext {
317            node: ctx.node_adapter().clone(),
318            config: ctx.node_config(),
319            beacon_engine_handle: beacon_engine_handle.clone(),
320            jwt_secret,
321            engine_events: event_sender.clone(),
322        };
323        let validator_builder = add_ons.engine_validator_builder();
324
325        let engine_validator = validator_builder
326            .clone()
327            .build_tree_validator(
328                &add_ons_ctx,
329                engine_tree_config.clone(),
330                changeset_cache.clone(),
331            )
332            .await?;
333
334        let consensus_engine_stream = UnboundedReceiverStream::from(consensus_engine_rx)
335            .maybe_skip_fcu(node_config.debug.skip_fcu)
336            .maybe_skip_new_payload(node_config.debug.skip_new_payload)
337            .maybe_reorg(
338                ctx.blockchain_db().clone(),
339                ctx.components().evm_config().clone(),
340                || async {
341                    let reorg_cache = ChangesetCache::new();
342                    validator_builder
343                        .build_tree_validator(&add_ons_ctx, engine_tree_config.clone(), reorg_cache)
344                        .await
345                },
346                node_config.debug.reorg_frequency,
347                node_config.debug.reorg_depth,
348            )
349            .await?
350            .maybe_store_messages(node_config.debug.engine_api_store.clone());
351
352        let engine_kind = if ctx.chain_spec().is_optimism() {
353            EngineApiKind::OpStack
354        } else {
355            EngineApiKind::Ethereum
356        };
357
358        // Spawn background persistence thread (like reth's PersistenceHandle).
359        // Handles flush and unwind requests serially — same thread guarantees
360        // no races between saving new blocks and rolling back.
361        {
362            use reth_provider::{DatabaseProviderFactory, SaveBlocksMode};
363            use reth_storage_api::{BlockExecutionWriter, DBProvider};
364
365            let pf = ctx.provider_factory().clone();
366            let (req_tx, req_rx) = std::sync::mpsc::channel::<PersistenceRequest>();
367            let (res_tx, res_rx) = crossbeam_channel::bounded::<FlushResult>(1);
368            let flush_done = Arc::new(tokio::sync::Notify::new());
369            let flush_done_thread = flush_done.clone();
370
371            std::thread::Builder::new()
372                .name("arb-persistence".into())
373                .spawn(move || {
374                    while let Ok(req) = req_rx.recv() {
375                        match req {
376                            PersistenceRequest::Flush(flush) => {
377                                let start = std::time::Instant::now();
378                                let count = flush.blocks.len();
379                                let last = flush.last_num_hash;
380
381                                let result = (|| -> Result<(), LauncherError> {
382                                    let provider_rw = pf.database_provider_rw()?;
383                                    provider_rw.save_blocks(flush.blocks, SaveBlocksMode::Full)?;
384                                    provider_rw.commit()?;
385                                    Ok(())
386                                })();
387
388                                match result {
389                                    Ok(()) => {
390                                        let _ = res_tx.send(FlushResult {
391                                            last_num_hash: last,
392                                            count,
393                                            duration: start.elapsed(),
394                                        });
395                                    }
396                                    Err(e) => {
397                                        error!(target: "reth::cli", "Background flush failed: {e}");
398                                        let _ = res_tx.send(FlushResult {
399                                            last_num_hash: last,
400                                            count: 0, // signal failure
401                                            duration: start.elapsed(),
402                                        });
403                                    }
404                                }
405                                flush_done_thread.notify_one();
406                            }
407                            PersistenceRequest::Unwind { target, done } => {
408                                let start = std::time::Instant::now();
409                                let result = (|| -> Result<(), LauncherError> {
410                                    let provider_rw = pf.database_provider_rw()?;
411                                    provider_rw.remove_block_and_execution_above(target)?;
412                                    provider_rw.commit()?;
413                                    Ok(())
414                                })();
415                                match &result {
416                                    Ok(()) => info!(
417                                        target: "reth::cli",
418                                        target,
419                                        duration_ms = start.elapsed().as_millis(),
420                                        "Persisted unwind complete"
421                                    ),
422                                    Err(e) => error!(
423                                        target: "reth::cli",
424                                        target,
425                                        err = %e,
426                                        "Persisted unwind failed"
427                                    ),
428                                }
429                                let _ = done.send(result);
430                            }
431                        }
432                    }
433                })
434                .expect("failed to spawn persistence thread");
435
436            let _ = FLUSH_HANDLE.set(FlushHandle {
437                sender: req_tx,
438                result_rx: res_rx,
439                flush_done,
440            });
441        }
442
443        {
444            use reth_chain_state::{
445                AnchoredTrieInput, ComputedTrieData, DeferredTrieData, LazyOverlay,
446            };
447            use reth_provider::providers::OverlayStateProviderFactory;
448            use reth_trie_parallel::root::ParallelStateRoot;
449
450            let pf = ctx.provider_factory().clone();
451            let runtime = ctx.task_executor().clone();
452            let changeset_cache_for_root = changeset_cache.clone();
453
454            let state_root_fn: ParallelStateRootFn = Box::new(move |overlay, prefix_sets| {
455                let anchor_hash = alloy_primitives::B256::ZERO;
456                let computed = ComputedTrieData {
457                    hashed_state: Arc::clone(&overlay.state),
458                    trie_updates: Arc::clone(&overlay.nodes),
459                    anchored_trie_input: Some(AnchoredTrieInput {
460                        anchor_hash,
461                        trie_input: overlay,
462                    }),
463                };
464                let lazy = LazyOverlay::new(anchor_hash, vec![DeferredTrieData::ready(computed)]);
465                let factory =
466                    OverlayStateProviderFactory::new(pf.clone(), changeset_cache_for_root.clone())
467                        .with_lazy_overlay(Some(lazy));
468
469                ParallelStateRoot::new(factory, prefix_sets, runtime.clone())
470                    .incremental_root_with_updates()
471                    .map_err(LauncherError::from)
472            });
473            let _ = PARALLEL_STATE_ROOT_FN.set(state_root_fn);
474        }
475
476        let (mut orchestrator, arb_tree_sender) = build_arb_engine_orchestrator(
477            engine_kind,
478            consensus.clone(),
479            network_client.clone(),
480            Box::pin(consensus_engine_stream),
481            pipeline,
482            ctx.task_executor().clone(),
483            ctx.provider_factory().clone(),
484            ctx.blockchain_db().clone(),
485            pruner,
486            ctx.components().payload_builder_handle().clone(),
487            engine_validator,
488            engine_tree_config,
489            ctx.sync_metrics_tx(),
490            ctx.components().evm_config().clone(),
491            changeset_cache,
492        );
493
494        let _ = TREE_SENDER.set(arb_tree_sender);
495        let _ = ENGINE_HANDLE.set(beacon_engine_handle.clone());
496        info!(target: "reth::cli", "Arbitrum engine tree sender and handle captured");
497
498        info!(target: "reth::cli", "Consensus engine initialized");
499
500        #[allow(clippy::needless_continue)]
501        let events = stream_select!(
502            event_sender.new_listener().map(Into::into),
503            pipeline_events.map(Into::into),
504            ctx.consensus_layer_events(),
505            pruner_events.map(Into::into),
506            static_file_producer_events.map(Into::into),
507        );
508
509        ctx.task_executor().spawn_critical_task(
510            "events task",
511            Box::pin(node::handle_events(
512                Some(Box::new(ctx.components().network().clone())),
513                Some(ctx.head().number),
514                events,
515            )),
516        );
517
518        let RpcHandle {
519            rpc_server_handles,
520            rpc_registry,
521            engine_events,
522            beacon_engine_handle,
523            engine_shutdown: _,
524        } = add_ons.launch_add_ons(add_ons_ctx).await?;
525
526        let (engine_shutdown, shutdown_rx) = EngineShutdown::new();
527
528        let initial_target = ctx.initial_backfill_target()?;
529        let mut built_payloads = ctx
530            .components()
531            .payload_builder_handle()
532            .subscribe()
533            .await
534            .map_err(|e| eyre::eyre!("Failed to subscribe to payload builder events: {:?}", e))?
535            .into_built_payload_stream()
536            .fuse();
537
538        let chainspec = ctx.chain_spec();
539        let provider = ctx.blockchain_db().clone();
540        let (exit, rx) = oneshot::channel();
541        let terminate_after_backfill = ctx.terminate_after_initial_backfill();
542        let startup_sync_state_idle = ctx.node_config().debug.startup_sync_state_idle;
543
544        info!(target: "reth::cli", "Starting consensus engine");
545        let consensus_engine = async move {
546            if let Some(initial_target) = initial_target {
547                debug!(target: "reth::cli", %initial_target, "start backfill sync");
548                orchestrator.start_backfill_sync(initial_target);
549            } else if startup_sync_state_idle {
550                network_handle.update_sync_state(SyncState::Idle);
551            }
552
553            let mut res = Ok(());
554            let mut shutdown_rx = shutdown_rx.fuse();
555
556            loop {
557                tokio::select! {
558                    event = orchestrator.next() => {
559                        let Some(event) = event else { break };
560                        debug!(target: "reth::cli", "Event: {event}");
561                        match event {
562                            ChainEvent::BackfillSyncFinished => {
563                                if terminate_after_backfill {
564                                    debug!(target: "reth::cli", "Terminating after initial backfill");
565                                    break
566                                }
567                                if startup_sync_state_idle {
568                                    network_handle.update_sync_state(SyncState::Idle);
569                                }
570                            }
571                            ChainEvent::BackfillSyncStarted => {
572                                network_handle.update_sync_state(SyncState::Syncing);
573                            }
574                            ChainEvent::FatalError => {
575                                error!(target: "reth::cli", "Fatal error in consensus engine");
576                                res = Err(eyre::eyre!("Fatal error in consensus engine"));
577                                break
578                            }
579                            ChainEvent::Handler(ev) => {
580                                if let Some(head) = ev.canonical_header() {
581                                    network_handle.update_sync_state(SyncState::Idle);
582                                    let head_block = Head {
583                                        number: head.number(),
584                                        hash: head.hash(),
585                                        difficulty: head.difficulty(),
586                                        timestamp: head.timestamp(),
587                                        total_difficulty: chainspec.final_paris_total_difficulty()
588                                            .filter(|_| chainspec.is_paris_active_at_block(head.number()))
589                                            .unwrap_or_default(),
590                                    };
591                                    network_handle.update_status(head_block);
592
593                                    let updated = BlockRangeUpdate {
594                                        earliest: provider.earliest_block_number().unwrap_or_default(),
595                                        latest: head.number(),
596                                        latest_hash: head.hash(),
597                                    };
598                                    network_handle.update_block_range(updated);
599                                }
600                                event_sender.notify(ev);
601                            }
602                        }
603                    }
604                    payload = built_payloads.select_next_some(), if !built_payloads.is_terminated() => {
605                        if let Some(executed_block) = payload.executed_block() {
606                            debug!(target: "reth::cli", block=?executed_block.recovered_block.num_hash(), "inserting built payload");
607                            orchestrator.handler_mut().handler_mut().on_event(EngineApiRequest::InsertExecutedBlock(executed_block.into_executed_payload()).into());
608                        }
609                    }
610                    shutdown_req = &mut shutdown_rx => {
611                        if let Ok(req) = shutdown_req {
612                            debug!(target: "reth::cli", "received engine shutdown request");
613                            orchestrator.handler_mut().handler_mut().on_event(
614                                FromOrchestrator::Terminate { tx: req.done_tx }.into()
615                            );
616                        }
617                    }
618                }
619            }
620
621            let _ = exit.send(res);
622        };
623        ctx.task_executor()
624            .spawn_critical_task("consensus engine", Box::pin(consensus_engine));
625
626        let engine_events_for_ethstats = engine_events.new_listener();
627
628        let full_node = FullNode {
629            evm_config: ctx.components().evm_config().clone(),
630            pool: ctx.components().pool().clone(),
631            network: ctx.components().network().clone(),
632            provider: ctx.node_adapter().provider.clone(),
633            payload_builder_handle: ctx.components().payload_builder_handle().clone(),
634            task_executor: ctx.task_executor().clone(),
635            config: ctx.node_config().clone(),
636            data_dir: ctx.data_dir().clone(),
637            add_ons_handle: RpcHandle {
638                rpc_server_handles,
639                rpc_registry,
640                engine_events,
641                beacon_engine_handle,
642                engine_shutdown,
643            },
644        };
645        on_node_started.on_event(FullNode::clone(&full_node))?;
646
647        ctx.spawn_ethstats(engine_events_for_ethstats).await?;
648
649        let handle = NodeHandle {
650            node_exit_future: NodeExitFuture::new(
651                async { rx.await? },
652                full_node.config.debug.terminate,
653            ),
654            node: full_node,
655        };
656
657        Ok(handle)
658    }
659}
660
661impl<T, CB, AO> LaunchNode<NodeBuilderWithComponents<T, CB, AO>> for ArbEngineLauncher
662where
663    T: FullNodeTypes<
664            Types: NodeTypesForProvider<Payload = ArbEngineTypes, Primitives = ArbPrimitives>,
665            Provider = BlockchainProvider<
666                NodeTypesWithDBAdapter<<T as FullNodeTypes>::Types, <T as FullNodeTypes>::DB>,
667            >,
668        >,
669    CB: NodeComponentsBuilder<T> + 'static,
670    AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>
671        + EngineValidatorAddOn<NodeAdapter<T, CB::Components>>
672        + 'static,
673{
674    type Node = NodeHandle<NodeAdapter<T, CB::Components>, AO>;
675    type Future = Pin<Box<dyn Future<Output = eyre::Result<Self::Node>> + Send>>;
676
677    fn launch_node(self, target: NodeBuilderWithComponents<T, CB, AO>) -> Self::Future {
678        Box::pin(self.launch_node(target))
679    }
680}