Skip to content

Commit

Permalink
feat(reth-bench): send-payload CLI (#14472)
Browse files Browse the repository at this point in the history
  • Loading branch information
shekhirin authored Feb 13, 2025
1 parent 08011a8 commit 431df62
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 13 deletions.
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 19 additions & 13 deletions bin/reth-bench/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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-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-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-eips.workspace = true
alloy-primitives.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"] }
Expand All @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions bin/reth-bench/src/bench/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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 {
Expand All @@ -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,
}
}

Expand Down
238 changes: 238 additions & 0 deletions bin/reth-bench/src/bench/send_payload.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}

0 comments on commit 431df62

Please sign in to comment.