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