Skip to main content

reth_node_core/args/
debug.rs

1//! clap [`Args`] for debugging purposes.
2
3use alloy_primitives::B256;
4use clap::{
5    builder::{PossibleValue, TypedValueParser},
6    Arg, Args, Command,
7};
8use std::{collections::HashSet, ffi::OsStr, fmt, path::PathBuf, str::FromStr};
9use strum::{AsRefStr, EnumIter, IntoStaticStr, ParseError, VariantArray, VariantNames};
10
11/// Parameters for debugging purposes
12#[derive(Debug, Clone, Args, PartialEq, Eq)]
13#[command(next_help_heading = "Debug")]
14pub struct DebugArgs {
15    /// Flag indicating whether the node should be terminated after the pipeline sync.
16    #[arg(long = "debug.terminate", help_heading = "Debug")]
17    pub terminate: bool,
18
19    /// Set the chain tip manually for testing purposes.
20    ///
21    /// NOTE: This is a temporary flag
22    #[arg(long = "debug.tip", help_heading = "Debug")]
23    pub tip: Option<B256>,
24
25    /// Runs the sync only up to the specified block.
26    #[arg(long = "debug.max-block", help_heading = "Debug")]
27    pub max_block: Option<u64>,
28
29    /// Runs a fake consensus client that advances the chain using recent block hashes
30    /// on Etherscan. If specified, requires an `ETHERSCAN_API_KEY` environment variable.
31    #[arg(
32        long = "debug.etherscan",
33        help_heading = "Debug",
34        conflicts_with = "tip",
35        conflicts_with = "rpc_consensus_url",
36        value_name = "ETHERSCAN_API_URL"
37    )]
38    pub etherscan: Option<Option<String>>,
39
40    /// Runs a fake consensus client using blocks fetched from an RPC endpoint.
41    /// Supports both HTTP and `WebSocket` endpoints - `WebSocket` endpoints will use
42    /// subscriptions, while HTTP endpoints will poll for new blocks.
43    #[arg(
44        long = "debug.rpc-consensus-url",
45        alias = "debug.rpc-consensus-ws",
46        help_heading = "Debug",
47        conflicts_with = "tip",
48        conflicts_with = "etherscan",
49        value_name = "RPC_URL"
50    )]
51    pub rpc_consensus_url: Option<String>,
52
53    /// If provided, the engine will skip `n` consecutive FCUs.
54    #[arg(long = "debug.skip-fcu", help_heading = "Debug")]
55    pub skip_fcu: Option<usize>,
56
57    /// If provided, the engine will skip `n` consecutive new payloads.
58    #[arg(long = "debug.skip-new-payload", help_heading = "Debug")]
59    pub skip_new_payload: Option<usize>,
60
61    /// Skip trie state-root computation during engine validation.
62    ///
63    /// This trusts the block header's state root and is intended for experiments that measure
64    /// execution without trie state-root work.
65    #[arg(long = "debug.skip-state-root", help_heading = "Debug", hide = true)]
66    pub skip_state_root: bool,
67
68    /// If set, bypasses genesis hash validation during init.
69    /// Intended for tools that direct-write the database (e.g. snapshot
70    /// importers, state-actor) and want reth to trust the DB-resident
71    /// genesis state instead of recomputing it from the chainspec's alloc.
72    /// When the bypass fires, a structured `tracing::warn!` is emitted so
73    /// the divergence stays observable in operator logs.
74    #[arg(long = "debug.skip-genesis-validation", help_heading = "Debug")]
75    pub skip_genesis_validation: bool,
76
77    /// If provided, the chain will be reorged at specified frequency.
78    #[arg(long = "debug.reorg-frequency", help_heading = "Debug")]
79    pub reorg_frequency: Option<usize>,
80
81    /// The reorg depth for chain reorgs.
82    #[arg(long = "debug.reorg-depth", requires = "reorg_frequency", help_heading = "Debug")]
83    pub reorg_depth: Option<usize>,
84
85    /// The path to store engine API messages at.
86    /// If specified, all of the intercepted engine API messages
87    /// will be written to specified location.
88    #[arg(long = "debug.engine-api-store", help_heading = "Debug", value_name = "PATH")]
89    pub engine_api_store: Option<PathBuf>,
90
91    /// Determines which type of invalid block hook to install
92    ///
93    /// Example: `witness,prestate`
94    #[arg(
95        long = "debug.invalid-block-hook",
96        help_heading = "Debug",
97        value_parser = InvalidBlockSelectionValueParser::default(),
98        default_value = "witness"
99    )]
100    pub invalid_block_hook: Option<InvalidBlockSelection>,
101
102    /// The RPC URL of a healthy node to use for comparing invalid block hook results against.
103    ///
104    ///Debug setting that enables execution witness comparison for troubleshooting bad blocks.
105    /// When enabled, the node will collect execution witnesses from the specified source and
106    /// compare them against local execution when a bad block is encountered, helping identify
107    /// discrepancies in state execution.
108    #[arg(
109        long = "debug.healthy-node-rpc-url",
110        help_heading = "Debug",
111        value_name = "URL",
112        verbatim_doc_comment
113    )]
114    pub healthy_node_rpc_url: Option<String>,
115
116    /// The URL of the ethstats server to connect to.
117    /// Example: `nodename:secret@host:port`
118    #[arg(long = "ethstats", help_heading = "Debug")]
119    pub ethstats: Option<String>,
120
121    /// Set the node to idle state when the backfill is not running.
122    ///
123    /// This makes the `eth_syncing` RPC return "Idle" when the node has just started or finished
124    /// the backfill, but did not yet receive any new blocks.
125    #[arg(long = "debug.startup-sync-state-idle", help_heading = "Debug")]
126    pub startup_sync_state_idle: bool,
127
128    /// Load the KZG trusted setup when it is first needed instead of in the background on
129    /// startup.
130    #[arg(long = "debug.lazy-kzg-settings", help_heading = "Debug", hide = true)]
131    pub lazy_kzg_settings: bool,
132}
133
134impl Default for DebugArgs {
135    fn default() -> Self {
136        Self {
137            terminate: false,
138            tip: None,
139            max_block: None,
140            etherscan: None,
141            rpc_consensus_url: None,
142            skip_fcu: None,
143            skip_new_payload: None,
144            skip_state_root: false,
145            skip_genesis_validation: false,
146            reorg_frequency: None,
147            reorg_depth: None,
148            engine_api_store: None,
149            invalid_block_hook: Some(InvalidBlockSelection::default()),
150            healthy_node_rpc_url: None,
151            ethstats: None,
152            startup_sync_state_idle: false,
153            lazy_kzg_settings: false,
154        }
155    }
156}
157
158/// Describes the invalid block hooks that should be installed.
159///
160/// # Example
161///
162/// Create a [`InvalidBlockSelection`] from a selection.
163///
164/// ```
165/// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
166/// let config: InvalidBlockSelection = vec![InvalidBlockHookType::Witness].into();
167/// ```
168#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
169pub struct InvalidBlockSelection(HashSet<InvalidBlockHookType>);
170
171impl Default for InvalidBlockSelection {
172    fn default() -> Self {
173        Self([InvalidBlockHookType::Witness].into())
174    }
175}
176
177impl InvalidBlockSelection {
178    /// Creates a new _unique_ [`InvalidBlockSelection`] from the given items.
179    ///
180    /// # Note
181    ///
182    /// This will dedupe the selection and remove duplicates while preserving the order.
183    ///
184    /// # Example
185    ///
186    /// Create a selection from the [`InvalidBlockHookType`] string identifiers
187    ///
188    /// ```
189    /// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
190    /// let selection = vec!["witness", "prestate", "opcode"];
191    /// let config = InvalidBlockSelection::try_from_selection(selection).unwrap();
192    /// assert_eq!(
193    ///     config,
194    ///     InvalidBlockSelection::from([
195    ///         InvalidBlockHookType::Witness,
196    ///         InvalidBlockHookType::PreState,
197    ///         InvalidBlockHookType::Opcode
198    ///     ])
199    /// );
200    /// ```
201    ///
202    /// Create a unique selection from the [`InvalidBlockHookType`] string identifiers
203    ///
204    /// ```
205    /// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
206    /// let selection = vec!["witness", "prestate", "opcode", "witness", "prestate"];
207    /// let config = InvalidBlockSelection::try_from_selection(selection).unwrap();
208    /// assert_eq!(
209    ///     config,
210    ///     InvalidBlockSelection::from([
211    ///         InvalidBlockHookType::Witness,
212    ///         InvalidBlockHookType::PreState,
213    ///         InvalidBlockHookType::Opcode
214    ///     ])
215    /// );
216    /// ```
217    pub fn try_from_selection<I, T>(selection: I) -> Result<Self, T::Error>
218    where
219        I: IntoIterator<Item = T>,
220        T: TryInto<InvalidBlockHookType>,
221    {
222        selection.into_iter().map(TryInto::try_into).collect()
223    }
224
225    /// Clones the set of configured [`InvalidBlockHookType`].
226    pub fn to_selection(&self) -> HashSet<InvalidBlockHookType> {
227        self.0.clone()
228    }
229}
230
231impl From<&[InvalidBlockHookType]> for InvalidBlockSelection {
232    fn from(s: &[InvalidBlockHookType]) -> Self {
233        Self(s.iter().copied().collect())
234    }
235}
236
237impl From<Vec<InvalidBlockHookType>> for InvalidBlockSelection {
238    fn from(s: Vec<InvalidBlockHookType>) -> Self {
239        Self(s.into_iter().collect())
240    }
241}
242
243impl<const N: usize> From<[InvalidBlockHookType; N]> for InvalidBlockSelection {
244    fn from(s: [InvalidBlockHookType; N]) -> Self {
245        Self(s.iter().copied().collect())
246    }
247}
248
249impl FromIterator<InvalidBlockHookType> for InvalidBlockSelection {
250    fn from_iter<I>(iter: I) -> Self
251    where
252        I: IntoIterator<Item = InvalidBlockHookType>,
253    {
254        Self(iter.into_iter().collect())
255    }
256}
257
258impl FromStr for InvalidBlockSelection {
259    type Err = ParseError;
260
261    fn from_str(s: &str) -> Result<Self, Self::Err> {
262        if s.is_empty() {
263            return Ok(Self(Default::default()))
264        }
265        let hooks = s.split(',').map(str::trim).peekable();
266        Self::try_from_selection(hooks)
267    }
268}
269
270impl fmt::Display for InvalidBlockSelection {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        write!(f, "[{}]", self.0.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", "))
273    }
274}
275
276/// clap value parser for [`InvalidBlockSelection`].
277#[derive(Clone, Debug, Default)]
278#[non_exhaustive]
279struct InvalidBlockSelectionValueParser;
280
281impl TypedValueParser for InvalidBlockSelectionValueParser {
282    type Value = InvalidBlockSelection;
283
284    fn parse_ref(
285        &self,
286        _cmd: &Command,
287        arg: Option<&Arg>,
288        value: &OsStr,
289    ) -> Result<Self::Value, clap::Error> {
290        let val =
291            value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
292        val.parse::<InvalidBlockSelection>().map_err(|err| {
293            let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
294            let possible_values = InvalidBlockHookType::all_variant_names().to_vec().join(",");
295            let msg = format!(
296                "Invalid value '{val}' for {arg}: {err}.\n    [possible values: {possible_values}]"
297            );
298            clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
299        })
300    }
301
302    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
303        let values = InvalidBlockHookType::all_variant_names().iter().map(PossibleValue::new);
304        Some(Box::new(values))
305    }
306}
307
308/// The type of invalid block hook to install
309#[derive(
310    Debug,
311    Clone,
312    Copy,
313    PartialEq,
314    Eq,
315    Hash,
316    AsRefStr,
317    IntoStaticStr,
318    VariantNames,
319    VariantArray,
320    EnumIter,
321)]
322#[strum(serialize_all = "kebab-case")]
323pub enum InvalidBlockHookType {
324    /// A witness value enum
325    Witness,
326    /// A prestate trace value enum
327    PreState,
328    /// An opcode trace value enum
329    Opcode,
330}
331
332impl FromStr for InvalidBlockHookType {
333    type Err = ParseError;
334
335    fn from_str(s: &str) -> Result<Self, Self::Err> {
336        Ok(match s {
337            "witness" => Self::Witness,
338            "prestate" => Self::PreState,
339            "opcode" => Self::Opcode,
340            _ => return Err(ParseError::VariantNotFound),
341        })
342    }
343}
344
345impl TryFrom<&str> for InvalidBlockHookType {
346    type Error = ParseError;
347    fn try_from(s: &str) -> Result<Self, <Self as TryFrom<&str>>::Error> {
348        FromStr::from_str(s)
349    }
350}
351
352impl fmt::Display for InvalidBlockHookType {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        f.pad(self.as_ref())
355    }
356}
357
358impl InvalidBlockHookType {
359    /// Returns all variant names of the enum
360    pub const fn all_variant_names() -> &'static [&'static str] {
361        <Self as VariantNames>::VARIANTS
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use clap::Parser;
369
370    /// A helper type to parse Args more easily
371    #[derive(Parser)]
372    struct CommandParser<T: Args> {
373        #[command(flatten)]
374        args: T,
375    }
376
377    #[test]
378    fn test_parse_default_debug_args() {
379        let default_args = DebugArgs::default();
380        let args = CommandParser::<DebugArgs>::parse_from(["reth"]).args;
381        assert_eq!(args, default_args);
382    }
383
384    #[test]
385    fn test_parse_skip_state_root() {
386        let expected_args = DebugArgs { skip_state_root: true, ..Default::default() };
387        let args = CommandParser::<DebugArgs>::parse_from(["reth", "--debug.skip-state-root"]).args;
388        assert_eq!(args, expected_args);
389    }
390
391    #[test]
392    fn test_parse_invalid_block_args_none() {
393        let expected_args = DebugArgs {
394            invalid_block_hook: Some(InvalidBlockSelection::from(vec![])),
395            ..Default::default()
396        };
397        let args =
398            CommandParser::<DebugArgs>::parse_from(["reth", "--debug.invalid-block-hook", ""]).args;
399        assert_eq!(args, expected_args);
400    }
401
402    #[test]
403    fn test_parse_invalid_block_args() {
404        let expected_args = DebugArgs {
405            invalid_block_hook: Some(InvalidBlockSelection::from([InvalidBlockHookType::Witness])),
406            ..Default::default()
407        };
408        let args = CommandParser::<DebugArgs>::parse_from([
409            "reth",
410            "--debug.invalid-block-hook",
411            "witness",
412        ])
413        .args;
414        assert_eq!(args, expected_args);
415
416        let expected_args = DebugArgs {
417            invalid_block_hook: Some(InvalidBlockSelection::from([
418                InvalidBlockHookType::Witness,
419                InvalidBlockHookType::PreState,
420            ])),
421            ..Default::default()
422        };
423        let args = CommandParser::<DebugArgs>::parse_from([
424            "reth",
425            "--debug.invalid-block-hook",
426            "witness,prestate",
427        ])
428        .args;
429        assert_eq!(args, expected_args);
430
431        let args = CommandParser::<DebugArgs>::parse_from([
432            "reth",
433            "--debug.invalid-block-hook",
434            "witness,prestate,prestate",
435        ])
436        .args;
437        assert_eq!(args, expected_args);
438
439        let args = CommandParser::<DebugArgs>::parse_from([
440            "reth",
441            "--debug.invalid-block-hook",
442            "witness,witness,prestate",
443        ])
444        .args;
445        assert_eq!(args, expected_args);
446
447        let args = CommandParser::<DebugArgs>::parse_from([
448            "reth",
449            "--debug.invalid-block-hook",
450            "prestate,witness,prestate",
451        ])
452        .args;
453        assert_eq!(args, expected_args);
454    }
455}