reth/commands/debug_cmd/
build_block.rs

1//! Command for debugging block building.
2use alloy_consensus::TxEip4844;
3use alloy_eips::{
4    eip2718::Encodable2718,
5    eip4844::{env_settings::EnvKzgSettings, BlobTransactionSidecar},
6};
7use alloy_primitives::{Address, Bytes, B256, U256};
8use alloy_rlp::Decodable;
9use alloy_rpc_types::engine::{BlobsBundleV1, PayloadAttributes};
10use clap::Parser;
11use eyre::Context;
12use reth_basic_payload_builder::{
13    BuildArguments, BuildOutcome, Cancelled, PayloadBuilder, PayloadConfig,
14};
15use reth_beacon_consensus::EthBeaconConsensus;
16use reth_blockchain_tree::{
17    BlockchainTree, BlockchainTreeConfig, ShareableBlockchainTree, TreeExternals,
18};
19use reth_chainspec::ChainSpec;
20use reth_cli::chainspec::ChainSpecParser;
21use reth_cli_commands::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
22use reth_cli_runner::CliContext;
23use reth_consensus::{Consensus, FullConsensus};
24use reth_errors::RethResult;
25use reth_ethereum_payload_builder::EthereumBuilderConfig;
26use reth_evm::execute::{BlockExecutorProvider, Executor};
27use reth_execution_types::ExecutionOutcome;
28use reth_fs_util as fs;
29use reth_node_api::{BlockTy, EngineApiMessageVersion, PayloadBuilderAttributes};
30use reth_node_ethereum::{EthEvmConfig, EthExecutorProvider};
31use reth_primitives::{
32    BlobTransaction, BlockExt, PooledTransactionsElement, SealedBlockFor, SealedBlockWithSenders,
33    SealedHeader, Transaction, TransactionSigned,
34};
35use reth_provider::{
36    providers::{BlockchainProvider, ProviderNodeTypes},
37    BlockHashReader, BlockReader, BlockWriter, ChainSpecProvider, ProviderFactory,
38    StageCheckpointReader, StateProviderFactory,
39};
40use reth_revm::{cached::CachedReads, database::StateProviderDatabase, primitives::KzgSettings};
41use reth_stages::StageId;
42use reth_transaction_pool::{
43    blobstore::InMemoryBlobStore, BlobStore, EthPooledTransaction, PoolConfig, TransactionOrigin,
44    TransactionPool, TransactionValidationTaskExecutor,
45};
46use reth_trie::StateRoot;
47use reth_trie_db::DatabaseStateRoot;
48use std::{path::PathBuf, str::FromStr, sync::Arc};
49use tracing::*;
50
51/// `reth debug build-block` command
52/// This debug routine requires that the node is positioned at the block before the target.
53/// The script will then parse the block and attempt to build a similar one.
54#[derive(Debug, Parser)]
55pub struct Command<C: ChainSpecParser> {
56    #[command(flatten)]
57    env: EnvironmentArgs<C>,
58
59    /// Overrides the KZG trusted setup by reading from the supplied file.
60    #[arg(long, value_name = "PATH")]
61    trusted_setup_file: Option<PathBuf>,
62
63    #[arg(long)]
64    parent_beacon_block_root: Option<B256>,
65
66    #[arg(long)]
67    prev_randao: B256,
68
69    #[arg(long)]
70    timestamp: u64,
71
72    #[arg(long)]
73    suggested_fee_recipient: Address,
74
75    /// Array of transactions.
76    /// NOTE: 4844 transactions must be provided in the same order as they appear in the blobs
77    /// bundle.
78    #[arg(long, value_delimiter = ',')]
79    transactions: Vec<String>,
80
81    /// Path to the file that contains a corresponding blobs bundle.
82    #[arg(long)]
83    blobs_bundle_path: Option<PathBuf>,
84}
85
86impl<C: ChainSpecParser<ChainSpec = ChainSpec>> Command<C> {
87    /// Fetches the best block block from the database.
88    ///
89    /// If the database is empty, returns the genesis block.
90    fn lookup_best_block<N: ProviderNodeTypes<ChainSpec = C::ChainSpec>>(
91        &self,
92        factory: ProviderFactory<N>,
93    ) -> RethResult<Arc<SealedBlockFor<BlockTy<N>>>> {
94        let provider = factory.provider()?;
95
96        let best_number =
97            provider.get_stage_checkpoint(StageId::Finish)?.unwrap_or_default().block_number;
98        let best_hash = provider
99            .block_hash(best_number)?
100            .expect("the hash for the latest block is missing, database is corrupt");
101
102        Ok(Arc::new(
103            provider
104                .block(best_number.into())?
105                .expect("the header for the latest block is missing, database is corrupt")
106                .seal(best_hash),
107        ))
108    }
109
110    /// Loads the trusted setup params from a given file path or falls back to
111    /// `EnvKzgSettings::Default`.
112    fn kzg_settings(&self) -> eyre::Result<EnvKzgSettings> {
113        if let Some(ref trusted_setup_file) = self.trusted_setup_file {
114            let trusted_setup = KzgSettings::load_trusted_setup_file(trusted_setup_file)
115                .wrap_err_with(|| {
116                    format!("Failed to load trusted setup file: {:?}", trusted_setup_file)
117                })?;
118            Ok(EnvKzgSettings::Custom(Arc::new(trusted_setup)))
119        } else {
120            Ok(EnvKzgSettings::Default)
121        }
122    }
123
124    /// Execute `debug in-memory-merkle` command
125    pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(
126        self,
127        ctx: CliContext,
128    ) -> eyre::Result<()> {
129        let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
130
131        let consensus: Arc<dyn FullConsensus> =
132            Arc::new(EthBeaconConsensus::new(provider_factory.chain_spec()));
133
134        let executor = EthExecutorProvider::ethereum(provider_factory.chain_spec());
135
136        // configure blockchain tree
137        let tree_externals =
138            TreeExternals::new(provider_factory.clone(), Arc::clone(&consensus), executor);
139        let tree = BlockchainTree::new(tree_externals, BlockchainTreeConfig::default())?;
140        let blockchain_tree = Arc::new(ShareableBlockchainTree::new(tree));
141
142        // fetch the best block from the database
143        let best_block = self
144            .lookup_best_block(provider_factory.clone())
145            .wrap_err("the head block is missing")?;
146
147        let blockchain_db =
148            BlockchainProvider::new(provider_factory.clone(), blockchain_tree.clone())?;
149        let blob_store = InMemoryBlobStore::default();
150
151        let validator =
152            TransactionValidationTaskExecutor::eth_builder(provider_factory.chain_spec())
153                .with_head_timestamp(best_block.timestamp)
154                .kzg_settings(self.kzg_settings()?)
155                .with_additional_tasks(1)
156                .build_with_tasks(
157                    blockchain_db.clone(),
158                    ctx.task_executor.clone(),
159                    blob_store.clone(),
160                );
161
162        let transaction_pool = reth_transaction_pool::Pool::eth_pool(
163            validator,
164            blob_store.clone(),
165            PoolConfig::default(),
166        );
167        info!(target: "reth::cli", "Transaction pool initialized");
168
169        let mut blobs_bundle = self
170            .blobs_bundle_path
171            .map(|path| -> eyre::Result<BlobsBundleV1> {
172                let contents = fs::read_to_string(&path)
173                    .wrap_err(format!("could not read {}", path.display()))?;
174                serde_json::from_str(&contents).wrap_err("failed to deserialize blobs bundle")
175            })
176            .transpose()?;
177
178        for tx_bytes in &self.transactions {
179            debug!(target: "reth::cli", bytes = ?tx_bytes, "Decoding transaction");
180            let transaction = TransactionSigned::decode(&mut &Bytes::from_str(tx_bytes)?[..])?
181                .into_ecrecovered()
182                .ok_or_else(|| eyre::eyre!("failed to recover tx"))?;
183
184            let encoded_length = match &transaction.transaction {
185                Transaction::Eip4844(TxEip4844 { blob_versioned_hashes, .. }) => {
186                    let blobs_bundle = blobs_bundle.as_mut().ok_or_else(|| {
187                        eyre::eyre!("encountered a blob tx. `--blobs-bundle-path` must be provided")
188                    })?;
189
190                    let sidecar: BlobTransactionSidecar =
191                        blobs_bundle.pop_sidecar(blob_versioned_hashes.len());
192
193                    // first construct the tx, calculating the length of the tx with sidecar before
194                    // insertion
195                    let tx = BlobTransaction::try_from_signed(
196                        transaction.as_ref().clone(),
197                        sidecar.clone(),
198                    )
199                    .expect("should not fail to convert blob tx if it is already eip4844");
200                    let pooled = PooledTransactionsElement::BlobTransaction(tx);
201                    let encoded_length = pooled.encode_2718_len();
202
203                    // insert the blob into the store
204                    blob_store.insert(transaction.hash(), sidecar)?;
205
206                    encoded_length
207                }
208                _ => transaction.encode_2718_len(),
209            };
210
211            debug!(target: "reth::cli", ?transaction, "Adding transaction to the pool");
212            transaction_pool
213                .add_transaction(
214                    TransactionOrigin::External,
215                    EthPooledTransaction::new(transaction, encoded_length),
216                )
217                .await?;
218        }
219
220        let payload_attrs = PayloadAttributes {
221            parent_beacon_block_root: self.parent_beacon_block_root,
222            prev_randao: self.prev_randao,
223            timestamp: self.timestamp,
224            suggested_fee_recipient: self.suggested_fee_recipient,
225            // TODO: add support for withdrawals
226            withdrawals: None,
227            target_blobs_per_block: None,
228            max_blobs_per_block: None,
229        };
230        let payload_config = PayloadConfig::new(
231            Arc::new(SealedHeader::new(best_block.header().clone(), best_block.hash())),
232            reth_payload_builder::EthPayloadBuilderAttributes::try_new(
233                best_block.hash(),
234                payload_attrs,
235                EngineApiMessageVersion::default() as u8,
236            )?,
237        );
238
239        let args = BuildArguments::new(
240            blockchain_db.clone(),
241            transaction_pool,
242            CachedReads::default(),
243            payload_config,
244            Cancelled::default(),
245            None,
246        );
247
248        let payload_builder = reth_ethereum_payload_builder::EthereumPayloadBuilder::new(
249            EthEvmConfig::new(provider_factory.chain_spec()),
250            EthereumBuilderConfig::new(Default::default()),
251        );
252
253        match payload_builder.try_build(args)? {
254            BuildOutcome::Better { payload, .. } => {
255                let block = payload.block();
256                debug!(target: "reth::cli", ?block, "Built new payload");
257
258                consensus.validate_header_with_total_difficulty(block, U256::MAX)?;
259                consensus.validate_header(block)?;
260                consensus.validate_block_pre_execution(block)?;
261
262                let senders = block.senders().expect("sender recovery failed");
263                let block_with_senders =
264                    SealedBlockWithSenders::<BlockTy<N>>::new(block.clone(), senders).unwrap();
265
266                let state_provider = blockchain_db.latest()?;
267                let db = StateProviderDatabase::new(&state_provider);
268                let executor =
269                    EthExecutorProvider::ethereum(provider_factory.chain_spec()).executor(db);
270
271                let block_execution_output =
272                    executor.execute((&block_with_senders.clone().unseal(), U256::MAX).into())?;
273                let execution_outcome =
274                    ExecutionOutcome::from((block_execution_output, block.number));
275                debug!(target: "reth::cli", ?execution_outcome, "Executed block");
276
277                let hashed_post_state = state_provider.hashed_post_state(execution_outcome.state());
278                let (state_root, trie_updates) = StateRoot::overlay_root_with_updates(
279                    provider_factory.provider()?.tx_ref(),
280                    hashed_post_state.clone(),
281                )?;
282
283                if state_root != block_with_senders.state_root {
284                    eyre::bail!(
285                        "state root mismatch. expected: {}. got: {}",
286                        block_with_senders.state_root,
287                        state_root
288                    );
289                }
290
291                // Attempt to insert new block without committing
292                let provider_rw = provider_factory.provider_rw()?;
293                provider_rw.append_blocks_with_state(
294                    Vec::from([block_with_senders]),
295                    execution_outcome,
296                    hashed_post_state.into_sorted(),
297                    trie_updates,
298                )?;
299                info!(target: "reth::cli", "Successfully appended built block");
300            }
301            _ => unreachable!("other outcomes are unreachable"),
302        };
303
304        Ok(())
305    }
306}