Skip to main content

reth_node_builder/launch/
invalid_block_hook.rs

1//! Invalid block hook helpers for the node builder.
2
3use crate::AddOnsContext;
4use alloy_primitives::U64;
5use alloy_rpc_client::{ClientBuilder, RpcClient};
6use reth_chainspec::EthChainSpec;
7use reth_engine_primitives::{InvalidBlockHook, InvalidBlockHooks, NoopInvalidBlockHook};
8use reth_invalid_block_hooks::InvalidBlockWitnessHook;
9use reth_node_api::{FullNodeComponents, NodeTypes};
10use reth_node_core::{
11    args::InvalidBlockHookType,
12    dirs::{ChainPath, DataDirPath},
13    node_config::NodeConfig,
14};
15use reth_primitives_traits::NodePrimitives;
16use reth_provider::ChainSpecProvider;
17use reth_tasks::Runtime;
18use std::time::Duration;
19
20/// Timeout for a request to the healthy node.
21const HEALTHY_NODE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
22
23/// Extension trait for [`AddOnsContext`] to create invalid block hooks.
24pub trait InvalidBlockHookExt {
25    /// Node primitives type.
26    type Primitives: NodePrimitives;
27
28    /// Creates an invalid block hook based on the node configuration.
29    fn create_invalid_block_hook(
30        &self,
31        data_dir: &ChainPath<DataDirPath>,
32    ) -> impl std::future::Future<Output = eyre::Result<Box<dyn InvalidBlockHook<Self::Primitives>>>>
33           + Send;
34}
35
36impl<N> InvalidBlockHookExt for AddOnsContext<'_, N>
37where
38    N: FullNodeComponents,
39{
40    type Primitives = <N::Types as NodeTypes>::Primitives;
41
42    async fn create_invalid_block_hook(
43        &self,
44        data_dir: &ChainPath<DataDirPath>,
45    ) -> eyre::Result<Box<dyn InvalidBlockHook<Self::Primitives>>> {
46        create_invalid_block_hook(
47            self.config,
48            data_dir,
49            self.node.provider().clone(),
50            self.node.evm_config().clone(),
51            self.node.provider().chain_spec().chain().id(),
52            self.node.task_executor().clone(),
53        )
54        .await
55    }
56}
57
58/// Creates an invalid block hook based on the node configuration.
59///
60/// This function constructs the appropriate [`InvalidBlockHook`] based on the debug
61/// configuration in the node config. It supports:
62/// - Witness hooks for capturing block witness data
63/// - Healthy node verification via RPC
64///
65/// # Arguments
66/// * `config` - The node configuration containing debug settings
67/// * `data_dir` - The data directory for storing hook outputs
68/// * `provider` - The blockchain database provider
69/// * `evm_config` - The EVM configuration
70/// * `chain_id` - The chain ID for verification
71/// * `runtime` - The runtime for background diagnostics and shutdown handling.
72pub async fn create_invalid_block_hook<N, P, E>(
73    config: &NodeConfig<P::ChainSpec>,
74    data_dir: &ChainPath<DataDirPath>,
75    provider: P,
76    evm_config: E,
77    chain_id: u64,
78    runtime: Runtime,
79) -> eyre::Result<Box<dyn InvalidBlockHook<N>>>
80where
81    N: NodePrimitives,
82    P: reth_provider::StateProviderFactory + reth_provider::ChainSpecProvider + Clone + 'static,
83    E: reth_evm::ConfigureEvm<Primitives = N> + 'static,
84{
85    let Some(ref hook) = config.debug.invalid_block_hook else {
86        return Ok(Box::new(NoopInvalidBlockHook::default()))
87    };
88
89    let healthy_node_rpc_client = get_healthy_node_client(config, chain_id).await?;
90
91    let output_directory = data_dir.invalid_block_hooks();
92    let hooks = hook
93        .iter()
94        .copied()
95        .map(|hook| {
96            let output_directory = output_directory.join(hook.to_string());
97            std::fs::create_dir_all(&output_directory)?;
98
99            Ok(match hook {
100                InvalidBlockHookType::Witness => Box::new(InvalidBlockWitnessHook::new(
101                    provider.clone(),
102                    evm_config.clone(),
103                    output_directory,
104                    healthy_node_rpc_client.clone(),
105                    runtime.clone(),
106                )),
107                InvalidBlockHookType::PreState | InvalidBlockHookType::Opcode => {
108                    eyre::bail!("invalid block hook {hook:?} is not implemented yet");
109                }
110            } as Box<dyn InvalidBlockHook<_>>)
111        })
112        .collect::<Result<_, _>>()?;
113
114    Ok(Box::new(InvalidBlockHooks(hooks)))
115}
116
117/// Returns an RPC client for the healthy node, if configured in the node config.
118async fn get_healthy_node_client<C>(
119    config: &NodeConfig<C>,
120    chain_id: u64,
121) -> eyre::Result<Option<RpcClient>>
122where
123    C: EthChainSpec,
124{
125    let Some(url) = config.debug.healthy_node_rpc_url.as_ref() else {
126        return Ok(None);
127    };
128
129    // A healthy node that stops answering must not hang node launch or the invalid block hook.
130    let http = reqwest::Client::builder().timeout(HEALTHY_NODE_REQUEST_TIMEOUT).build()?;
131    let client = ClientBuilder::default().http_with_client(http, url.parse()?);
132
133    // Verify that the healthy node is running the same chain as the current node.
134    let healthy_chain_id: U64 = client.request_noparams("eth_chainId").await?;
135
136    if healthy_chain_id.to::<u64>() != chain_id {
137        eyre::bail!("Invalid chain ID. Expected {}, got {}", chain_id, healthy_chain_id);
138    }
139
140    Ok(Some(client))
141}