Example/custom payload builder (#6269)

This commit is contained in:
Luca Provini
2024-02-01 13:26:18 +01:00
committed by GitHub
parent 9193b17574
commit a3128ae36d
6 changed files with 301 additions and 0 deletions

16
Cargo.lock generated
View File

@ -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"

View File

@ -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"]

View File

@ -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

View File

@ -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<Client, Pool, Tasks, Builder> {
/// 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<ChainSpec>,
/// The type responsible for building payloads.
///
/// See [PayloadBuilder]
builder: Builder,
}
// === impl EmptyBlockPayloadJobGenerator ===
impl<Client, Pool, Tasks, Builder> EmptyBlockPayloadJobGenerator<Client, Pool, Tasks, Builder> {
/// 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<ChainSpec>,
builder: Builder,
) -> Self {
Self { client, pool, executor, _config: config, builder, chain_spec }
}
}
impl<Client, Pool, Tasks, Builder> PayloadJobGenerator
for EmptyBlockPayloadJobGenerator<Client, Pool, Tasks, Builder>
where
Client: StateProviderFactory + BlockReaderIdExt + Clone + Unpin + 'static,
Pool: TransactionPool + Unpin + 'static,
Tasks: TaskSpawner + Clone + Unpin + 'static,
Builder: PayloadBuilder<Pool, Client> + Unpin + 'static,
<Builder as PayloadBuilder<Pool, Client>>::Attributes: Unpin + Clone,
<Builder as PayloadBuilder<Pool, Client>>::BuiltPayload: Unpin + Clone,
{
type Job = EmptyBlockPayloadJob<Client, Pool, Tasks, Builder>;
/// This is invoked when the node receives payload attributes from the beacon node via
/// `engine_forkchoiceUpdatedV1`
fn new_payload_job(
&self,
attributes: <Builder as PayloadBuilder<Pool, Client>>::Attributes,
) -> Result<Self::Job, PayloadBuilderError> {
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,
})
}
}

View File

@ -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<Client, Pool, Tasks, Builder>
where
Builder: PayloadBuilder<Pool, Client>,
{
/// The configuration for how the payload will be created.
pub(crate) config: PayloadConfig<Builder::Attributes>,
/// 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<Client, Pool, Tasks, Builder> PayloadJob for EmptyBlockPayloadJob<Client, Pool, Tasks, Builder>
where
Client: StateProviderFactory + Clone + Unpin + 'static,
Pool: TransactionPool + Unpin + 'static,
Tasks: TaskSpawner + Clone + 'static,
Builder: PayloadBuilder<Pool, Client> + Unpin + 'static,
<Builder as PayloadBuilder<Pool, Client>>::Attributes: Unpin + Clone,
<Builder as PayloadBuilder<Pool, Client>>::BuiltPayload: Unpin + Clone,
{
type PayloadAttributes = Builder::Attributes;
type ResolvePayloadFuture =
futures_util::future::Ready<Result<Self::BuiltPayload, PayloadBuilderError>>;
type BuiltPayload = Builder::BuiltPayload;
fn best_payload(&self) -> Result<Self::BuiltPayload, PayloadBuilderError> {
let payload = Builder::build_empty_payload(&self.client, self.config.clone())?;
Ok(payload)
}
fn payload_attributes(&self) -> Result<Self::PayloadAttributes, PayloadBuilderError> {
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<Client, Pool, Tasks, Builder> Future for EmptyBlockPayloadJob<Client, Pool, Tasks, Builder>
where
Client: StateProviderFactory + Clone + Unpin + 'static,
Pool: TransactionPool + Unpin + 'static,
Tasks: TaskSpawner + Clone + 'static,
Builder: PayloadBuilder<Pool, Client> + Unpin + 'static,
<Builder as PayloadBuilder<Pool, Client>>::Attributes: Unpin + Clone,
<Builder as PayloadBuilder<Pool, Client>>::BuiltPayload: Unpin + Clone,
{
type Output = Result<(), PayloadBuilderError>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Pending
}
}

View File

@ -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::<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;
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,
{
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)
}
}