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