mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
feat(reth-bench): send-invalid-payload command (#21335)
This commit is contained in:
@@ -32,7 +32,7 @@ alloy-eips.workspace = true
|
|||||||
alloy-json-rpc.workspace = true
|
alloy-json-rpc.workspace = true
|
||||||
alloy-consensus.workspace = true
|
alloy-consensus.workspace = true
|
||||||
alloy-network.workspace = true
|
alloy-network.workspace = true
|
||||||
alloy-primitives.workspace = true
|
alloy-primitives = { workspace = true, features = ["rand"] }
|
||||||
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }
|
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }
|
||||||
alloy-pubsub.workspace = true
|
alloy-pubsub.workspace = true
|
||||||
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
|
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mod new_payload_fcu;
|
|||||||
mod new_payload_only;
|
mod new_payload_only;
|
||||||
mod output;
|
mod output;
|
||||||
mod replay_payloads;
|
mod replay_payloads;
|
||||||
|
mod send_invalid_payload;
|
||||||
mod send_payload;
|
mod send_payload;
|
||||||
|
|
||||||
/// `reth bench` command
|
/// `reth bench` command
|
||||||
@@ -74,6 +75,18 @@ pub enum Subcommands {
|
|||||||
/// `reth-bench replay-payloads --payload-dir ./payloads --engine-rpc-url
|
/// `reth-bench replay-payloads --payload-dir ./payloads --engine-rpc-url
|
||||||
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex`
|
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex`
|
||||||
ReplayPayloads(replay_payloads::Command),
|
ReplayPayloads(replay_payloads::Command),
|
||||||
|
|
||||||
|
/// Generate and send an invalid `engine_newPayload` request for testing.
|
||||||
|
///
|
||||||
|
/// Takes a valid block and modifies fields to make it invalid, allowing you to test
|
||||||
|
/// Engine API rejection behavior. Block hash is recalculated after modifications
|
||||||
|
/// unless `--invalid-block-hash` or `--skip-hash-recalc` is used.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// `cast block latest --full --json | reth-bench send-invalid-payload --rpc-url localhost:5000
|
||||||
|
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex) --invalid-state-root`
|
||||||
|
SendInvalidPayload(Box<send_invalid_payload::Command>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BenchmarkCommand {
|
impl BenchmarkCommand {
|
||||||
@@ -89,6 +102,7 @@ impl BenchmarkCommand {
|
|||||||
Subcommands::SendPayload(command) => command.execute(ctx).await,
|
Subcommands::SendPayload(command) => command.execute(ctx).await,
|
||||||
Subcommands::GenerateBigBlock(command) => command.execute(ctx).await,
|
Subcommands::GenerateBigBlock(command) => command.execute(ctx).await,
|
||||||
Subcommands::ReplayPayloads(command) => command.execute(ctx).await,
|
Subcommands::ReplayPayloads(command) => command.execute(ctx).await,
|
||||||
|
Subcommands::SendInvalidPayload(command) => (*command).execute(ctx).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
219
bin/reth-bench/src/bench/send_invalid_payload/invalidation.rs
Normal file
219
bin/reth-bench/src/bench/send_invalid_payload/invalidation.rs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
use alloy_eips::eip4895::Withdrawal;
|
||||||
|
use alloy_primitives::{Address, Bloom, Bytes, B256, U256};
|
||||||
|
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
|
||||||
|
|
||||||
|
/// Configuration for invalidating payload fields
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(super) struct InvalidationConfig {
|
||||||
|
// Explicit value overrides (Option<T>)
|
||||||
|
pub(super) parent_hash: Option<B256>,
|
||||||
|
pub(super) fee_recipient: Option<Address>,
|
||||||
|
pub(super) state_root: Option<B256>,
|
||||||
|
pub(super) receipts_root: Option<B256>,
|
||||||
|
pub(super) logs_bloom: Option<Bloom>,
|
||||||
|
pub(super) prev_randao: Option<B256>,
|
||||||
|
pub(super) block_number: Option<u64>,
|
||||||
|
pub(super) gas_limit: Option<u64>,
|
||||||
|
pub(super) gas_used: Option<u64>,
|
||||||
|
pub(super) timestamp: Option<u64>,
|
||||||
|
pub(super) extra_data: Option<Bytes>,
|
||||||
|
pub(super) base_fee_per_gas: Option<u64>,
|
||||||
|
pub(super) block_hash: Option<B256>,
|
||||||
|
pub(super) blob_gas_used: Option<u64>,
|
||||||
|
pub(super) excess_blob_gas: Option<u64>,
|
||||||
|
|
||||||
|
// Auto-invalidation flags
|
||||||
|
pub(super) invalidate_parent_hash: bool,
|
||||||
|
pub(super) invalidate_state_root: bool,
|
||||||
|
pub(super) invalidate_receipts_root: bool,
|
||||||
|
pub(super) invalidate_gas_used: bool,
|
||||||
|
pub(super) invalidate_block_number: bool,
|
||||||
|
pub(super) invalidate_timestamp: bool,
|
||||||
|
pub(super) invalidate_base_fee: bool,
|
||||||
|
pub(super) invalidate_transactions: bool,
|
||||||
|
pub(super) invalidate_block_hash: bool,
|
||||||
|
pub(super) invalidate_withdrawals: bool,
|
||||||
|
pub(super) invalidate_blob_gas_used: bool,
|
||||||
|
pub(super) invalidate_excess_blob_gas: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvalidationConfig {
|
||||||
|
/// Returns true if `block_hash` is being explicitly set or auto-invalidated.
|
||||||
|
/// When true, the caller should skip recalculating the block hash since it will be overwritten.
|
||||||
|
pub(super) const fn should_skip_hash_recalc(&self) -> bool {
|
||||||
|
self.block_hash.is_some() || self.invalidate_block_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies invalidations to a V1 payload, returns list of what was changed.
|
||||||
|
pub(super) fn apply_to_payload_v1(&self, payload: &mut ExecutionPayloadV1) -> Vec<String> {
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
|
||||||
|
// Explicit value overrides
|
||||||
|
if let Some(parent_hash) = self.parent_hash {
|
||||||
|
payload.parent_hash = parent_hash;
|
||||||
|
changes.push(format!("parent_hash = {parent_hash}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(fee_recipient) = self.fee_recipient {
|
||||||
|
payload.fee_recipient = fee_recipient;
|
||||||
|
changes.push(format!("fee_recipient = {fee_recipient}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(state_root) = self.state_root {
|
||||||
|
payload.state_root = state_root;
|
||||||
|
changes.push(format!("state_root = {state_root}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(receipts_root) = self.receipts_root {
|
||||||
|
payload.receipts_root = receipts_root;
|
||||||
|
changes.push(format!("receipts_root = {receipts_root}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(logs_bloom) = self.logs_bloom {
|
||||||
|
payload.logs_bloom = logs_bloom;
|
||||||
|
changes.push("logs_bloom = <custom>".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prev_randao) = self.prev_randao {
|
||||||
|
payload.prev_randao = prev_randao;
|
||||||
|
changes.push(format!("prev_randao = {prev_randao}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(block_number) = self.block_number {
|
||||||
|
payload.block_number = block_number;
|
||||||
|
changes.push(format!("block_number = {block_number}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gas_limit) = self.gas_limit {
|
||||||
|
payload.gas_limit = gas_limit;
|
||||||
|
changes.push(format!("gas_limit = {gas_limit}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(gas_used) = self.gas_used {
|
||||||
|
payload.gas_used = gas_used;
|
||||||
|
changes.push(format!("gas_used = {gas_used}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(timestamp) = self.timestamp {
|
||||||
|
payload.timestamp = timestamp;
|
||||||
|
changes.push(format!("timestamp = {timestamp}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref extra_data) = self.extra_data {
|
||||||
|
payload.extra_data = extra_data.clone();
|
||||||
|
changes.push(format!("extra_data = {} bytes", extra_data.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(base_fee_per_gas) = self.base_fee_per_gas {
|
||||||
|
payload.base_fee_per_gas = U256::from_limbs([base_fee_per_gas, 0, 0, 0]);
|
||||||
|
changes.push(format!("base_fee_per_gas = {base_fee_per_gas}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(block_hash) = self.block_hash {
|
||||||
|
payload.block_hash = block_hash;
|
||||||
|
changes.push(format!("block_hash = {block_hash}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-invalidation flags
|
||||||
|
if self.invalidate_parent_hash {
|
||||||
|
let random_hash = B256::random();
|
||||||
|
payload.parent_hash = random_hash;
|
||||||
|
changes.push(format!("parent_hash = {random_hash} (auto-invalidated: random)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_state_root {
|
||||||
|
payload.state_root = B256::ZERO;
|
||||||
|
changes.push("state_root = ZERO (auto-invalidated: empty trie root)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_receipts_root {
|
||||||
|
payload.receipts_root = B256::ZERO;
|
||||||
|
changes.push("receipts_root = ZERO (auto-invalidated)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_gas_used {
|
||||||
|
let invalid_gas = payload.gas_limit + 1;
|
||||||
|
payload.gas_used = invalid_gas;
|
||||||
|
changes.push(format!("gas_used = {invalid_gas} (auto-invalidated: exceeds gas_limit)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_block_number {
|
||||||
|
let invalid_number = payload.block_number + 999;
|
||||||
|
payload.block_number = invalid_number;
|
||||||
|
changes.push(format!("block_number = {invalid_number} (auto-invalidated: huge gap)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_timestamp {
|
||||||
|
payload.timestamp = 0;
|
||||||
|
changes.push("timestamp = 0 (auto-invalidated: impossibly old)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_base_fee {
|
||||||
|
payload.base_fee_per_gas = U256::ZERO;
|
||||||
|
changes
|
||||||
|
.push("base_fee_per_gas = 0 (auto-invalidated: invalid post-London)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_transactions {
|
||||||
|
let invalid_tx = Bytes::from_static(&[0xff, 0xff, 0xff]);
|
||||||
|
payload.transactions.insert(0, invalid_tx);
|
||||||
|
changes.push("transactions = prepended invalid RLP (auto-invalidated)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_block_hash {
|
||||||
|
let random_hash = B256::random();
|
||||||
|
payload.block_hash = random_hash;
|
||||||
|
changes.push(format!("block_hash = {random_hash} (auto-invalidated: random)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies invalidations to a V2 payload, returns list of what was changed.
|
||||||
|
pub(super) fn apply_to_payload_v2(&self, payload: &mut ExecutionPayloadV2) -> Vec<String> {
|
||||||
|
let mut changes = self.apply_to_payload_v1(&mut payload.payload_inner);
|
||||||
|
|
||||||
|
// Handle withdrawals invalidation (V2+)
|
||||||
|
if self.invalidate_withdrawals {
|
||||||
|
let fake_withdrawal = Withdrawal {
|
||||||
|
index: u64::MAX,
|
||||||
|
validator_index: u64::MAX,
|
||||||
|
address: Address::ZERO,
|
||||||
|
amount: u64::MAX,
|
||||||
|
};
|
||||||
|
payload.withdrawals.push(fake_withdrawal);
|
||||||
|
changes.push("withdrawals = added fake withdrawal (auto-invalidated)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies invalidations to a V3 payload, returns list of what was changed.
|
||||||
|
pub(super) fn apply_to_payload_v3(&self, payload: &mut ExecutionPayloadV3) -> Vec<String> {
|
||||||
|
let mut changes = self.apply_to_payload_v2(&mut payload.payload_inner);
|
||||||
|
|
||||||
|
// Explicit overrides for V3 fields
|
||||||
|
if let Some(blob_gas_used) = self.blob_gas_used {
|
||||||
|
payload.blob_gas_used = blob_gas_used;
|
||||||
|
changes.push(format!("blob_gas_used = {blob_gas_used}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(excess_blob_gas) = self.excess_blob_gas {
|
||||||
|
payload.excess_blob_gas = excess_blob_gas;
|
||||||
|
changes.push(format!("excess_blob_gas = {excess_blob_gas}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-invalidation for V3 fields
|
||||||
|
if self.invalidate_blob_gas_used {
|
||||||
|
payload.blob_gas_used = u64::MAX;
|
||||||
|
changes.push("blob_gas_used = MAX (auto-invalidated)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.invalidate_excess_blob_gas {
|
||||||
|
payload.excess_blob_gas = u64::MAX;
|
||||||
|
changes.push("excess_blob_gas = MAX (auto-invalidated)".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
}
|
||||||
367
bin/reth-bench/src/bench/send_invalid_payload/mod.rs
Normal file
367
bin/reth-bench/src/bench/send_invalid_payload/mod.rs
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
//! Command for sending invalid payloads to test Engine API rejection.
|
||||||
|
|
||||||
|
mod invalidation;
|
||||||
|
use invalidation::InvalidationConfig;
|
||||||
|
|
||||||
|
use alloy_primitives::{Address, B256};
|
||||||
|
use alloy_provider::network::AnyRpcBlock;
|
||||||
|
use alloy_rpc_types_engine::ExecutionPayload;
|
||||||
|
use clap::Parser;
|
||||||
|
use eyre::{OptionExt, Result};
|
||||||
|
use op_alloy_consensus::OpTxEnvelope;
|
||||||
|
use reth_cli_runner::CliContext;
|
||||||
|
use std::io::{BufReader, Read, Write};
|
||||||
|
|
||||||
|
/// Command for generating and sending an invalid `engine_newPayload` request.
|
||||||
|
///
|
||||||
|
/// Takes a valid block and modifies fields to make it invalid for testing
|
||||||
|
/// Engine API rejection behavior. Block hash is recalculated after modifications
|
||||||
|
/// unless `--invalidate-block-hash` or `--skip-hash-recalc` is used.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct Command {
|
||||||
|
// ==================== Input Options ====================
|
||||||
|
/// Path to the JSON file containing the block. If not specified, stdin will be used.
|
||||||
|
#[arg(short, long, help_heading = "Input Options")]
|
||||||
|
path: Option<String>,
|
||||||
|
|
||||||
|
/// The engine RPC URL to use.
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help_heading = "Input Options",
|
||||||
|
required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
|
||||||
|
required_unless_present("mode")
|
||||||
|
)]
|
||||||
|
rpc_url: Option<String>,
|
||||||
|
|
||||||
|
/// The JWT secret to use. Can be either a path to a file containing the secret or the secret
|
||||||
|
/// itself.
|
||||||
|
#[arg(short, long, help_heading = "Input Options")]
|
||||||
|
jwt_secret: Option<String>,
|
||||||
|
|
||||||
|
/// The newPayload version to use (3 or 4).
|
||||||
|
#[arg(long, default_value_t = 3, help_heading = "Input Options")]
|
||||||
|
new_payload_version: u8,
|
||||||
|
|
||||||
|
/// The output mode to use.
|
||||||
|
#[arg(long, value_enum, default_value = "execute", help_heading = "Input Options")]
|
||||||
|
mode: Mode,
|
||||||
|
|
||||||
|
// ==================== Explicit Value Overrides ====================
|
||||||
|
/// Override the parent hash with a specific value.
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
parent_hash: Option<B256>,
|
||||||
|
|
||||||
|
/// Override the fee recipient (coinbase) with a specific address.
|
||||||
|
#[arg(long, value_name = "ADDR", help_heading = "Explicit Value Overrides")]
|
||||||
|
fee_recipient: Option<Address>,
|
||||||
|
|
||||||
|
/// Override the state root with a specific value.
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
state_root: Option<B256>,
|
||||||
|
|
||||||
|
/// Override the receipts root with a specific value.
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
receipts_root: Option<B256>,
|
||||||
|
|
||||||
|
/// Override the block number with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
block_number: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the gas limit with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
gas_limit: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the gas used with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
gas_used: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the timestamp with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
timestamp: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the base fee per gas with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
base_fee_per_gas: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the block hash with a specific value (skips hash recalculation).
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
block_hash: Option<B256>,
|
||||||
|
|
||||||
|
/// Override the blob gas used with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
blob_gas_used: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the excess blob gas with a specific value.
|
||||||
|
#[arg(long, value_name = "U64", help_heading = "Explicit Value Overrides")]
|
||||||
|
excess_blob_gas: Option<u64>,
|
||||||
|
|
||||||
|
/// Override the parent beacon block root with a specific value.
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
parent_beacon_block_root: Option<B256>,
|
||||||
|
|
||||||
|
/// Override the requests hash with a specific value (EIP-7685).
|
||||||
|
#[arg(long, value_name = "HASH", help_heading = "Explicit Value Overrides")]
|
||||||
|
requests_hash: Option<B256>,
|
||||||
|
|
||||||
|
// ==================== Auto-Invalidation Flags ====================
|
||||||
|
/// Invalidate the parent hash by setting it to a random value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_parent_hash: bool,
|
||||||
|
|
||||||
|
/// Invalidate the state root by setting it to a random value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_state_root: bool,
|
||||||
|
|
||||||
|
/// Invalidate the receipts root by setting it to a random value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_receipts_root: bool,
|
||||||
|
|
||||||
|
/// Invalidate the gas used by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_gas_used: bool,
|
||||||
|
|
||||||
|
/// Invalidate the block number by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_block_number: bool,
|
||||||
|
|
||||||
|
/// Invalidate the timestamp by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_timestamp: bool,
|
||||||
|
|
||||||
|
/// Invalidate the base fee by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_base_fee: bool,
|
||||||
|
|
||||||
|
/// Invalidate the transactions by modifying them.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_transactions: bool,
|
||||||
|
|
||||||
|
/// Invalidate the block hash by not recalculating it after modifications.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_block_hash: bool,
|
||||||
|
|
||||||
|
/// Invalidate the withdrawals by modifying them.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_withdrawals: bool,
|
||||||
|
|
||||||
|
/// Invalidate the blob gas used by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_blob_gas_used: bool,
|
||||||
|
|
||||||
|
/// Invalidate the excess blob gas by setting it to an incorrect value.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_excess_blob_gas: bool,
|
||||||
|
|
||||||
|
/// Invalidate the requests hash by setting it to a random value (EIP-7685).
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Auto-Invalidation Flags")]
|
||||||
|
invalidate_requests_hash: bool,
|
||||||
|
|
||||||
|
// ==================== Meta Flags ====================
|
||||||
|
/// Skip block hash recalculation after modifications.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Meta Flags")]
|
||||||
|
skip_hash_recalc: bool,
|
||||||
|
|
||||||
|
/// Print what would be done without actually sending the payload.
|
||||||
|
#[arg(long, default_value_t = false, help_heading = "Meta Flags")]
|
||||||
|
dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, clap::ValueEnum)]
|
||||||
|
enum Mode {
|
||||||
|
/// Execute the `cast` command. This works with blocks of any size, because it pipes the
|
||||||
|
/// payload into the `cast` command.
|
||||||
|
Execute,
|
||||||
|
/// Print the `cast` command. Caution: this may not work with large blocks because of the
|
||||||
|
/// command length limit.
|
||||||
|
Cast,
|
||||||
|
/// Print the JSON payload. Can be piped into `cast` command if the block is small enough.
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
/// Read input from either a file or stdin
|
||||||
|
fn read_input(&self) -> Result<String> {
|
||||||
|
Ok(match &self.path {
|
||||||
|
Some(path) => reth_fs_util::read_to_string(path)?,
|
||||||
|
None => String::from_utf8(
|
||||||
|
BufReader::new(std::io::stdin()).bytes().collect::<Result<Vec<_>, _>>()?,
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load JWT secret from either a file or use the provided string directly
|
||||||
|
fn load_jwt_secret(&self) -> Result<Option<String>> {
|
||||||
|
match &self.jwt_secret {
|
||||||
|
Some(secret) => match std::fs::read_to_string(secret) {
|
||||||
|
Ok(contents) => Ok(Some(contents.trim().to_string())),
|
||||||
|
Err(_) => Ok(Some(secret.clone())),
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build `InvalidationConfig` from command flags
|
||||||
|
const fn build_invalidation_config(&self) -> InvalidationConfig {
|
||||||
|
InvalidationConfig {
|
||||||
|
parent_hash: self.parent_hash,
|
||||||
|
fee_recipient: self.fee_recipient,
|
||||||
|
state_root: self.state_root,
|
||||||
|
receipts_root: self.receipts_root,
|
||||||
|
logs_bloom: None,
|
||||||
|
prev_randao: None,
|
||||||
|
block_number: self.block_number,
|
||||||
|
gas_limit: self.gas_limit,
|
||||||
|
gas_used: self.gas_used,
|
||||||
|
timestamp: self.timestamp,
|
||||||
|
extra_data: None,
|
||||||
|
base_fee_per_gas: self.base_fee_per_gas,
|
||||||
|
block_hash: self.block_hash,
|
||||||
|
blob_gas_used: self.blob_gas_used,
|
||||||
|
excess_blob_gas: self.excess_blob_gas,
|
||||||
|
invalidate_parent_hash: self.invalidate_parent_hash,
|
||||||
|
invalidate_state_root: self.invalidate_state_root,
|
||||||
|
invalidate_receipts_root: self.invalidate_receipts_root,
|
||||||
|
invalidate_gas_used: self.invalidate_gas_used,
|
||||||
|
invalidate_block_number: self.invalidate_block_number,
|
||||||
|
invalidate_timestamp: self.invalidate_timestamp,
|
||||||
|
invalidate_base_fee: self.invalidate_base_fee,
|
||||||
|
invalidate_transactions: self.invalidate_transactions,
|
||||||
|
invalidate_block_hash: self.invalidate_block_hash,
|
||||||
|
invalidate_withdrawals: self.invalidate_withdrawals,
|
||||||
|
invalidate_blob_gas_used: self.invalidate_blob_gas_used,
|
||||||
|
invalidate_excess_blob_gas: self.invalidate_excess_blob_gas,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the command
|
||||||
|
pub async fn execute(self, _ctx: CliContext) -> Result<()> {
|
||||||
|
let block_json = self.read_input()?;
|
||||||
|
let jwt_secret = self.load_jwt_secret()?;
|
||||||
|
|
||||||
|
let block = serde_json::from_str::<AnyRpcBlock>(&block_json)?
|
||||||
|
.into_inner()
|
||||||
|
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
|
||||||
|
.try_map_transactions(|tx| tx.try_into_either::<OpTxEnvelope>())?
|
||||||
|
.into_consensus();
|
||||||
|
|
||||||
|
let config = self.build_invalidation_config();
|
||||||
|
|
||||||
|
let parent_beacon_block_root =
|
||||||
|
self.parent_beacon_block_root.or(block.header.parent_beacon_block_root);
|
||||||
|
let blob_versioned_hashes =
|
||||||
|
block.body.blob_versioned_hashes_iter().copied().collect::<Vec<_>>();
|
||||||
|
let use_v4 = block.header.requests_hash.is_some();
|
||||||
|
let requests_hash = self.requests_hash.or(block.header.requests_hash);
|
||||||
|
|
||||||
|
let mut execution_payload = ExecutionPayload::from_block_slow(&block).0;
|
||||||
|
|
||||||
|
let changes = match &mut execution_payload {
|
||||||
|
ExecutionPayload::V1(p) => config.apply_to_payload_v1(p),
|
||||||
|
ExecutionPayload::V2(p) => config.apply_to_payload_v2(p),
|
||||||
|
ExecutionPayload::V3(p) => config.apply_to_payload_v3(p),
|
||||||
|
};
|
||||||
|
|
||||||
|
let skip_recalc = self.skip_hash_recalc || config.should_skip_hash_recalc();
|
||||||
|
if !skip_recalc {
|
||||||
|
let new_hash = match execution_payload.clone().into_block_raw() {
|
||||||
|
Ok(block) => block.header.hash_slow(),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: Could not recalculate block hash: {e}. Using original hash."
|
||||||
|
);
|
||||||
|
match &execution_payload {
|
||||||
|
ExecutionPayload::V1(p) => p.block_hash,
|
||||||
|
ExecutionPayload::V2(p) => p.payload_inner.block_hash,
|
||||||
|
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut execution_payload {
|
||||||
|
ExecutionPayload::V1(p) => p.block_hash = new_hash,
|
||||||
|
ExecutionPayload::V2(p) => p.payload_inner.block_hash = new_hash,
|
||||||
|
ExecutionPayload::V3(p) => p.payload_inner.payload_inner.block_hash = new_hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dry_run {
|
||||||
|
println!("=== Dry Run ===");
|
||||||
|
println!("Changes that would be applied:");
|
||||||
|
for change in &changes {
|
||||||
|
println!(" - {}", change);
|
||||||
|
}
|
||||||
|
if changes.is_empty() {
|
||||||
|
println!(" (no changes)");
|
||||||
|
}
|
||||||
|
if skip_recalc {
|
||||||
|
println!(" - Block hash recalculation: SKIPPED");
|
||||||
|
} else {
|
||||||
|
println!(" - Block hash recalculation: PERFORMED");
|
||||||
|
}
|
||||||
|
println!("\nResulting payload JSON:");
|
||||||
|
let json = serde_json::to_string_pretty(&execution_payload)?;
|
||||||
|
println!("{}", json);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_request = if use_v4 {
|
||||||
|
serde_json::to_string(&(
|
||||||
|
execution_payload,
|
||||||
|
blob_versioned_hashes,
|
||||||
|
parent_beacon_block_root,
|
||||||
|
requests_hash.unwrap_or_default(),
|
||||||
|
))?
|
||||||
|
} else {
|
||||||
|
serde_json::to_string(&(
|
||||||
|
execution_payload,
|
||||||
|
blob_versioned_hashes,
|
||||||
|
parent_beacon_block_root,
|
||||||
|
))?
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.mode {
|
||||||
|
Mode::Execute => {
|
||||||
|
let mut command = std::process::Command::new("cast");
|
||||||
|
let method = if use_v4 { "engine_newPayloadV4" } else { "engine_newPayloadV3" };
|
||||||
|
command.arg("rpc").arg(method).arg("--raw");
|
||||||
|
if let Some(rpc_url) = self.rpc_url {
|
||||||
|
command.arg("--rpc-url").arg(rpc_url);
|
||||||
|
}
|
||||||
|
if let Some(secret) = &jwt_secret {
|
||||||
|
command.arg("--jwt-secret").arg(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
|
||||||
|
|
||||||
|
process
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.ok_or_eyre("stdin not available")?
|
||||||
|
.write_all(json_request.as_bytes())?;
|
||||||
|
|
||||||
|
process.wait()?;
|
||||||
|
}
|
||||||
|
Mode::Cast => {
|
||||||
|
let mut cmd = format!(
|
||||||
|
"cast rpc engine_newPayloadV{} --raw '{}'",
|
||||||
|
self.new_payload_version, json_request
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(rpc_url) = self.rpc_url {
|
||||||
|
cmd += &format!(" --rpc-url {rpc_url}");
|
||||||
|
}
|
||||||
|
if let Some(secret) = &jwt_secret {
|
||||||
|
cmd += &format!(" --jwt-secret {secret}");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{cmd}");
|
||||||
|
}
|
||||||
|
Mode::Json => {
|
||||||
|
println!("{json_request}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user