Skip to main content

reth_node_core/args/
debug.rs

1//! clap [Args](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
129impl Default for DebugArgs {
130    fn default() -> Self {
131        Self {
132            terminate: false,
133            tip: None,
134            max_block: None,
135            etherscan: None,
136            rpc_consensus_url: None,
137            skip_fcu: None,
138            skip_new_payload: None,
139            skip_state_root: false,
140            skip_genesis_validation: false,
141            reorg_frequency: None,
142            reorg_depth: None,
143            engine_api_store: None,
144            invalid_block_hook: Some(InvalidBlockSelection::default()),
145            healthy_node_rpc_url: None,
146            ethstats: None,
147            startup_sync_state_idle: false,
148        }
149    }
150}
151
152/// Describes the invalid block hooks that should be installed.
153///
154/// # Example
155///
156/// Create a [`InvalidBlockSelection`] from a selection.
157///
158/// ```
159/// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
160/// let config: InvalidBlockSelection = vec![InvalidBlockHookType::Witness].into();
161/// ```
162#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
163pub struct InvalidBlockSelection(HashSet<InvalidBlockHookType>);
164
165impl Default for InvalidBlockSelection {
166    fn default() -> Self {
167        Self([InvalidBlockHookType::Witness].into())
168    }
169}
170
171impl InvalidBlockSelection {
172    /// Creates a new _unique_ [`InvalidBlockSelection`] from the given items.
173    ///
174    /// # Note
175    ///
176    /// This will dedupe the selection and remove duplicates while preserving the order.
177    ///
178    /// # Example
179    ///
180    /// Create a selection from the [`InvalidBlockHookType`] string identifiers
181    ///
182    /// ```
183    /// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
184    /// let selection = vec!["witness", "prestate", "opcode"];
185    /// let config = InvalidBlockSelection::try_from_selection(selection).unwrap();
186    /// assert_eq!(
187    ///     config,
188    ///     InvalidBlockSelection::from([
189    ///         InvalidBlockHookType::Witness,
190    ///         InvalidBlockHookType::PreState,
191    ///         InvalidBlockHookType::Opcode
192    ///     ])
193    /// );
194    /// ```
195    ///
196    /// Create a unique selection from the [`InvalidBlockHookType`] string identifiers
197    ///
198    /// ```
199    /// use reth_node_core::args::{InvalidBlockHookType, InvalidBlockSelection};
200    /// let selection = vec!["witness", "prestate", "opcode", "witness", "prestate"];
201    /// let config = InvalidBlockSelection::try_from_selection(selection).unwrap();
202    /// assert_eq!(
203    ///     config,
204    ///     InvalidBlockSelection::from([
205    ///         InvalidBlockHookType::Witness,
206    ///         InvalidBlockHookType::PreState,
207    ///         InvalidBlockHookType::Opcode
208    ///     ])
209    /// );
210    /// ```
211    pub fn try_from_selection<I, T>(selection: I) -> Result<Self, T::Error>
212    where
213        I: IntoIterator<Item = T>,
214        T: TryInto<InvalidBlockHookType>,
215    {
216        selection.into_iter().map(TryInto::try_into).collect()
217    }
218
219    /// Clones the set of configured [`InvalidBlockHookType`].
220    pub fn to_selection(&self) -> HashSet<InvalidBlockHookType> {
221        self.0.clone()
222    }
223}
224
225impl From<&[InvalidBlockHookType]> for InvalidBlockSelection {
226    fn from(s: &[InvalidBlockHookType]) -> Self {
227        Self(s.iter().copied().collect())
228    }
229}
230
231impl From<Vec<InvalidBlockHookType>> for InvalidBlockSelection {
232    fn from(s: Vec<InvalidBlockHookType>) -> Self {
233        Self(s.into_iter().collect())
234    }
235}
236
237impl<const N: usize> From<[InvalidBlockHookType; N]> for InvalidBlockSelection {
238    fn from(s: [InvalidBlockHookType; N]) -> Self {
239        Self(s.iter().copied().collect())
240    }
241}
242
243impl FromIterator<InvalidBlockHookType> for InvalidBlockSelection {
244    fn from_iter<I>(iter: I) -> Self
245    where
246        I: IntoIterator<Item = InvalidBlockHookType>,
247    {
248        Self(iter.into_iter().collect())
249    }
250}
251
252impl FromStr for InvalidBlockSelection {
253    type Err = ParseError;
254
255    fn from_str(s: &str) -> Result<Self, Self::Err> {
256        if s.is_empty() {
257            return Ok(Self(Default::default()))
258        }
259        let hooks = s.split(',').map(str::trim).peekable();
260        Self::try_from_selection(hooks)
261    }
262}
263
264impl fmt::Display for InvalidBlockSelection {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        write!(f, "[{}]", self.0.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(", "))
267    }
268}
269
270/// clap value parser for [`InvalidBlockSelection`].
271#[derive(Clone, Debug, Default)]
272#[non_exhaustive]
273struct InvalidBlockSelectionValueParser;
274
275impl TypedValueParser for InvalidBlockSelectionValueParser {
276    type Value = InvalidBlockSelection;
277
278    fn parse_ref(
279        &self,
280        _cmd: &Command,
281        arg: Option<&Arg>,
282        value: &OsStr,
283    ) -> Result<Self::Value, clap::Error> {
284        let val =
285            value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
286        val.parse::<InvalidBlockSelection>().map_err(|err| {
287            let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned());
288            let possible_values = InvalidBlockHookType::all_variant_names().to_vec().join(",");
289            let msg = format!(
290                "Invalid value '{val}' for {arg}: {err}.\n    [possible values: {possible_values}]"
291            );
292            clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg)
293        })
294    }
295
296    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
297        let values = InvalidBlockHookType::all_variant_names().iter().map(PossibleValue::new);
298        Some(Box::new(values))
299    }
300}
301
302/// The type of invalid block hook to install
303#[derive(
304    Debug,
305    Clone,
306    Copy,
307    PartialEq,
308    Eq,
309    Hash,
310    AsRefStr,
311    IntoStaticStr,
312    VariantNames,
313    VariantArray,
314    EnumIter,
315)]
316#[strum(serialize_all = "kebab-case")]
317pub enum InvalidBlockHookType {
318    /// A witness value enum
319    Witness,
320    /// A prestate trace value enum
321    PreState,
322    /// An opcode trace value enum
323    Opcode,
324}
325
326impl FromStr for InvalidBlockHookType {
327    type Err = ParseError;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        Ok(match s {
331            "witness" => Self::Witness,
332            "prestate" => Self::PreState,
333            "opcode" => Self::Opcode,
334            _ => return Err(ParseError::VariantNotFound),
335        })
336    }
337}
338
339impl TryFrom<&str> for InvalidBlockHookType {
340    type Error = ParseError;
341    fn try_from(s: &str) -> Result<Self, <Self as TryFrom<&str>>::Error> {
342        FromStr::from_str(s)
343    }
344}
345
346impl fmt::Display for InvalidBlockHookType {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        f.pad(self.as_ref())
349    }
350}
351
352impl InvalidBlockHookType {
353    /// Returns all variant names of the enum
354    pub const fn all_variant_names() -> &'static [&'static str] {
355        <Self as VariantNames>::VARIANTS
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use clap::Parser;
363
364    /// A helper type to parse Args more easily
365    #[derive(Parser)]
366    struct CommandParser<T: Args> {
367        #[command(flatten)]
368        args: T,
369    }
370
371    #[test]
372    fn test_parse_default_debug_args() {
373        let default_args = DebugArgs::default();
374        let args = CommandParser::<DebugArgs>::parse_from(["reth"]).args;
375        assert_eq!(args, default_args);
376    }
377
378    #[test]
379    fn test_parse_skip_state_root() {
380        let expected_args = DebugArgs { skip_state_root: true, ..Default::default() };
381        let args = CommandParser::<DebugArgs>::parse_from(["reth", "--debug.skip-state-root"]).args;
382        assert_eq!(args, expected_args);
383    }
384
385    #[test]
386    fn test_parse_invalid_block_args_none() {
387        let expected_args = DebugArgs {
388            invalid_block_hook: Some(InvalidBlockSelection::from(vec![])),
389            ..Default::default()
390        };
391        let args =
392            CommandParser::<DebugArgs>::parse_from(["reth", "--debug.invalid-block-hook", ""]).args;
393        assert_eq!(args, expected_args);
394    }
395
396    #[test]
397    fn test_parse_invalid_block_args() {
398        let expected_args = DebugArgs {
399            invalid_block_hook: Some(InvalidBlockSelection::from([InvalidBlockHookType::Witness])),
400            ..Default::default()
401        };
402        let args = CommandParser::<DebugArgs>::parse_from([
403            "reth",
404            "--debug.invalid-block-hook",
405            "witness",
406        ])
407        .args;
408        assert_eq!(args, expected_args);
409
410        let expected_args = DebugArgs {
411            invalid_block_hook: Some(InvalidBlockSelection::from([
412                InvalidBlockHookType::Witness,
413                InvalidBlockHookType::PreState,
414            ])),
415            ..Default::default()
416        };
417        let args = CommandParser::<DebugArgs>::parse_from([
418            "reth",
419            "--debug.invalid-block-hook",
420            "witness,prestate",
421        ])
422        .args;
423        assert_eq!(args, expected_args);
424
425        let args = CommandParser::<DebugArgs>::parse_from([
426            "reth",
427            "--debug.invalid-block-hook",
428            "witness,prestate,prestate",
429        ])
430        .args;
431        assert_eq!(args, expected_args);
432
433        let args = CommandParser::<DebugArgs>::parse_from([
434            "reth",
435            "--debug.invalid-block-hook",
436            "witness,witness,prestate",
437        ])
438        .args;
439        assert_eq!(args, expected_args);
440
441        let args = CommandParser::<DebugArgs>::parse_from([
442            "reth",
443            "--debug.invalid-block-hook",
444            "prestate,witness,prestate",
445        ])
446        .args;
447        assert_eq!(args, expected_args);
448    }
449}