reth_ethereum_cli/debug_cmd/
merkle.rs

1//! Command for debugging merkle tree calculation.
2use alloy_eips::BlockHashOrNumber;
3use backon::{ConstantBuilder, Retryable};
4use clap::Parser;
5use reth_chainspec::ChainSpec;
6use reth_cli::chainspec::ChainSpecParser;
7use reth_cli_commands::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
8use reth_cli_runner::CliContext;
9use reth_cli_util::get_secret_key;
10use reth_config::Config;
11use reth_consensus::{Consensus, ConsensusError};
12use reth_db_api::{cursor::DbCursorRO, tables, transaction::DbTx};
13use reth_ethereum_primitives::EthPrimitives;
14use reth_evm::{execute::Executor, ConfigureEvm};
15use reth_execution_types::ExecutionOutcome;
16use reth_network::{BlockDownloaderProvider, NetworkHandle};
17use reth_network_api::NetworkInfo;
18use reth_network_p2p::full_block::FullBlockClient;
19use reth_node_api::{BlockTy, NodePrimitives};
20use reth_node_core::{args::NetworkArgs, utils::get_single_header};
21use reth_node_ethereum::{consensus::EthBeaconConsensus, EthExecutorProvider};
22use reth_provider::{
23    providers::ProviderNodeTypes, BlockNumReader, BlockWriter, ChainSpecProvider,
24    DatabaseProviderFactory, LatestStateProviderRef, OriginalValuesKnown, ProviderFactory,
25    StateWriter, StorageLocation,
26};
27use reth_revm::database::StateProviderDatabase;
28use reth_stages::{
29    stages::{AccountHashingStage, MerkleStage, StorageHashingStage},
30    ExecInput, Stage, StageCheckpoint,
31};
32use reth_tasks::TaskExecutor;
33use std::{path::PathBuf, sync::Arc};
34use tracing::*;
35
36/// `reth debug merkle` command
37#[derive(Debug, Parser)]
38pub struct Command<C: ChainSpecParser> {
39    #[command(flatten)]
40    env: EnvironmentArgs<C>,
41
42    #[command(flatten)]
43    network: NetworkArgs,
44
45    /// The number of retries per request
46    #[arg(long, default_value = "5")]
47    retries: usize,
48
49    /// The height to finish at
50    #[arg(long)]
51    to: u64,
52
53    /// The depth after which we should start comparing branch nodes
54    #[arg(long)]
55    skip_node_depth: Option<usize>,
56}
57
58impl<C: ChainSpecParser<ChainSpec = ChainSpec>> Command<C> {
59    async fn build_network<
60        N: ProviderNodeTypes<
61            ChainSpec = C::ChainSpec,
62            Primitives: NodePrimitives<
63                Block = reth_ethereum_primitives::Block,
64                Receipt = reth_ethereum_primitives::Receipt,
65                BlockHeader = alloy_consensus::Header,
66            >,
67        >,
68    >(
69        &self,
70        config: &Config,
71        task_executor: TaskExecutor,
72        provider_factory: ProviderFactory<N>,
73        network_secret_path: PathBuf,
74        default_peers_path: PathBuf,
75    ) -> eyre::Result<NetworkHandle> {
76        let secret_key = get_secret_key(&network_secret_path)?;
77        let network = self
78            .network
79            .network_config(config, provider_factory.chain_spec(), secret_key, default_peers_path)
80            .with_task_executor(Box::new(task_executor))
81            .build(provider_factory)
82            .start_network()
83            .await?;
84        info!(target: "reth::cli", peer_id = %network.peer_id(), local_addr = %network.local_addr(), "Connected to P2P network");
85        debug!(target: "reth::cli", peer_id = ?network.peer_id(), "Full peer ID");
86        Ok(network)
87    }
88
89    /// Execute `merkle-debug` command
90    pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec, Primitives = EthPrimitives>>(
91        self,
92        ctx: CliContext,
93    ) -> eyre::Result<()> {
94        let Environment { provider_factory, config, data_dir } =
95            self.env.init::<N>(AccessRights::RW)?;
96
97        let provider_rw = provider_factory.database_provider_rw()?;
98
99        // Configure and build network
100        let network_secret_path =
101            self.network.p2p_secret_key.clone().unwrap_or_else(|| data_dir.p2p_secret());
102        let network = self
103            .build_network(
104                &config,
105                ctx.task_executor.clone(),
106                provider_factory.clone(),
107                network_secret_path,
108                data_dir.known_peers(),
109            )
110            .await?;
111
112        let executor_provider = EthExecutorProvider::ethereum(provider_factory.chain_spec());
113
114        // Initialize the fetch client
115        info!(target: "reth::cli", target_block_number = self.to, "Downloading tip of block range");
116        let fetch_client = network.fetch_client().await?;
117
118        // fetch the header at `self.to`
119        let retries = self.retries.max(1);
120        let backoff = ConstantBuilder::default().with_max_times(retries);
121        let client = fetch_client.clone();
122        let to_header = (move || {
123            get_single_header(client.clone(), BlockHashOrNumber::Number(self.to))
124        })
125        .retry(backoff)
126        .notify(|err, _| warn!(target: "reth::cli", "Error requesting header: {err}. Retrying..."))
127        .await?;
128        info!(target: "reth::cli", target_block_number=self.to, "Finished downloading tip of block range");
129
130        // build the full block client
131        let consensus: Arc<dyn Consensus<BlockTy<N>, Error = ConsensusError>> =
132            Arc::new(EthBeaconConsensus::new(provider_factory.chain_spec()));
133        let block_range_client = FullBlockClient::new(fetch_client, consensus);
134
135        // get best block number
136        let best_block_number = provider_rw.best_block_number()?;
137        assert!(best_block_number < self.to, "Nothing to run");
138
139        // get the block range from the network
140        let block_range = best_block_number + 1..=self.to;
141        info!(target: "reth::cli", ?block_range, "Downloading range of blocks");
142        let blocks = block_range_client
143            .get_full_block_range(to_header.hash_slow(), self.to - best_block_number)
144            .await;
145
146        let mut account_hashing_stage = AccountHashingStage::default();
147        let mut storage_hashing_stage = StorageHashingStage::default();
148        let mut merkle_stage = MerkleStage::default_execution();
149
150        for block in blocks.into_iter().rev() {
151            let block_number = block.number;
152            let sealed_block =
153                block.try_recover().map_err(|_| eyre::eyre!("Error sealing block with senders"))?;
154            trace!(target: "reth::cli", block_number, "Executing block");
155
156            provider_rw.insert_block(sealed_block.clone(), StorageLocation::Database)?;
157
158            let executor = executor_provider.batch_executor(StateProviderDatabase::new(
159                LatestStateProviderRef::new(&provider_rw),
160            ));
161            let output = executor.execute(&sealed_block)?;
162
163            provider_rw.write_state(
164                &ExecutionOutcome::single(block_number, output),
165                OriginalValuesKnown::Yes,
166                StorageLocation::Database,
167            )?;
168
169            let checkpoint = Some(StageCheckpoint::new(
170                block_number
171                    .checked_sub(1)
172                    .ok_or_else(|| eyre::eyre!("GenesisBlockHasNoParent"))?,
173            ));
174
175            let mut account_hashing_done = false;
176            while !account_hashing_done {
177                let output = account_hashing_stage
178                    .execute(&provider_rw, ExecInput { target: Some(block_number), checkpoint })?;
179                account_hashing_done = output.done;
180            }
181
182            let mut storage_hashing_done = false;
183            while !storage_hashing_done {
184                let output = storage_hashing_stage
185                    .execute(&provider_rw, ExecInput { target: Some(block_number), checkpoint })?;
186                storage_hashing_done = output.done;
187            }
188
189            let incremental_result = merkle_stage
190                .execute(&provider_rw, ExecInput { target: Some(block_number), checkpoint });
191
192            if incremental_result.is_ok() {
193                debug!(target: "reth::cli", block_number, "Successfully computed incremental root");
194                continue
195            }
196
197            warn!(target: "reth::cli", block_number, "Incremental calculation failed, retrying from scratch");
198            let incremental_account_trie = provider_rw
199                .tx_ref()
200                .cursor_read::<tables::AccountsTrie>()?
201                .walk_range(..)?
202                .collect::<Result<Vec<_>, _>>()?;
203            let incremental_storage_trie = provider_rw
204                .tx_ref()
205                .cursor_dup_read::<tables::StoragesTrie>()?
206                .walk_range(..)?
207                .collect::<Result<Vec<_>, _>>()?;
208
209            let clean_input = ExecInput { target: Some(sealed_block.number), checkpoint: None };
210            loop {
211                let clean_result = merkle_stage
212                    .execute(&provider_rw, clean_input)
213                    .map_err(|e| eyre::eyre!("Clean state root calculation failed: {}", e))?;
214                if clean_result.done {
215                    break;
216                }
217            }
218
219            let clean_account_trie = provider_rw
220                .tx_ref()
221                .cursor_read::<tables::AccountsTrie>()?
222                .walk_range(..)?
223                .collect::<Result<Vec<_>, _>>()?;
224            let clean_storage_trie = provider_rw
225                .tx_ref()
226                .cursor_dup_read::<tables::StoragesTrie>()?
227                .walk_range(..)?
228                .collect::<Result<Vec<_>, _>>()?;
229
230            info!(target: "reth::cli", block_number, "Comparing incremental trie vs clean trie");
231
232            // Account trie
233            let mut incremental_account_mismatched = Vec::new();
234            let mut clean_account_mismatched = Vec::new();
235            let mut incremental_account_trie_iter = incremental_account_trie.into_iter().peekable();
236            let mut clean_account_trie_iter = clean_account_trie.into_iter().peekable();
237            while incremental_account_trie_iter.peek().is_some() ||
238                clean_account_trie_iter.peek().is_some()
239            {
240                match (incremental_account_trie_iter.next(), clean_account_trie_iter.next()) {
241                    (Some(incremental), Some(clean)) => {
242                        similar_asserts::assert_eq!(incremental.0, clean.0, "Nibbles don't match");
243                        if incremental.1 != clean.1 &&
244                            clean.0 .0.len() > self.skip_node_depth.unwrap_or_default()
245                        {
246                            incremental_account_mismatched.push(incremental);
247                            clean_account_mismatched.push(clean);
248                        }
249                    }
250                    (Some(incremental), None) => {
251                        warn!(target: "reth::cli", next = ?incremental, "Incremental account trie has more entries");
252                    }
253                    (None, Some(clean)) => {
254                        warn!(target: "reth::cli", next = ?clean, "Clean account trie has more entries");
255                    }
256                    (None, None) => {
257                        info!(target: "reth::cli", "Exhausted all account trie entries");
258                    }
259                }
260            }
261
262            // Storage trie
263            let mut first_mismatched_storage = None;
264            let mut incremental_storage_trie_iter = incremental_storage_trie.into_iter().peekable();
265            let mut clean_storage_trie_iter = clean_storage_trie.into_iter().peekable();
266            while incremental_storage_trie_iter.peek().is_some() ||
267                clean_storage_trie_iter.peek().is_some()
268            {
269                match (incremental_storage_trie_iter.next(), clean_storage_trie_iter.next()) {
270                    (Some(incremental), Some(clean)) => {
271                        if incremental != clean &&
272                            clean.1.nibbles.len() > self.skip_node_depth.unwrap_or_default()
273                        {
274                            first_mismatched_storage = Some((incremental, clean));
275                            break
276                        }
277                    }
278                    (Some(incremental), None) => {
279                        warn!(target: "reth::cli", next = ?incremental, "Incremental storage trie has more entries");
280                    }
281                    (None, Some(clean)) => {
282                        warn!(target: "reth::cli", next = ?clean, "Clean storage trie has more entries")
283                    }
284                    (None, None) => {
285                        info!(target: "reth::cli", "Exhausted all storage trie entries.")
286                    }
287                }
288            }
289
290            similar_asserts::assert_eq!(
291                (
292                    incremental_account_mismatched,
293                    first_mismatched_storage.as_ref().map(|(incremental, _)| incremental)
294                ),
295                (
296                    clean_account_mismatched,
297                    first_mismatched_storage.as_ref().map(|(_, clean)| clean)
298                ),
299                "Mismatched trie nodes"
300            );
301        }
302
303        info!(target: "reth::cli", ?block_range, "Successfully validated incremental roots");
304
305        Ok(())
306    }
307}
308
309impl<C: ChainSpecParser> Command<C> {
310    /// Returns the underlying chain being used to run this command
311    pub const fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
312        Some(&self.env.chain)
313    }
314}