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