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