From a3128ae36d3e0c7ba44c2aa14476f9ccd0d1fd8d Mon Sep 17 00:00:00 2001 From: Luca Provini Date: Thu, 1 Feb 2024 13:26:18 +0100 Subject: [PATCH] Example/custom payload builder (#6269) --- Cargo.lock | 16 ++++ Cargo.toml | 1 + examples/custom-payload-builder/Cargo.toml | 21 ++++ .../custom-payload-builder/src/generator.rs | 96 +++++++++++++++++++ examples/custom-payload-builder/src/job.rs | 76 +++++++++++++++ examples/custom-payload-builder/src/main.rs | 91 ++++++++++++++++++ 6 files changed, 301 insertions(+) create mode 100644 examples/custom-payload-builder/Cargo.toml create mode 100644 examples/custom-payload-builder/src/generator.rs create mode 100644 examples/custom-payload-builder/src/job.rs create mode 100644 examples/custom-payload-builder/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 16ba50294..0477e3807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,6 +1831,22 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "custom-payload-builder" +version = "0.0.0" +dependencies = [ + "clap", + "eyre", + "futures-util", + "reth", + "reth-basic-payload-builder", + "reth-node-api", + "reth-payload-builder", + "reth-primitives", + "tokio", + "tracing", +] + [[package]] name = "darling" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index d6b33e28b..53e0f0112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "examples/manual-p2p/", "examples/rpc-db/", "examples/trace-transaction-cli/", + "examples/custom-payload-builder/", "testing/ef-tests/", ] default-members = ["bin/reth"] diff --git a/examples/custom-payload-builder/Cargo.toml b/examples/custom-payload-builder/Cargo.toml new file mode 100644 index 000000000..7c1694828 --- /dev/null +++ b/examples/custom-payload-builder/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "custom-payload-builder" +version = "0.0.0" +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 + +tracing.workspace = true +clap = { workspace = true, features = ["derive"] } +futures-util.workspace = true +eyre.workspace = true +tokio.workspace = true \ No newline at end of file diff --git a/examples/custom-payload-builder/src/generator.rs b/examples/custom-payload-builder/src/generator.rs new file mode 100644 index 000000000..fe29c33bb --- /dev/null +++ b/examples/custom-payload-builder/src/generator.rs @@ -0,0 +1,96 @@ +use crate::job::EmptyBlockPayloadJob; +use reth::{ + providers::{BlockReaderIdExt, BlockSource, StateProviderFactory}, + tasks::TaskSpawner, + transaction_pool::TransactionPool, +}; +use reth_basic_payload_builder::{BasicPayloadJobGeneratorConfig, PayloadBuilder, PayloadConfig}; +use reth_node_api::PayloadBuilderAttributes; +use reth_payload_builder::{error::PayloadBuilderError, PayloadJobGenerator}; +use reth_primitives::{BlockNumberOrTag, Bytes, ChainSpec}; +use std::sync::Arc; + +/// The generator type that creates new jobs that builds empty blocks. +#[derive(Debug)] +pub struct EmptyBlockPayloadJobGenerator { + /// The client that can interact with the chain. + client: Client, + /// txpool + pool: Pool, + /// How to spawn building tasks + executor: Tasks, + /// The configuration for the job generator. + _config: BasicPayloadJobGeneratorConfig, + /// The chain spec. + chain_spec: Arc, + /// The type responsible for building payloads. + /// + /// See [PayloadBuilder] + builder: Builder, +} + +// === impl EmptyBlockPayloadJobGenerator === + +impl EmptyBlockPayloadJobGenerator { + /// Creates a new [EmptyBlockPayloadJobGenerator] with the given config and custom + /// [PayloadBuilder] + pub fn with_builder( + client: Client, + pool: Pool, + executor: Tasks, + config: BasicPayloadJobGeneratorConfig, + chain_spec: Arc, + builder: Builder, + ) -> Self { + Self { client, pool, executor, _config: config, builder, chain_spec } + } +} + +impl PayloadJobGenerator + for EmptyBlockPayloadJobGenerator +where + Client: StateProviderFactory + BlockReaderIdExt + Clone + Unpin + 'static, + Pool: TransactionPool + Unpin + 'static, + Tasks: TaskSpawner + Clone + Unpin + 'static, + Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: Unpin + Clone, + >::BuiltPayload: Unpin + Clone, +{ + type Job = EmptyBlockPayloadJob; + + /// This is invoked when the node receives payload attributes from the beacon node via + /// `engine_forkchoiceUpdatedV1` + fn new_payload_job( + &self, + attributes: >::Attributes, + ) -> Result { + let parent_block = if attributes.parent().is_zero() { + // use latest block if parent is zero: genesis block + self.client + .block_by_number_or_tag(BlockNumberOrTag::Latest)? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent()))? + .seal_slow() + } else { + let block = self + .client + .find_block_by_hash(attributes.parent(), BlockSource::Any)? + .ok_or_else(|| PayloadBuilderError::MissingParentBlock(attributes.parent()))?; + + // we already know the hash, so we can seal it + block.seal(attributes.parent()) + }; + let config = PayloadConfig::new( + Arc::new(parent_block), + Bytes::default(), + attributes, + Arc::clone(&self.chain_spec), + ); + Ok(EmptyBlockPayloadJob { + client: self.client.clone(), + _pool: self.pool.clone(), + _executor: self.executor.clone(), + _builder: self.builder.clone(), + config, + }) + } +} diff --git a/examples/custom-payload-builder/src/job.rs b/examples/custom-payload-builder/src/job.rs new file mode 100644 index 000000000..6438c6b08 --- /dev/null +++ b/examples/custom-payload-builder/src/job.rs @@ -0,0 +1,76 @@ +use futures_util::Future; +use reth::{ + providers::StateProviderFactory, tasks::TaskSpawner, transaction_pool::TransactionPool, +}; +use reth_basic_payload_builder::{PayloadBuilder, PayloadConfig}; +use reth_payload_builder::{error::PayloadBuilderError, KeepPayloadJobAlive, PayloadJob}; + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +/// A [PayloadJob] that builds empty blocks. +pub struct EmptyBlockPayloadJob +where + Builder: PayloadBuilder, +{ + /// The configuration for how the payload will be created. + pub(crate) config: PayloadConfig, + /// The client that can interact with the chain. + pub(crate) client: Client, + /// The transaction pool. + pub(crate) _pool: Pool, + /// How to spawn building tasks + pub(crate) _executor: Tasks, + /// The type responsible for building payloads. + /// + /// See [PayloadBuilder] + pub(crate) _builder: Builder, +} + +impl PayloadJob for EmptyBlockPayloadJob +where + Client: StateProviderFactory + Clone + Unpin + 'static, + Pool: TransactionPool + Unpin + 'static, + Tasks: TaskSpawner + Clone + 'static, + Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: Unpin + Clone, + >::BuiltPayload: Unpin + Clone, +{ + type PayloadAttributes = Builder::Attributes; + type ResolvePayloadFuture = + futures_util::future::Ready>; + type BuiltPayload = Builder::BuiltPayload; + + fn best_payload(&self) -> Result { + let payload = Builder::build_empty_payload(&self.client, self.config.clone())?; + Ok(payload) + } + + fn payload_attributes(&self) -> Result { + Ok(self.config.attributes.clone()) + } + + fn resolve(&mut self) -> (Self::ResolvePayloadFuture, KeepPayloadJobAlive) { + let payload = self.best_payload(); + (futures_util::future::ready(payload), KeepPayloadJobAlive::No) + } +} + +/// A [PayloadJob] is a a future that's being polled by the `PayloadBuilderService` +impl Future for EmptyBlockPayloadJob +where + Client: StateProviderFactory + Clone + Unpin + 'static, + Pool: TransactionPool + Unpin + 'static, + Tasks: TaskSpawner + Clone + 'static, + Builder: PayloadBuilder + Unpin + 'static, + >::Attributes: Unpin + Clone, + >::BuiltPayload: Unpin + Clone, +{ + type Output = Result<(), PayloadBuilderError>; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + Poll::Pending + } +} diff --git a/examples/custom-payload-builder/src/main.rs b/examples/custom-payload-builder/src/main.rs new file mode 100644 index 000000000..6c1ca5094 --- /dev/null +++ b/examples/custom-payload-builder/src/main.rs @@ -0,0 +1,91 @@ +//! Example for how hook into the node via the CLI extension mechanism without registering +//! additional arguments +//! +//! Run with +//! +//! ```not_rust +//! cargo run -p custom-payload-builder -- node +//! ``` +//! +//! This launch the regular reth node overriding the engine api payload builder with our custom. +use clap::Parser; +use generator::EmptyBlockPayloadJobGenerator; +use reth::{ + cli::{ + components::RethNodeComponents, + config::PayloadBuilderConfig, + ext::{NoArgsCliExt, RethNodeCommandConfig}, + Cli, + }, + payload::PayloadBuilderHandle, + providers::CanonStateSubscriptions, + tasks::TaskSpawner, +}; +use reth_basic_payload_builder::{BasicPayloadJobGeneratorConfig, PayloadBuilder}; +use reth_node_api::EngineTypes; +use reth_payload_builder::PayloadBuilderService; + +pub mod generator; +pub mod job; + +fn main() { + Cli::>::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; + +impl RethNodeCommandConfig for MyCustomBuilder { + fn spawn_payload_builder_service( + &mut self, + conf: &Conf, + components: &Reth, + payload_builder: Builder, + ) -> eyre::Result> + where + Conf: PayloadBuilderConfig, + Reth: RethNodeComponents, + Engine: EngineTypes + 'static, + Builder: PayloadBuilder< + Reth::Pool, + Reth::Provider, + Attributes = Engine::PayloadBuilderAttributes, + BuiltPayload = Engine::BuiltPayload, + > + Unpin + + 'static, + { + tracing::info!("Spawning a custom payload builder"); + + 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 = EmptyBlockPayloadJobGenerator::with_builder( + components.provider(), + components.pool(), + components.task_executor(), + payload_job_config, + components.chain_spec().clone(), + payload_builder, + ); + + let (payload_service, payload_builder) = PayloadBuilderService::new( + payload_generator, + components.events().canonical_state_stream(), + ); + + components + .task_executor() + .spawn_critical("custom payload builder service", Box::pin(payload_service)); + + Ok(payload_builder) + } +}