mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(reth-bench): send-payload CLI (#14472)
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -2510,6 +2510,17 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "delegate"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297806318ef30ad066b15792a8372858020ae3ca2e414ee6c2133b1eb9e9e945"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -6638,12 +6649,14 @@ dependencies = [
|
||||
name = "reth-bench"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"alloy-consensus",
|
||||
"alloy-eips",
|
||||
"alloy-json-rpc",
|
||||
"alloy-primitives",
|
||||
"alloy-provider",
|
||||
"alloy-pubsub",
|
||||
"alloy-rpc-client",
|
||||
"alloy-rpc-types",
|
||||
"alloy-rpc-types-engine",
|
||||
"alloy-transport",
|
||||
"alloy-transport-http",
|
||||
@ -6652,17 +6665,21 @@ dependencies = [
|
||||
"async-trait",
|
||||
"clap",
|
||||
"csv",
|
||||
"delegate",
|
||||
"eyre",
|
||||
"futures",
|
||||
"op-alloy-rpc-types",
|
||||
"reqwest",
|
||||
"reth-cli-runner",
|
||||
"reth-cli-util",
|
||||
"reth-fs-util",
|
||||
"reth-node-api",
|
||||
"reth-node-core",
|
||||
"reth-primitives",
|
||||
"reth-primitives-traits",
|
||||
"reth-tracing",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
|
||||
@ -16,24 +16,28 @@ workspace = true
|
||||
# reth
|
||||
reth-cli-runner.workspace = true
|
||||
reth-cli-util.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-fs-util.workspace = true
|
||||
reth-node-api.workspace = true
|
||||
reth-node-core.workspace = true
|
||||
reth-primitives = { workspace = true, features = ["alloy-compat"] }
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-tracing.workspace = true
|
||||
|
||||
# alloy
|
||||
alloy-provider = { workspace = true, features = ["engine-api", "reqwest-rustls-tls"], default-features = false }
|
||||
alloy-rpc-types-engine.workspace = true
|
||||
alloy-transport.workspace = true
|
||||
alloy-transport-http.workspace = true
|
||||
alloy-transport-ws.workspace = true
|
||||
alloy-transport-ipc.workspace = true
|
||||
alloy-pubsub.workspace = true
|
||||
alloy-json-rpc.workspace = true
|
||||
alloy-rpc-client.workspace = true
|
||||
alloy-consensus = { workspace = true }
|
||||
alloy-eips.workspace = true
|
||||
alloy-json-rpc.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
alloy-provider = { workspace = true, features = ["engine-api", "reqwest-rustls-tls"], default-features = false }
|
||||
alloy-pubsub.workspace = true
|
||||
alloy-rpc-client.workspace = true
|
||||
alloy-rpc-types = { workspace = true }
|
||||
alloy-rpc-types-engine = { workspace = true }
|
||||
alloy-transport-http.workspace = true
|
||||
alloy-transport-ipc.workspace = true
|
||||
alloy-transport-ws.workspace = true
|
||||
alloy-transport.workspace = true
|
||||
op-alloy-rpc-types.workspace = true
|
||||
|
||||
# reqwest
|
||||
reqwest = { workspace = true, default-features = false, features = ["rustls-tls-native-roots"] }
|
||||
@ -44,18 +48,20 @@ tower.workspace = true
|
||||
# tracing
|
||||
tracing.workspace = true
|
||||
|
||||
# io
|
||||
# serde
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
# async
|
||||
tokio = { workspace = true, features = ["sync", "macros", "time", "rt-multi-thread"] }
|
||||
futures.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
tokio = { workspace = true, features = ["sync", "macros", "time", "rt-multi-thread"] }
|
||||
|
||||
# misc
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
delegate = "0.13"
|
||||
eyre.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
|
||||
# for writing data
|
||||
csv = "1.3.0"
|
||||
|
||||
@ -9,6 +9,7 @@ mod context;
|
||||
mod new_payload_fcu;
|
||||
mod new_payload_only;
|
||||
mod output;
|
||||
mod send_payload;
|
||||
|
||||
/// `reth bench` command
|
||||
#[derive(Debug, Parser)]
|
||||
@ -28,6 +29,18 @@ pub enum Subcommands {
|
||||
|
||||
/// Benchmark which only calls subsequent `newPayload` calls.
|
||||
NewPayloadOnly(new_payload_only::Command),
|
||||
|
||||
/// Command for generating and sending an `engine_newPayload` request constructed from an RPC
|
||||
/// block.
|
||||
///
|
||||
/// This command takes a JSON block input (either from a file or stdin) and generates
|
||||
/// an execution payload that can be used with the `engine_newPayloadV*` API.
|
||||
///
|
||||
/// One powerful use case is pairing this command with the `cast block` command, for example:
|
||||
///
|
||||
/// `cast block latest--full --json | reth-bench send-payload --rpc-url localhost:5000
|
||||
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
|
||||
SendPayload(send_payload::Command),
|
||||
}
|
||||
|
||||
impl BenchmarkCommand {
|
||||
@ -39,6 +52,7 @@ impl BenchmarkCommand {
|
||||
match self.command {
|
||||
Subcommands::NewPayloadFcu(command) => command.execute(ctx).await,
|
||||
Subcommands::NewPayloadOnly(command) => command.execute(ctx).await,
|
||||
Subcommands::SendPayload(command) => command.execute(ctx).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
238
bin/reth-bench/src/bench/send_payload.rs
Normal file
238
bin/reth-bench/src/bench/send_payload.rs
Normal file
@ -0,0 +1,238 @@
|
||||
use alloy_consensus::{
|
||||
Block as PrimitiveBlock, BlockBody, Header as PrimitiveHeader,
|
||||
Transaction as PrimitiveTransaction,
|
||||
};
|
||||
use alloy_eips::{eip7702::SignedAuthorization, Encodable2718, Typed2718};
|
||||
use alloy_primitives::{bytes::BufMut, Bytes, ChainId, TxKind, B256, U256};
|
||||
use alloy_rpc_types::{
|
||||
AccessList, Block as RpcBlock, BlockTransactions, Transaction as EthRpcTransaction,
|
||||
};
|
||||
use alloy_rpc_types_engine::ExecutionPayload;
|
||||
use clap::Parser;
|
||||
use delegate::delegate;
|
||||
use eyre::{OptionExt, Result};
|
||||
use op_alloy_rpc_types::Transaction as OpRpcTransaction;
|
||||
use reth_cli_runner::CliContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
/// Command for generating and sending an `engine_newPayload` request constructed from an RPC
|
||||
/// block.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Command {
|
||||
/// Path to the json file to parse. If not specified, stdin will be used.
|
||||
#[arg(short, long)]
|
||||
path: Option<String>,
|
||||
|
||||
/// The engine RPC url to use.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
// Required if `mode` is `execute` or `cast`.
|
||||
required_if_eq_any([("mode", "execute"), ("mode", "cast")]),
|
||||
// If `mode` is not specified, then `execute` is used, so we need to require it.
|
||||
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)]
|
||||
jwt_secret: Option<String>,
|
||||
|
||||
#[arg(long, default_value_t = 3)]
|
||||
new_payload_version: u8,
|
||||
|
||||
/// The mode to use.
|
||||
#[arg(long, value_enum, default_value = "execute")]
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RpcTransaction {
|
||||
Ethereum(EthRpcTransaction),
|
||||
Optimism(OpRpcTransaction),
|
||||
}
|
||||
|
||||
impl Typed2718 for RpcTransaction {
|
||||
delegate! {
|
||||
to match self {
|
||||
Self::Ethereum(tx) => tx,
|
||||
Self::Optimism(tx) => tx,
|
||||
} {
|
||||
fn ty(&self) -> u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveTransaction for RpcTransaction {
|
||||
delegate! {
|
||||
to match self {
|
||||
Self::Ethereum(tx) => tx,
|
||||
Self::Optimism(tx) => tx,
|
||||
} {
|
||||
fn chain_id(&self) -> Option<ChainId>;
|
||||
fn nonce(&self) -> u64;
|
||||
fn gas_limit(&self) -> u64;
|
||||
fn gas_price(&self) -> Option<u128>;
|
||||
fn max_fee_per_gas(&self) -> u128;
|
||||
fn max_priority_fee_per_gas(&self) -> Option<u128>;
|
||||
fn max_fee_per_blob_gas(&self) -> Option<u128>;
|
||||
fn priority_fee_or_price(&self) -> u128;
|
||||
fn effective_gas_price(&self, base_fee: Option<u64>) -> u128;
|
||||
fn is_dynamic_fee(&self) -> bool;
|
||||
fn kind(&self) -> TxKind;
|
||||
fn is_create(&self) -> bool;
|
||||
fn value(&self) -> U256;
|
||||
fn input(&self) -> &Bytes;
|
||||
fn access_list(&self) -> Option<&AccessList>;
|
||||
fn blob_versioned_hashes(&self) -> Option<&[B256]>;
|
||||
fn authorization_list(&self) -> Option<&[SignedAuthorization]>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable2718 for RpcTransaction {
|
||||
delegate! {
|
||||
to match self {
|
||||
Self::Ethereum(tx) => tx.inner,
|
||||
Self::Optimism(tx) => tx.inner.inner,
|
||||
} {
|
||||
fn encode_2718_len(&self) -> usize;
|
||||
fn encode_2718(&self, out: &mut dyn BufMut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(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) => {
|
||||
// Try to read as file first
|
||||
match std::fs::read_to_string(secret) {
|
||||
Ok(contents) => Ok(Some(contents.trim().to_string())),
|
||||
// If file read fails, use the string directly
|
||||
Err(_) => Ok(Some(secret.clone())),
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the generate payload command
|
||||
pub async fn execute(self, _ctx: CliContext) -> Result<()> {
|
||||
// Load block
|
||||
let block_json = self.read_input()?;
|
||||
|
||||
// Load JWT secret
|
||||
let jwt_secret = self.load_jwt_secret()?;
|
||||
|
||||
// Parse the block
|
||||
let block: RpcBlock<RpcTransaction> = serde_json::from_str(&block_json)?;
|
||||
|
||||
// Extract parent beacon block root
|
||||
let parent_beacon_block_root = block.header.parent_beacon_block_root;
|
||||
|
||||
// Extract transactions
|
||||
let transactions = match block.transactions {
|
||||
BlockTransactions::Hashes(_) => {
|
||||
return Err(eyre::eyre!("Block must include full transaction data. Send the eth_getBlockByHash request with full: `true`"));
|
||||
}
|
||||
BlockTransactions::Full(txs) => txs,
|
||||
BlockTransactions::Uncle => {
|
||||
return Err(eyre::eyre!("Cannot process uncle blocks"));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract blob versioned hashes
|
||||
let blob_versioned_hashes = transactions
|
||||
.iter()
|
||||
.filter_map(|tx| tx.blob_versioned_hashes().map(|v| v.to_vec()))
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Convert to execution payload
|
||||
let execution_payload = ExecutionPayload::from_block_slow(&PrimitiveBlock::new(
|
||||
PrimitiveHeader::from(block.header),
|
||||
BlockBody { transactions, ommers: vec![], withdrawals: block.withdrawals },
|
||||
))
|
||||
.0;
|
||||
|
||||
// Create JSON request data
|
||||
let json_request = serde_json::to_string(&(
|
||||
execution_payload,
|
||||
blob_versioned_hashes,
|
||||
parent_beacon_block_root,
|
||||
))?;
|
||||
|
||||
// Print output or execute command
|
||||
match self.mode {
|
||||
Mode::Execute => {
|
||||
// Create cast command
|
||||
let mut command = std::process::Command::new("cast");
|
||||
command.arg("rpc").arg("engine_newPayloadV3").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);
|
||||
}
|
||||
|
||||
// Start cast process
|
||||
let mut process = command.stdin(std::process::Stdio::piped()).spawn()?;
|
||||
|
||||
// Write to cast's stdin
|
||||
process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_eyre("stdin not available")?
|
||||
.write_all(json_request.as_bytes())?;
|
||||
|
||||
// Wait for cast to finish
|
||||
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