diff --git a/Cargo.lock b/Cargo.lock index b1bcf4a81549f..7554591a28cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7803,6 +7803,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "strum", "tempfile", "tokio", "toml", diff --git a/book/cli/reth/node.md b/book/cli/reth/node.md index bf167c6a22298..ff0ded763cc93 100644 --- a/book/cli/reth/node.md +++ b/book/cli/reth/node.md @@ -515,6 +515,13 @@ Debug: --debug.engine-api-store The path to store engine API messages at. If specified, all of the intercepted engine API messages will be written to specified location + --debug.bad-block-hook + Determines which type of bad block hook to install + + Example: `witness,prestate` + + [possible values: witness, pre-state, opcode] + Database: --db.log-level Database logging level. Levels higher than "notice" require a debug build diff --git a/crates/engine/service/src/service.rs b/crates/engine/service/src/service.rs index 6783ed01d3125..78b8353b4171d 100644 --- a/crates/engine/service/src/service.rs +++ b/crates/engine/service/src/service.rs @@ -10,7 +10,7 @@ use reth_engine_tree::{ download::BasicBlockDownloader, engine::{EngineApiRequest, EngineApiRequestHandler, EngineHandler}, persistence::PersistenceHandle, - tree::{EngineApiTreeHandler, TreeConfig}, + tree::{EngineApiTreeHandler, InvalidBlockHook, TreeConfig}, }; pub use reth_engine_tree::{ chain::{ChainEvent, ChainOrchestrator}, @@ -80,6 +80,7 @@ where pruner: Pruner>, payload_builder: PayloadBuilderHandle, tree_config: TreeConfig, + invalid_block_hook: InvalidBlockHook, ) -> Self { let downloader = BasicBlockDownloader::new(client, consensus.clone()); @@ -97,6 +98,7 @@ where payload_builder, canonical_in_memory_state, tree_config, + invalid_block_hook, ); let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree); @@ -196,6 +198,7 @@ mod tests { pruner, PayloadBuilderHandle::new(tx), TreeConfig::default(), + Box::new(|_, _, _, _| {}), ); } } diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index d790f421c588f..06e5377bc83d8 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -24,12 +24,12 @@ use reth_payload_builder::PayloadBuilderHandle; use reth_payload_primitives::{PayloadAttributes, PayloadBuilderAttributes}; use reth_payload_validator::ExecutionPayloadValidator; use reth_primitives::{ - Block, BlockNumHash, BlockNumber, GotExpected, Header, Receipts, Requests, SealedBlock, - SealedBlockWithSenders, SealedHeader, B256, U256, + Block, BlockNumHash, BlockNumber, GotExpected, Header, Receipt, Receipts, Requests, + SealedBlock, SealedBlockWithSenders, SealedHeader, B256, U256, }; use reth_provider::{ - BlockReader, ExecutionOutcome, ProviderError, StateProviderBox, StateProviderFactory, - StateRootProvider, + BlockExecutionOutput, BlockReader, ExecutionOutcome, ProviderError, StateProviderBox, + StateProviderFactory, StateRootProvider, }; use reth_revm::database::StateProviderDatabase; use reth_rpc_types::{ @@ -40,9 +40,10 @@ use reth_rpc_types::{ ExecutionPayload, }; use reth_stages_api::ControlFlow; -use reth_trie::HashedPostState; +use reth_trie::{updates::TrieUpdates, HashedPostState}; use std::{ collections::{BTreeMap, HashMap, HashSet}, + fmt::Debug, ops::Bound, sync::{ mpsc::{Receiver, RecvError, RecvTimeoutError, Sender}, @@ -62,6 +63,16 @@ mod metrics; use crate::{engine::EngineApiRequest, tree::metrics::EngineApiMetrics}; pub use config::TreeConfig; +/// This exists only to share the hook definition across many function definitions easily +pub type InvalidBlockHook = Box< + dyn Fn( + SealedBlockWithSenders, + SealedHeader, + BlockExecutionOutput, + Option<(TrieUpdates, B256)>, + ) + Send, +>; + /// Keeps track of the state of the tree. /// /// ## Invariants @@ -377,7 +388,7 @@ pub enum TreeAction { /// /// This type is responsible for processing engine API requests, maintaining the canonical state and /// emitting events. -#[derive(Debug)] +// #[derive(Debug)] pub struct EngineApiTreeHandler { provider: P, executor_provider: E, @@ -414,6 +425,8 @@ pub struct EngineApiTreeHandler { config: TreeConfig, /// Metrics for the engine api. metrics: EngineApiMetrics, + /// An invalid block hook + invalid_block_hook: InvalidBlockHook, } impl EngineApiTreeHandler @@ -454,9 +467,15 @@ where config, metrics: Default::default(), incoming_tx, + invalid_block_hook: Box::new(|_, _, _, _| {}), } } + /// Sets the invalid block hook to be the given function + fn set_invalid_block_hook(&mut self, invalid_block_hook: InvalidBlockHook) { + self.invalid_block_hook = invalid_block_hook; + } + /// Creates a new [`EngineApiTreeHandler`] instance and spawns it in its /// own thread. /// @@ -472,6 +491,7 @@ where payload_builder: PayloadBuilderHandle, canonical_in_memory_state: CanonicalInMemoryState, config: TreeConfig, + invalid_block_hook: InvalidBlockHook, ) -> (Sender>>, UnboundedReceiver) { let best_block_number = provider.best_block_number().unwrap_or(0); let header = provider.sealed_header(best_block_number).ok().flatten().unwrap_or_default(); @@ -489,7 +509,7 @@ where header.num_hash(), ); - let task = Self::new( + let mut task = Self::new( provider, executor_provider, consensus, @@ -502,6 +522,7 @@ where payload_builder, config, ); + task.set_invalid_block_hook(invalid_block_hook); let incoming = task.incoming_tx.clone(); std::thread::Builder::new().name("Tree Task".to_string()).spawn(|| task.run()).unwrap(); (incoming, outgoing) @@ -1742,10 +1763,14 @@ where let output = executor.execute((&block, U256::MAX).into())?; debug!(target: "engine", elapsed=?exec_time.elapsed(), ?block_number, "Executed block"); - self.consensus.validate_block_post_execution( + if let Err(err) = self.consensus.validate_block_post_execution( &block, PostExecutionInput::new(&output.receipts, &output.requests), - )?; + ) { + // call post-block hook + (self.invalid_block_hook)(block.seal_slow(), parent_block, output, None); + return Err(err.into()) + } let hashed_state = HashedPostState::from_bundle_state(&output.state.state); @@ -1753,6 +1778,13 @@ where let (state_root, trie_output) = state_provider.hashed_state_root_with_updates(hashed_state.clone())?; if state_root != block.state_root { + // call post-block hook + (self.invalid_block_hook)( + block.clone().seal_slow(), + parent_block, + output, + Some((trie_output, state_root)), + ); return Err(ConsensusError::BodyStateRootDiff( GotExpected { got: state_root, expected: block.state_root }.into(), ) @@ -1999,6 +2031,27 @@ where } } +impl std::fmt::Debug for EngineApiTreeHandler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EngineApiTreeHandler") + .field("provider", &self.provider) + .field("executor_provider", &self.executor_provider) + .field("consensus", &self.consensus) + .field("payload_validator", &self.payload_validator) + .field("state", &self.state) + .field("incoming_tx", &self.incoming_tx) + .field("persistence", &self.persistence) + .field("persistence_state", &self.persistence_state) + .field("backfill_sync_state", &self.backfill_sync_state) + .field("canonical_in_memory_state", &self.canonical_in_memory_state) + .field("payload_builder", &self.payload_builder) + .field("config", &self.config) + .field("metrics", &self.metrics) + .field("invalid_block_hook", &format!("{:p}", self.invalid_block_hook)) + .finish() + } +} + /// The state of the persistence task. #[derive(Default, Debug)] pub struct PersistenceState { diff --git a/crates/node/builder/src/launch/engine.rs b/crates/node/builder/src/launch/engine.rs index 4d2ed714c2b5c..2dd404a22488b 100644 --- a/crates/node/builder/src/launch/engine.rs +++ b/crates/node/builder/src/launch/engine.rs @@ -29,7 +29,7 @@ use reth_rpc_engine_api::{capabilities::EngineCapabilities, EngineApi}; use reth_rpc_types::engine::ClientVersionV1; use reth_tasks::TaskExecutor; use reth_tokio_util::EventSender; -use reth_tracing::tracing::{debug, error, info}; +use reth_tracing::tracing::{debug, error, info, warn}; use tokio::sync::{mpsc::unbounded_channel, oneshot}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -196,6 +196,13 @@ where let pruner_events = pruner.events(); info!(target: "reth::cli", prune_config=?ctx.prune_config().unwrap_or_default(), "Pruner initialized"); + // TODO: implement methods which convert this value into an actual function + if let Some(ref hook_type) = ctx.node_config().debug.bad_block_hook { + warn!(target: "reth::cli", ?hook_type, "Bad block hooks are not implemented yet! The `debug.bad-block-hook` flag will do nothing for now."); + } + + let bad_block_hook = Box::new(|_, _, _, _| {}); + // Configure the consensus engine let mut eth_service = EngineService::new( ctx.consensus(), @@ -210,6 +217,7 @@ where pruner, ctx.components().payload_builder().clone(), TreeConfig::default(), + bad_block_hook, ); let event_sender = EventSender::default(); diff --git a/crates/node/core/Cargo.toml b/crates/node/core/Cargo.toml index 585f4a8eab3d6..437409c8277d2 100644 --- a/crates/node/core/Cargo.toml +++ b/crates/node/core/Cargo.toml @@ -53,6 +53,7 @@ rand.workspace = true derive_more.workspace = true toml.workspace = true serde.workspace = true +strum = { workspace = true, features = ["derive"] } # io dirs-next = "2.0.0" diff --git a/crates/node/core/src/args/debug.rs b/crates/node/core/src/args/debug.rs index 010e9112c437e..3bb0568203623 100644 --- a/crates/node/core/src/args/debug.rs +++ b/crates/node/core/src/args/debug.rs @@ -1,8 +1,12 @@ //! clap [Args](clap::Args) for debugging purposes -use clap::Args; +use clap::{ + builder::{PossibleValue, TypedValueParser}, + Arg, Args, Command, +}; use reth_primitives::B256; -use std::path::PathBuf; +use std::{collections::HashSet, ffi::OsStr, fmt, path::PathBuf, str::FromStr}; +use strum::{AsRefStr, EnumIter, IntoStaticStr, ParseError, VariantArray, VariantNames}; /// Parameters for debugging purposes #[derive(Debug, Clone, Args, PartialEq, Eq, Default)] @@ -59,8 +63,231 @@ pub struct DebugArgs { /// will be written to specified location. #[arg(long = "debug.engine-api-store", help_heading = "Debug", value_name = "PATH")] pub engine_api_store: Option, + + /// Determines which type of bad block hook to install + /// + /// Example: `witness,prestate` + #[arg(long = "debug.bad-block-hook", help_heading = "Debug", value_parser = BadBlockSelectionValueParser::default())] + pub bad_block_hook: Option, +} + +/// Describes the invalid block hooks that should be installed. +/// +/// # Example +/// +/// Create a [`BadBlockSelection`] from a selection. +/// +/// ``` +/// use reth_node_core::args::{BadBlockHook, BadBlockSelection}; +/// let config: BadBlockSelection = vec![BadBlockHook::Witness].into(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BadBlockSelection(HashSet); + +impl BadBlockSelection { + /// Creates a new _unique_ [`BadBlockSelection`] from the given items. + /// + /// # Note + /// + /// This will dedupe the selection and remove duplicates while preserving the order. + /// + /// # Example + /// + /// Create a selection from the [`BadBlockHook`] string identifiers + /// + /// ``` + /// use reth_node_core::args::{BadBlockHook, BadBlockSelection}; + /// let selection = vec!["witness", "prestate", "opcode"]; + /// let config = BadBlockSelection::try_from_selection(selection).unwrap(); + /// assert_eq!( + /// config, + /// BadBlockSelection::from([ + /// BadBlockHook::Witness, + /// BadBlockHook::PreState, + /// BadBlockHook::Opcode + /// ]) + /// ); + /// ``` + /// + /// Create a unique selection from the [`BadBlockHook`] string identifiers + /// + /// ``` + /// use reth_node_core::args::{BadBlockHook, BadBlockSelection}; + /// let selection = vec!["witness", "prestate", "opcode", "witness", "prestate"]; + /// let config = BadBlockSelection::try_from_selection(selection).unwrap(); + /// assert_eq!( + /// config, + /// BadBlockSelection::from([ + /// BadBlockHook::Witness, + /// BadBlockHook::PreState, + /// BadBlockHook::Opcode + /// ]) + /// ); + /// ``` + pub fn try_from_selection(selection: I) -> Result + where + I: IntoIterator, + T: TryInto, + { + selection.into_iter().map(TryInto::try_into).collect() + } +} + +impl From> for BadBlockSelection { + fn from(s: Vec) -> Self { + Self(s.into_iter().collect()) + } +} + +impl FromIterator for BadBlockSelection { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Self(iter.into_iter().collect()) + } +} + +impl FromStr for BadBlockSelection { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Ok(Self(Default::default())) + } + let hooks = s.split(',').map(str::trim).peekable(); + Self::try_from_selection(hooks) + } +} + +impl fmt::Display for BadBlockSelection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}]", self.0.iter().map(|s| s.to_string()).collect::>().join(", ")) + } } +/// clap value parser for [`BadBlockSelection`]. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +struct BadBlockSelectionValueParser; + +impl TypedValueParser for BadBlockSelectionValueParser { + type Value = BadBlockSelection; + + fn parse_ref( + &self, + _cmd: &Command, + arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + let val = + value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?; + val.parse::().map_err(|err| { + let arg = arg.map(|a| a.to_string()).unwrap_or_else(|| "...".to_owned()); + let possible_values = BadBlockHook::all_variant_names().to_vec().join(","); + let msg = format!( + "Invalid value '{val}' for {arg}: {err}.\n [possible values: {possible_values}]" + ); + clap::Error::raw(clap::error::ErrorKind::InvalidValue, msg) + }) + } + + fn possible_values(&self) -> Option + '_>> { + let values = BadBlockHook::all_variant_names().iter().map(PossibleValue::new); + Some(Box::new(values)) + } +} + +/// The type of bad block hook to install +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + AsRefStr, + IntoStaticStr, + VariantNames, + VariantArray, + EnumIter, +)] +#[strum(serialize_all = "kebab-case")] +pub enum BadBlockHook { + /// A witness value enum + Witness, + /// A prestate trace value enum + PreState, + /// An opcode trace value enum + Opcode, +} + +impl FromStr for BadBlockHook { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "witness" => Self::Witness, + "prestate" => Self::PreState, + "opcode" => Self::Opcode, + _ => return Err(ParseError::VariantNotFound), + }) + } +} + +impl TryFrom<&str> for BadBlockHook { + type Error = ParseError; + fn try_from(s: &str) -> Result>::Error> { + FromStr::from_str(s) + } +} + +impl fmt::Display for BadBlockHook { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad(self.as_ref()) + } +} + +impl BadBlockHook { + /// Returns all variant names of the enum + pub const fn all_variant_names() -> &'static [&'static str] { + ::VARIANTS + } +} + +// impl BadBlockHook { +// // TODO: should this be a function in its own crate? +// /// Returns the actual function for the desired type of hook. +// fn hook(&self) -> InvalidBlockHook { +// unimplemented!() +// } +// } + +// /// Returns a function, which is itself an invalid block hook, that calls all of the provided +// hooks /// in a loop for the same block. +// pub fn hook_stack(hooks: Vec) -> InvalidBlockHook { +// let new_hook = |block: SealedBlockWithSenders, parent: SealedHeader, exec_output: +// BlockExecutionOutput, trie_updates: Option<(TrieUpdates, B256)>| { for hook in +// hooks { let hook = hook.hook(); +// (hook)(block, parent, exec_output, trie_updates) +// } +// } +// } + +// TODO: impl various debug hooks somewhere (in this crate? would pull in lots of deps) and add a +// way to: +// * loop through multiple, run each trace +// * map enum variant => fn +// +// Witness => witness_hook +// +// use reth_primitives::{Receipt, SealedBlockWithSenders, SealedHeader, B256}; +// use reth_provider::BlockExecutionOutput; +// use reth_trie::TrieUpdates +// fn witness_hook(block: SealedBlockWithSenders, parent: SealedHeader, exec_output: +// BlockExecutionOutput, trie_updates: Option<(TrieUpdates, B256)>) { todo!() +// } + #[cfg(test)] mod tests { use super::*; diff --git a/crates/node/core/src/args/mod.rs b/crates/node/core/src/args/mod.rs index 5aa5c58633fc5..dcbd92d78a4d4 100644 --- a/crates/node/core/src/args/mod.rs +++ b/crates/node/core/src/args/mod.rs @@ -14,7 +14,7 @@ pub use rpc_state_cache::RpcStateCacheArgs; /// DebugArgs struct for debugging purposes mod debug; -pub use debug::DebugArgs; +pub use debug::{BadBlockHook, BadBlockSelection, DebugArgs}; /// DatabaseArgs struct for configuring the database mod database;