feat: integrate builder (#6611)

This commit is contained in:
Matthias Seitz
2024-02-29 17:50:04 +01:00
committed by GitHub
parent 7d36206dfe
commit c5955f1305
73 changed files with 2201 additions and 3022 deletions

View File

@ -8,6 +8,7 @@ license.workspace = true
[dependencies]
reth.workspace = true
reth-transaction-pool.workspace = true
reth-node-ethereum.workspace = true
clap = { workspace = true, features = ["derive"] }
jsonrpsee = { workspace = true, features = ["server", "macros"] }

View File

@ -14,24 +14,38 @@
use clap::Parser;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use reth::cli::{
components::{RethNodeComponents, RethRpcComponents},
config::RethRpcConfig,
ext::{RethCliExt, RethNodeCommandConfig},
Cli,
};
use reth::cli::Cli;
use reth_node_ethereum::EthereumNode;
use reth_transaction_pool::TransactionPool;
fn main() {
Cli::<MyRethCliExt>::parse().run().unwrap();
}
Cli::<RethCliTxpoolExt>::parse()
.run(|builder, args| async move {
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
if !args.enable_ext {
return Ok(())
}
/// The type that tells the reth CLI what extensions to use
struct MyRethCliExt;
// here we get the configured pool.
let pool = ctx.pool().clone();
impl RethCliExt for MyRethCliExt {
/// This tells the reth CLI to install the `txpool` rpc namespace via `RethCliTxpoolExt`
type Node = RethCliTxpoolExt;
let ext = TxpoolExt { pool };
// now we merge our extension namespace into all configured transports
ctx.modules.merge_configured(ext.into_rpc())?;
println!("txpool extension enabled");
Ok(())
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
@ -42,34 +56,6 @@ struct RethCliTxpoolExt {
pub enable_ext: bool,
}
impl RethNodeCommandConfig for RethCliTxpoolExt {
// This is the entrypoint for the CLI to extend the RPC server with custom rpc namespaces.
fn extend_rpc_modules<Conf, Reth>(
&mut self,
_config: &Conf,
_components: &Reth,
rpc_components: RethRpcComponents<'_, Reth>,
) -> eyre::Result<()>
where
Conf: RethRpcConfig,
Reth: RethNodeComponents,
{
if !self.enable_ext {
return Ok(())
}
// here we get the configured pool type from the CLI.
let pool = rpc_components.registry.pool().clone();
let ext = TxpoolExt { pool };
// now we merge our extension namespace into all configured transports
rpc_components.modules.merge_configured(ext.into_rpc())?;
println!("txpool extension enabled");
Ok(())
}
}
/// trait interface for a custom rpc namespace: `txpool`
///
/// This defines an additional namespace where all methods are configured as trait functions.

View File

@ -7,10 +7,9 @@ license.workspace = true
[dependencies]
reth.workspace = true
eyre.workspace = true
reth-node-ethereum.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
futures-util.workspace = true
tokio = { workspace = true, features = ["time"] }

View File

@ -15,33 +15,26 @@
//!
//! See lighthouse beacon Node API: <https://lighthouse-book.sigmaprime.io/api-bn.html#beacon-node-api>
#![warn(unused_crate_dependencies)]
use clap::Parser;
use futures_util::stream::StreamExt;
use mev_share_sse::{client::EventStream, EventClient};
use reth::{
cli::{
components::RethNodeComponents,
ext::{RethCliExt, RethNodeCommandConfig},
Cli,
},
rpc::types::beacon::events::PayloadAttributesEvent,
tasks::TaskSpawner,
};
use reth::{cli::Cli, rpc::types::beacon::events::PayloadAttributesEvent};
use reth_node_ethereum::EthereumNode;
use std::net::{IpAddr, Ipv4Addr};
use tracing::{info, warn};
fn main() {
Cli::<BeaconEventsExt>::parse().run().unwrap();
}
Cli::<BeaconEventsConfig>::parse()
.run(|builder, args| async move {
let handle = builder.node(EthereumNode::default()).launch().await?;
/// The type that tells the reth CLI what extensions to use
#[derive(Debug, Default)]
#[non_exhaustive]
struct BeaconEventsExt;
handle.node.task_executor.spawn(Box::pin(args.run()));
impl RethCliExt for BeaconEventsExt {
/// This tells the reth CLI to install additional CLI arguments
type Node = BeaconEventsConfig;
handle.wait_for_node_exit().await
})
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
@ -95,13 +88,6 @@ impl BeaconEventsConfig {
}
}
impl RethNodeCommandConfig for BeaconEventsConfig {
fn on_node_started<Reth: RethNodeComponents>(&mut self, components: &Reth) -> eyre::Result<()> {
components.task_executor().spawn(Box::pin(self.clone().run()));
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -7,5 +7,4 @@ license.workspace = true
[dependencies]
reth.workspace = true
clap.workspace = true
eyre.workspace = true
reth-node-ethereum.workspace = true

View File

@ -15,38 +15,30 @@
//! > "Node started"
//! once the node has been started.
use clap::Parser;
use reth::cli::{
components::RethNodeComponents,
ext::{NoArgsCliExt, RethNodeCommandConfig},
Cli,
};
use reth::cli::Cli;
use reth_node_ethereum::EthereumNode;
fn main() {
Cli::<NoArgsCliExt<MyRethConfig>>::parse()
.with_node_extension(MyRethConfig::default())
.run()
Cli::parse_args()
.run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.on_node_started(|_ctx| {
println!("Node started");
Ok(())
})
.on_rpc_started(|_ctx, _handles| {
println!("RPC started");
Ok(())
})
.on_component_initialized(|_ctx| {
println!("All components initialized");
Ok(())
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
struct MyRethConfig;
impl RethNodeCommandConfig for MyRethConfig {
fn on_components_initialized<Reth: RethNodeComponents>(
&mut self,
_components: &Reth,
) -> eyre::Result<()> {
println!("All components initialized");
Ok(())
}
fn on_node_started<Reth: RethNodeComponents>(
&mut self,
_components: &Reth,
) -> eyre::Result<()> {
println!("Node started");
Ok(())
}
}

View File

@ -0,0 +1,18 @@
[package]
name = "custom-dev-node"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
[dependencies]
reth.workspace = true
reth-node-core.workspace = true
reth-primitives.workspace = true
reth-node-ethereum.workspace = true
futures-util.workspace = true
eyre.workspace = true
tokio.workspace = true
serde_json.workspace = true

View File

@ -0,0 +1,96 @@
//! This example shows how to run a custom dev node programmatically and submit a transaction
//! through rpc.
#![warn(unused_crate_dependencies)]
use futures_util::StreamExt;
use reth::{
builder::{NodeBuilder, NodeHandle},
providers::CanonStateSubscriptions,
rpc::eth::EthTransactions,
tasks::TaskManager,
};
use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig};
use reth_node_ethereum::EthereumNode;
use reth_primitives::{b256, hex, ChainSpec, Genesis};
use std::sync::Arc;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let tasks = TaskManager::current();
// create node config
let node_config = NodeConfig::test()
.dev()
.with_rpc(RpcServerArgs::default().with_http())
.with_chain(custom_chain());
let NodeHandle { mut node, node_exit_future } = NodeBuilder::new(node_config)
.testing_node(tasks.executor())
.node(EthereumNode::default())
.launch()
.await?;
let mut notifications = node.provider.canonical_state_stream();
// submit tx through rpc
let raw_tx = hex!("02f876820a28808477359400847735940082520894ab0840c0e43688012c1adb0f5e3fc665188f83d28a029d394a5d630544000080c080a0a044076b7e67b5deecc63f61a8d7913fab86ca365b344b5759d1fe3563b4c39ea019eab979dd000da04dfc72bb0377c092d30fd9e1cab5ae487de49586cc8b0090");
let eth_api = node.rpc_registry.eth_api();
let hash = eth_api.send_raw_transaction(raw_tx.into()).await?;
let expected = b256!("b1c6512f4fc202c04355fbda66755e0e344b152e633010e8fd75ecec09b63398");
assert_eq!(hash, expected);
println!("submitted transaction: {hash}");
let head = notifications.next().await.unwrap();
let tx = head.tip().transactions().next().unwrap();
assert_eq!(tx.hash(), hash);
println!("mined transaction: {hash}");
node_exit_future.await
}
fn custom_chain() -> Arc<ChainSpec> {
let custom_genesis = r#"
{
"nonce": "0x42",
"timestamp": "0x0",
"extraData": "0x5343",
"gasLimit": "0x1388",
"difficulty": "0x400000000",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"0x6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b": {
"balance": "0x4a47e3c12448f4ad000000"
}
},
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"config": {
"ethash": {},
"chainId": 2600,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"terminalTotalDifficulty": 0,
"terminalTotalDifficultyPassed": true,
"shanghaiTime": 0
}
}
"#;
let genesis: Genesis = serde_json::from_str(custom_genesis).unwrap();
Arc::new(genesis.into())
}

View File

@ -0,0 +1,18 @@
[package]
name = "custom-evm"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
[dependencies]
reth.workspace = true
reth-node-api.workspace = true
reth-node-core.workspace = true
reth-primitives.workspace = true
reth-node-ethereum.workspace = true
reth-tracing.workspace = true
alloy-chains.workspace = true
eyre.workspace = true
tokio.workspace = true

View File

@ -0,0 +1,147 @@
//! This example shows how to implement a node with a custom EVM
#![warn(unused_crate_dependencies)]
use alloy_chains::Chain;
use reth::{
builder::{node::NodeTypes, NodeBuilder},
primitives::{
address,
revm_primitives::{CfgEnvWithHandlerCfg, Env, PrecompileResult, TxEnv},
Address, Bytes, U256,
},
revm::{
handler::register::EvmHandler,
precompile::{Precompile, PrecompileSpecId, Precompiles},
Database, Evm, EvmBuilder,
},
tasks::TaskManager,
};
use reth_node_api::{ConfigureEvm, ConfigureEvmEnv};
use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig};
use reth_node_ethereum::{EthEngineTypes, EthEvmConfig, EthereumNode};
use reth_primitives::{ChainSpec, Genesis, Header, Transaction};
use reth_tracing::{RethTracer, Tracer};
use std::sync::Arc;
/// Custom EVM configuration
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct MyEvmConfig;
impl MyEvmConfig {
/// Sets the precompiles to the EVM handler
///
/// This will be invoked when the EVM is created via [ConfigureEvm::evm] or
/// [ConfigureEvm::evm_with_inspector]
///
/// This will use the default mainnet precompiles and add additional precompiles.
pub fn set_precompiles<EXT, DB>(handler: &mut EvmHandler<EXT, DB>)
where
DB: Database,
{
// first we need the evm spec id, which determines the precompiles
let spec_id = handler.cfg.spec_id;
// install the precompiles
handler.pre_execution.load_precompiles = Arc::new(move || {
let mut precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec_id)).clone();
precompiles.inner.insert(
address!("0000000000000000000000000000000000000999"),
Precompile::Env(Self::my_precompile),
);
precompiles
});
}
/// A custom precompile that does nothing
fn my_precompile(_data: &Bytes, _gas: u64, _env: &Env) -> PrecompileResult {
Ok((0, Bytes::new()))
}
}
impl ConfigureEvmEnv for MyEvmConfig {
type TxMeta = ();
fn fill_tx_env<T>(tx_env: &mut TxEnv, transaction: T, sender: Address, meta: Self::TxMeta)
where
T: AsRef<Transaction>,
{
EthEvmConfig::fill_tx_env(tx_env, transaction, sender, meta)
}
fn fill_cfg_env(
cfg_env: &mut CfgEnvWithHandlerCfg,
chain_spec: &ChainSpec,
header: &Header,
total_difficulty: U256,
) {
EthEvmConfig::fill_cfg_env(cfg_env, chain_spec, header, total_difficulty)
}
}
impl ConfigureEvm for MyEvmConfig {
fn evm<'a, DB: Database + 'a>(&self, db: DB) -> Evm<'a, (), DB> {
EvmBuilder::default()
.with_db(db)
// add additional precompiles
.append_handler_register(MyEvmConfig::set_precompiles)
.build()
}
fn evm_with_inspector<'a, DB: Database + 'a, I>(&self, db: DB, inspector: I) -> Evm<'a, I, DB> {
EvmBuilder::default()
.with_db(db)
.with_external_context(inspector)
// add additional precompiles
.append_handler_register(MyEvmConfig::set_precompiles)
.build()
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
struct MyCustomNode;
/// Configure the node types
impl NodeTypes for MyCustomNode {
type Primitives = ();
type Engine = EthEngineTypes;
type Evm = MyEvmConfig;
fn evm_config(&self) -> Self::Evm {
Self::Evm::default()
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let _guard = RethTracer::new().init()?;
let tasks = TaskManager::current();
// create optimism genesis with canyon at block 2
let spec = ChainSpec::builder()
.chain(Chain::mainnet())
.genesis(Genesis::default())
.london_activated()
.paris_activated()
.shanghai_activated()
.cancun_activated()
.build();
let node_config =
NodeConfig::test().with_rpc(RpcServerArgs::default().with_http()).with_chain(spec);
let handle = NodeBuilder::new(node_config)
.testing_node(tasks.executor())
.with_types(MyCustomNode::default())
.with_components(EthereumNode::components())
.launch()
.await
.unwrap();
println!("Node started");
handle.node_exit_future.await
}

View File

@ -7,6 +7,6 @@ license.workspace = true
[dependencies]
reth.workspace = true
reth-node-ethereum.workspace = true
clap = { workspace = true, features = ["derive"] }
futures-util.workspace = true
eyre.workspace = true
futures-util.workspace = true

View File

@ -11,15 +11,11 @@
#![warn(unused_crate_dependencies)]
use clap::Parser;
use futures_util::stream::StreamExt;
use futures_util::StreamExt;
use reth::{
cli::{
components::{RethNodeComponents, RethRpcComponents, RethRpcServerHandles},
config::RethRpcConfig,
ext::{RethCliExt, RethNodeCommandConfig},
Cli,
},
primitives::{Address, BlockId, IntoRecoveredTransaction},
builder::NodeHandle,
cli::Cli,
primitives::{Address, BlockNumberOrTag, IntoRecoveredTransaction},
revm::{
inspector_handle_register,
interpreter::{Interpreter, OpCode},
@ -29,21 +25,79 @@ use reth::{
compat::transaction::transaction_to_call_request,
eth::{revm_utils::EvmOverrides, EthTransactions},
},
tasks::TaskSpawner,
transaction_pool::TransactionPool,
};
use reth_node_ethereum::node::EthereumNode;
use std::collections::HashSet;
fn main() {
Cli::<MyRethCliExt>::parse().run().unwrap();
}
Cli::<RethCliTxpoolExt>::parse()
.run(|builder, args| async move {
// launch the node
let NodeHandle { mut node, node_exit_future } =
builder.node(EthereumNode::default()).launch().await?;
/// The type that tells the reth CLI what extensions to use
struct MyRethCliExt;
let recipients = args.recipients.iter().copied().collect::<HashSet<_>>();
impl RethCliExt for MyRethCliExt {
/// This tells the reth CLI to trace addresses via `RethCliTxpoolExt`
type Node = RethCliTxpoolExt;
// create a new subscription to pending transactions
let mut pending_transactions = node.pool.new_pending_pool_transactions_listener();
// get an instance of the `trace_` API handler
let eth_api = node.rpc_registry.eth_api();
println!("Spawning trace task!");
// Spawn an async block to listen for transactions.
node.task_executor.spawn(Box::pin(async move {
// Waiting for new transactions
while let Some(event) = pending_transactions.next().await {
let tx = event.transaction;
println!("Transaction received: {tx:?}");
if recipients.is_empty() {
// convert the pool transaction
let call_request =
transaction_to_call_request(tx.to_recovered_transaction());
let result = eth_api
.spawn_with_call_at(
call_request,
BlockNumberOrTag::Latest.into(),
EvmOverrides::default(),
move |db, env| {
let mut dummy_inspector = DummyInspector::default();
{
// configure the evm with the custom inspector
let mut evm = Evm::builder()
.with_db(db)
.with_external_context(&mut dummy_inspector)
.with_env_with_handler_cfg(env)
.append_handler_register(inspector_handle_register)
.build();
// execute the transaction on a blocking task and await the
// inspector result
let _ = evm.transact()?;
}
Ok(dummy_inspector)
},
)
.await;
if let Ok(ret_val) = result {
let hash = tx.hash();
println!(
"Inspector result for transaction {}: \n {}",
hash,
ret_val.ret_val.join("\n")
);
}
}
}
}));
node_exit_future.await
})
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
@ -74,76 +128,3 @@ where
}
}
}
impl RethNodeCommandConfig for RethCliTxpoolExt {
/// Sets up a subscription to listen for new pending transactions and traces them.
/// If the transaction is from one of the specified recipients, it will be traced.
/// If no recipients are specified, all transactions will be traced.
fn on_rpc_server_started<Conf, Reth>(
&mut self,
_config: &Conf,
components: &Reth,
rpc_components: RethRpcComponents<'_, Reth>,
_handles: RethRpcServerHandles,
) -> eyre::Result<()>
where
Conf: RethRpcConfig,
Reth: RethNodeComponents,
{
let recipients = self.recipients.iter().copied().collect::<HashSet<_>>();
// create a new subscription to pending transactions
let mut pending_transactions = components.pool().new_pending_pool_transactions_listener();
let eth_api = rpc_components.registry.eth_api();
println!("Spawning trace task!");
// Spawn an async block to listen for transactions.
components.task_executor().spawn(Box::pin(async move {
// Waiting for new transactions
while let Some(event) = pending_transactions.next().await {
let tx = event.transaction;
println!("Transaction received: {tx:?}");
if recipients.is_empty() {
// convert the pool transaction
let call_request = transaction_to_call_request(tx.to_recovered_transaction());
let result = eth_api
.spawn_with_call_at(
call_request,
BlockId::default(),
EvmOverrides::default(),
move |db, env| {
let mut dummy_inspector = DummyInspector::default();
{
// configure the evm with the custom inspector
let mut evm = Evm::builder()
.with_db(db)
.with_external_context(&mut dummy_inspector)
.with_env_with_handler_cfg(env)
.append_handler_register(inspector_handle_register)
.build();
// execute the transaction on a blocking task and await the
// inspector result
let _ = evm.transact()?;
}
Ok(dummy_inspector)
},
)
.await;
if let Ok(ret_val) = result {
let hash = tx.hash();
println!(
"Inspector result for transaction {}: \n {}",
hash,
ret_val.ret_val.join("\n")
);
}
}
}
}));
Ok(())
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "custom-node-components"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true
[dependencies]
reth.workspace = true
reth-node-ethereum.workspace = true
reth-transaction-pool.workspace = true
reth-tracing.workspace = true
eyre.workspace = true

View File

@ -0,0 +1,104 @@
//! This example shows how to configure custom components for a reth node.
#![warn(unused_crate_dependencies)]
use reth::{
builder::{components::PoolBuilder, BuilderContext, FullNodeTypes},
cli::Cli,
providers::CanonStateSubscriptions,
transaction_pool::{
blobstore::InMemoryBlobStore, EthTransactionPool, TransactionValidationTaskExecutor,
},
};
use reth_node_ethereum::EthereumNode;
use reth_tracing::tracing::{debug, info};
use reth_transaction_pool::PoolConfig;
fn main() {
Cli::parse_args()
.run(|builder, _| async move {
let handle = builder
// use the default ethereum node types
.with_types(EthereumNode::default())
// Configure the components of the node
// use default ethereum components but use our custom pool
.with_components(EthereumNode::components().pool(CustomPoolBuilder::default()))
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}
/// A custom pool builder
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CustomPoolBuilder {
/// Use custom pool config
pool_config: PoolConfig,
}
/// Implement the `PoolBuilder` trait for the custom pool builder
///
/// This will be used to build the transaction pool and its maintenance tasks during launch.
impl<Node> PoolBuilder<Node> for CustomPoolBuilder
where
Node: FullNodeTypes,
{
type Pool = EthTransactionPool<Node::Provider, InMemoryBlobStore>;
async fn build_pool(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Pool> {
let data_dir = ctx.data_dir();
let blob_store = InMemoryBlobStore::default();
let validator = TransactionValidationTaskExecutor::eth_builder(ctx.chain_spec())
.with_head_timestamp(ctx.head().timestamp)
.kzg_settings(ctx.kzg_settings()?)
.with_additional_tasks(5)
.build_with_tasks(
ctx.provider().clone(),
ctx.task_executor().clone(),
blob_store.clone(),
);
let transaction_pool =
reth_transaction_pool::Pool::eth_pool(validator, blob_store, self.pool_config);
info!(target: "reth::cli", "Transaction pool initialized");
let transactions_path = data_dir.txpool_transactions_path();
// spawn txpool maintenance task
{
let pool = transaction_pool.clone();
let chain_events = ctx.provider().canonical_state_stream();
let client = ctx.provider().clone();
let transactions_backup_config =
reth_transaction_pool::maintain::LocalTransactionBackupConfig::with_local_txs_backup(transactions_path);
ctx.task_executor().spawn_critical_with_graceful_shutdown_signal(
"local transactions backup task",
|shutdown| {
reth_transaction_pool::maintain::backup_local_transactions_task(
shutdown,
pool.clone(),
transactions_backup_config,
)
},
);
// spawn the maintenance task
ctx.task_executor().spawn_critical(
"txpool maintenance task",
reth_transaction_pool::maintain::maintain_transaction_pool_future(
client,
pool,
chain_events,
ctx.task_executor().clone(),
Default::default(),
),
);
debug!(target: "reth::cli", "Spawned txpool maintenance task");
}
Ok(transaction_pool)
}
}

View File

@ -5,19 +5,19 @@ publish = false
edition.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reth.workspace = true
reth-rpc-api.workspace = true
reth-rpc-types.workspace = true
reth-node-api.workspace = true
reth-node-core.workspace = true
reth-primitives.workspace = true
reth-payload-builder.workspace = true
reth-basic-payload-builder.workspace = true
reth-ethereum-payload-builder.workspace = true
reth-node-ethereum.workspace = true
reth-tracing.workspace = true
alloy-chains.workspace = true
jsonrpsee.workspace = true
eyre.workspace = true
tokio.workspace = true
thiserror.workspace = true

View File

@ -15,25 +15,43 @@
//! Once traits are implemented and custom types are defined, the [EngineTypes] trait can be
//! implemented:
#![warn(unused_crate_dependencies)]
use alloy_chains::Chain;
use jsonrpsee::http_client::HttpClient;
use reth::builder::spawn_node;
use reth::{
builder::{
components::{ComponentsBuilder, PayloadServiceBuilder},
node::NodeTypes,
BuilderContext, FullNodeTypes, Node, NodeBuilder, PayloadBuilderConfig,
},
primitives::revm_primitives::{BlockEnv, CfgEnvWithHandlerCfg},
providers::{CanonStateSubscriptions, StateProviderFactory},
tasks::TaskManager,
transaction_pool::TransactionPool,
};
use reth_basic_payload_builder::{
BasicPayloadJobGenerator, BasicPayloadJobGeneratorConfig, BuildArguments, BuildOutcome,
PayloadBuilder, PayloadConfig,
};
use reth_node_api::{
validate_version_specific_fields, AttributesValidationError, EngineApiMessageVersion,
EngineTypes, PayloadAttributes, PayloadBuilderAttributes, PayloadOrAttributes,
};
use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig};
use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes};
use reth_primitives::{
revm::config::revm_spec_by_timestamp_after_merge,
revm_primitives::{BlobExcessGasAndPrice, BlockEnv, CfgEnv, CfgEnvWithHandlerCfg, SpecId},
Address, ChainSpec, Genesis, Header, Withdrawals, B256, U256,
use reth_node_ethereum::{
node::{EthereumNetworkBuilder, EthereumPoolBuilder},
EthEvmConfig,
};
use reth_rpc_api::{EngineApiClient, EthApiClient};
use reth_payload_builder::{
error::PayloadBuilderError, EthBuiltPayload, EthPayloadBuilderAttributes, PayloadBuilderHandle,
PayloadBuilderService,
};
use reth_primitives::{Address, ChainSpec, Genesis, Header, Withdrawals, B256};
use reth_rpc_types::{
engine::{ForkchoiceState, PayloadAttributes as EthPayloadAttributes, PayloadId},
engine::{PayloadAttributes as EthPayloadAttributes, PayloadId},
withdrawal::Withdrawal,
};
use reth_tracing::{RethTracer, Tracer};
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
use thiserror::Error;
@ -84,7 +102,7 @@ impl PayloadAttributes for CustomPayloadAttributes {
}
}
/// Newtype around the payload builder attributes type
/// New type around the payload builder attributes type
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomPayloadBuilderAttributes(EthPayloadBuilderAttributes);
@ -123,50 +141,13 @@ impl PayloadBuilderAttributes for CustomPayloadBuilderAttributes {
fn withdrawals(&self) -> &Withdrawals {
&self.0.withdrawals
}
fn cfg_and_block_env(
&self,
chain_spec: &ChainSpec,
parent: &Header,
) -> (CfgEnvWithHandlerCfg, BlockEnv) {
// configure evm env based on parent block
let mut cfg = CfgEnv::default();
cfg.chain_id = chain_spec.chain().id();
// ensure we're not missing any timestamp based hardforks
let spec_id = revm_spec_by_timestamp_after_merge(chain_spec, self.timestamp());
// if the parent block did not have excess blob gas (i.e. it was pre-cancun), but it is
// cancun now, we need to set the excess blob gas to the default value
let blob_excess_gas_and_price = parent
.next_block_excess_blob_gas()
.or_else(|| {
if spec_id == SpecId::CANCUN {
// default excess blob gas is zero
Some(0)
} else {
None
}
})
.map(BlobExcessGasAndPrice::new);
let block_env = BlockEnv {
number: U256::from(parent.number + 1),
coinbase: self.suggested_fee_recipient(),
timestamp: U256::from(self.timestamp()),
difficulty: U256::ZERO,
prevrandao: Some(self.prev_randao()),
gas_limit: U256::from(parent.gas_limit),
// calculate basefee based on parent block's gas usage
basefee: U256::from(
parent
.next_block_base_fee(chain_spec.base_fee_params(self.timestamp()))
.unwrap_or_default(),
),
// calculate excess gas based on parent block's blob gas usage
blob_excess_gas_and_price,
};
(CfgEnvWithHandlerCfg::new_with_spec_id(cfg, spec_id), block_env)
self.0.cfg_and_block_env(chain_spec, parent)
}
}
@ -190,10 +171,156 @@ impl EngineTypes for CustomEngineTypes {
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
struct MyCustomNode;
/// Configure the node types
impl NodeTypes for MyCustomNode {
type Primitives = ();
// use the custom engine types
type Engine = CustomEngineTypes;
// use the default ethereum EVM config
type Evm = EthEvmConfig;
fn evm_config(&self) -> Self::Evm {
Self::Evm::default()
}
}
/// Implement the Node trait for the custom node
///
/// This provides a preset configuration for the node
impl<N> Node<N> for MyCustomNode
where
N: FullNodeTypes<Engine = CustomEngineTypes>,
{
type PoolBuilder = EthereumPoolBuilder;
type NetworkBuilder = EthereumNetworkBuilder;
type PayloadBuilder = CustomPayloadServiceBuilder;
fn components(
self,
) -> ComponentsBuilder<N, Self::PoolBuilder, Self::PayloadBuilder, Self::NetworkBuilder> {
ComponentsBuilder::default()
.node_types::<N>()
.pool(EthereumPoolBuilder::default())
.payload(CustomPayloadServiceBuilder::default())
.network(EthereumNetworkBuilder::default())
}
}
/// A custom payload service builder that supports the custom engine types
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct CustomPayloadServiceBuilder;
impl<Node, Pool> PayloadServiceBuilder<Node, Pool> for CustomPayloadServiceBuilder
where
Node: FullNodeTypes<Engine = CustomEngineTypes>,
Pool: TransactionPool + Unpin + 'static,
{
async fn spawn_payload_service(
self,
ctx: &BuilderContext<Node>,
pool: Pool,
) -> eyre::Result<PayloadBuilderHandle<Node::Engine>> {
let payload_builder = CustomPayloadBuilder::default();
let conf = ctx.payload_builder_config();
let payload_job_config = BasicPayloadJobGeneratorConfig::default()
.interval(conf.interval())
.deadline(conf.deadline())
.max_payload_tasks(conf.max_payload_tasks())
.extradata(conf.extradata_rlp_bytes())
.max_gas_limit(conf.max_gas_limit());
let payload_generator = BasicPayloadJobGenerator::with_builder(
ctx.provider().clone(),
pool,
ctx.task_executor().clone(),
payload_job_config,
ctx.chain_spec(),
payload_builder,
);
let (payload_service, payload_builder) =
PayloadBuilderService::new(payload_generator, ctx.provider().canonical_state_stream());
ctx.task_executor().spawn_critical("payload builder service", Box::pin(payload_service));
Ok(payload_builder)
}
}
/// The type responsible for building custom payloads
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct CustomPayloadBuilder;
impl<Pool, Client> PayloadBuilder<Pool, Client> for CustomPayloadBuilder
where
Client: StateProviderFactory,
Pool: TransactionPool,
{
type Attributes = CustomPayloadBuilderAttributes;
type BuiltPayload = EthBuiltPayload;
fn try_build(
&self,
args: BuildArguments<Pool, Client, Self::Attributes, Self::BuiltPayload>,
) -> Result<BuildOutcome<Self::BuiltPayload>, PayloadBuilderError> {
let BuildArguments { client, pool, cached_reads, config, cancel, best_payload } = args;
let PayloadConfig {
initialized_block_env,
initialized_cfg,
parent_block,
extra_data,
attributes,
chain_spec,
} = config;
// This reuses the default EthereumPayloadBuilder to build the payload
// but any custom logic can be implemented here
reth_ethereum_payload_builder::EthereumPayloadBuilder::default().try_build(BuildArguments {
client,
pool,
cached_reads,
config: PayloadConfig {
initialized_block_env,
initialized_cfg,
parent_block,
extra_data,
attributes: attributes.0,
chain_spec,
},
cancel,
best_payload,
})
}
fn build_empty_payload(
client: &Client,
config: PayloadConfig<Self::Attributes>,
) -> Result<Self::BuiltPayload, PayloadBuilderError> {
let PayloadConfig {
initialized_block_env,
initialized_cfg,
parent_block,
extra_data,
attributes,
chain_spec,
} = config;
<reth_ethereum_payload_builder::EthereumPayloadBuilder as PayloadBuilder<Pool,Client>> ::build_empty_payload(client,
PayloadConfig { initialized_block_env, initialized_cfg, parent_block, extra_data, attributes: attributes.0, chain_spec }
)
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
// this launches a test node with http
let rpc_args = RpcServerArgs::default().with_http();
let _guard = RethTracer::new().init()?;
let tasks = TaskManager::current();
// create optimism genesis with canyon at block 2
let spec = ChainSpec::builder()
@ -204,46 +331,17 @@ async fn main() -> eyre::Result<()> {
.shanghai_activated()
.build();
let genesis_hash = spec.genesis_hash();
// create node config
let node_config = NodeConfig::test().with_rpc(rpc_args).with_chain(spec);
let node_config =
NodeConfig::test().with_rpc(RpcServerArgs::default().with_http()).with_chain(spec);
let (handle, _manager) = spawn_node(node_config).await.unwrap();
let handle = NodeBuilder::new(node_config)
.testing_node(tasks.executor())
.launch_node(MyCustomNode::default())
.await
.unwrap();
// call a function on the node
let client = handle.rpc_server_handles().auth.http_client();
let block_number = client.block_number().await.unwrap();
println!("Node started");
// it should be zero, since this is an ephemeral test node
assert_eq!(block_number, U256::ZERO);
// call the engine_forkchoiceUpdated function with payload attributes
let forkchoice_state = ForkchoiceState {
head_block_hash: genesis_hash,
safe_block_hash: genesis_hash,
finalized_block_hash: genesis_hash,
};
let payload_attributes = CustomPayloadAttributes {
inner: EthPayloadAttributes {
timestamp: 1,
prev_randao: Default::default(),
suggested_fee_recipient: Default::default(),
withdrawals: Some(vec![]),
parent_beacon_block_root: None,
},
custom: 42,
};
// call the engine_forkchoiceUpdated function with payload attributes
let res = <HttpClient as EngineApiClient<CustomEngineTypes>>::fork_choice_updated_v2(
&client,
forkchoice_state,
Some(payload_attributes),
)
.await;
assert!(res.is_ok());
Ok(())
handle.node_exit_future.await
}

View File

@ -5,17 +5,15 @@ publish = false
edition.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reth.workspace = true
reth-primitives.workspace = true
reth-node-api.workspace = true
reth-basic-payload-builder.workspace = true
reth-payload-builder.workspace = true
reth-node-ethereum.workspace = true
reth-ethereum-payload-builder.workspace = true
tracing.workspace = true
clap = { workspace = true, features = ["derive"] }
futures-util.workspace = true
eyre.workspace = true
tokio.workspace = true
eyre.workspace = true

View File

@ -8,58 +8,40 @@
//! ```
//!
//! This launch the regular reth node overriding the engine api payload builder with our custom.
use clap::Parser;
#![warn(unused_crate_dependencies)]
use generator::EmptyBlockPayloadJobGenerator;
use reth::{
cli::{
components::RethNodeComponents,
config::PayloadBuilderConfig,
ext::{NoArgsCliExt, RethNodeCommandConfig},
Cli,
},
builder::{components::PayloadServiceBuilder, node::FullNodeTypes, BuilderContext},
cli::{config::PayloadBuilderConfig, Cli},
payload::PayloadBuilderHandle,
providers::CanonStateSubscriptions,
tasks::TaskSpawner,
transaction_pool::TransactionPool,
};
use reth_basic_payload_builder::{BasicPayloadJobGeneratorConfig, PayloadBuilder};
use reth_node_api::EngineTypes;
use reth_basic_payload_builder::BasicPayloadJobGeneratorConfig;
use reth_node_ethereum::{EthEngineTypes, EthereumNode};
use reth_payload_builder::PayloadBuilderService;
pub mod generator;
pub mod job;
fn main() {
Cli::<NoArgsCliExt<MyCustomBuilder>>::parse()
.with_node_extension(MyCustomBuilder::default())
.run()
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
struct MyCustomBuilder;
pub struct CustomPayloadBuilder;
impl RethNodeCommandConfig for MyCustomBuilder {
fn spawn_payload_builder_service<Conf, Reth, Builder, Engine>(
&mut self,
conf: &Conf,
components: &Reth,
payload_builder: Builder,
) -> eyre::Result<PayloadBuilderHandle<Engine>>
where
Conf: PayloadBuilderConfig,
Reth: RethNodeComponents,
Engine: EngineTypes + 'static,
Builder: PayloadBuilder<
Reth::Pool,
Reth::Provider,
Attributes = Engine::PayloadBuilderAttributes,
BuiltPayload = Engine::BuiltPayload,
> + Unpin
+ 'static,
{
impl<Node, Pool> PayloadServiceBuilder<Node, Pool> for CustomPayloadBuilder
where
Node: FullNodeTypes<Engine = EthEngineTypes>,
Pool: TransactionPool + Unpin + 'static,
{
async fn spawn_payload_service(
self,
ctx: &BuilderContext<Node>,
pool: Pool,
) -> eyre::Result<PayloadBuilderHandle<Node::Engine>> {
tracing::info!("Spawning a custom payload builder");
let conf = ctx.payload_builder_config();
let payload_job_config = BasicPayloadJobGeneratorConfig::default()
.interval(conf.interval())
@ -69,23 +51,38 @@ impl RethNodeCommandConfig for MyCustomBuilder {
.max_gas_limit(conf.max_gas_limit());
let payload_generator = EmptyBlockPayloadJobGenerator::with_builder(
components.provider(),
components.pool(),
components.task_executor(),
ctx.provider().clone(),
pool,
ctx.task_executor().clone(),
payload_job_config,
components.chain_spec().clone(),
payload_builder,
ctx.chain_spec().clone(),
reth_ethereum_payload_builder::EthereumPayloadBuilder::default(),
);
let (payload_service, payload_builder) = PayloadBuilderService::new(
payload_generator,
components.events().canonical_state_stream(),
);
let (payload_service, payload_builder) =
PayloadBuilderService::new(payload_generator, ctx.provider().canonical_state_stream());
components
.task_executor()
ctx.task_executor()
.spawn_critical("custom payload builder service", Box::pin(payload_service));
Ok(payload_builder)
}
}
fn main() {
Cli::parse_args()
.run(|builder, _| async move {
let handle = builder
.with_types(EthereumNode::default())
// Configure the components of the node
// use default ethereum components but use our custom payload builder
.with_components(
EthereumNode::components().payload(CustomPayloadBuilder::default()),
)
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}

View File

@ -7,10 +7,6 @@ license.workspace = true
[dependencies]
reth.workspace = true
reth-node-ethereum.workspace = true
clap = { workspace = true, features = ["derive"] }
jsonrpsee = { workspace = true, features = ["server", "macros"] }
futures-util.workspace = true
eyre.workspace = true
[dev-dependencies]
tokio.workspace = true
futures-util.workspace = true

View File

@ -8,35 +8,65 @@
//!
//! If no recipients are specified, all transactions will be traced.
#![warn(unused_crate_dependencies)]
use clap::Parser;
use futures_util::StreamExt;
use reth::{
cli::{
components::{RethNodeComponents, RethRpcComponents, RethRpcServerHandles},
config::RethRpcConfig,
ext::{RethCliExt, RethNodeCommandConfig},
Cli,
},
builder::NodeHandle,
cli::Cli,
primitives::{Address, IntoRecoveredTransaction},
rpc::{
compat::transaction::transaction_to_call_request,
types::trace::{parity::TraceType, tracerequest::TraceCallRequest},
},
tasks::TaskSpawner,
transaction_pool::TransactionPool,
};
use reth_node_ethereum::node::EthereumNode;
use std::collections::HashSet;
fn main() {
Cli::<MyRethCliExt>::parse().run().unwrap();
}
Cli::<RethCliTxpoolExt>::parse()
.run(|builder, args| async move {
// launch the node
let NodeHandle { mut node, node_exit_future } =
builder.node(EthereumNode::default()).launch().await?;
/// The type that tells the reth CLI what extensions to use
struct MyRethCliExt;
let recipients = args.recipients.iter().copied().collect::<HashSet<_>>();
impl RethCliExt for MyRethCliExt {
/// This tells the reth CLI to trace addresses via `RethCliTxpoolExt`
type Node = RethCliTxpoolExt;
// create a new subscription to pending transactions
let mut pending_transactions = node.pool.new_pending_pool_transactions_listener();
// get an instance of the `trace_` API handler
let traceapi = node.rpc_registry.trace_api();
println!("Spawning trace task!");
// Spawn an async block to listen for transactions.
node.task_executor.spawn(Box::pin(async move {
// Waiting for new transactions
while let Some(event) = pending_transactions.next().await {
let tx = event.transaction;
println!("Transaction received: {tx:?}");
if let Some(tx_recipient_address) = tx.to() {
if recipients.is_empty() || recipients.contains(&tx_recipient_address) {
// trace the transaction with `trace_call`
let callrequest =
transaction_to_call_request(tx.to_recovered_transaction());
let tracerequest = TraceCallRequest::new(callrequest)
.with_trace_type(TraceType::Trace);
if let Ok(trace_result) = traceapi.trace_call(tracerequest).await {
let hash = tx.hash();
println!("trace result for transaction {hash}: {trace_result:?}");
}
}
}
}
}));
node_exit_future.await
})
.unwrap();
}
/// Our custom cli args extension that adds one flag to reth default CLI.
@ -46,50 +76,3 @@ struct RethCliTxpoolExt {
#[arg(long, value_delimiter = ',')]
pub recipients: Vec<Address>,
}
impl RethNodeCommandConfig for RethCliTxpoolExt {
fn on_rpc_server_started<Conf, Reth>(
&mut self,
_config: &Conf,
components: &Reth,
rpc_components: RethRpcComponents<'_, Reth>,
_handles: RethRpcServerHandles,
) -> eyre::Result<()>
where
Conf: RethRpcConfig,
Reth: RethNodeComponents,
{
let recipients = self.recipients.iter().copied().collect::<HashSet<_>>();
// create a new subscription to pending transactions
let mut pending_transactions = components.pool().new_pending_pool_transactions_listener();
// get an instance of the `trace_` API handler
let traceapi = rpc_components.registry.trace_api();
println!("Spawning trace task!");
// Spawn an async block to listen for transactions.
components.task_executor().spawn(Box::pin(async move {
// Waiting for new transactions
while let Some(event) = pending_transactions.next().await {
let tx = event.transaction;
println!("Transaction received: {tx:?}");
if let Some(tx_recipient_address) = tx.to() {
if recipients.is_empty() || recipients.contains(&tx_recipient_address) {
// trace the transaction with `trace_call`
let callrequest =
transaction_to_call_request(tx.to_recovered_transaction());
let tracerequest =
TraceCallRequest::new(callrequest).with_trace_type(TraceType::Trace);
if let Ok(trace_result) = traceapi.trace_call(tracerequest).await {
let hash = tx.hash();
println!("trace result for transaction {hash}: {trace_result:?}");
}
}
}
}
}));
Ok(())
}
}