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