reth_ethereum_cli/debug_cmd/
build_block.rs

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