//! This example shows how to implement a custom [EngineTypes]. //! //! The [EngineTypes] trait can be implemented to configure the engine to work with custom types, //! as long as those types implement certain traits. //! //! Custom payload attributes can be supported by implementing two main traits: //! //! [PayloadAttributes] can be implemented for payload attributes types that are used as //! arguments to the `engine_forkchoiceUpdated` method. This type should be used to define and //! _spawn_ payload jobs. //! //! [PayloadBuilderAttributes] can be implemented for payload attributes types that _describe_ //! running payload jobs. //! //! Once traits are implemented and custom types are defined, the [EngineTypes] trait can be //! implemented: #![cfg_attr(not(test), warn(unused_crate_dependencies))] use std::convert::Infallible; use serde::{Deserialize, Serialize}; use thiserror::Error; use alloy_genesis::Genesis; use reth::{ api::PayloadTypes, 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_chainspec::{Chain, ChainSpec}; use reth_node_api::{ payload::{EngineApiMessageVersion, EngineObjectValidationError, PayloadOrAttributes}, validate_version_specific_fields, EngineTypes, PayloadAttributes, PayloadBuilderAttributes, }; use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig}; use reth_node_ethereum::node::{ EthereumAddOns, EthereumConsensusBuilder, EthereumExecutorBuilder, EthereumNetworkBuilder, EthereumPoolBuilder, }; use reth_payload_builder::{ error::PayloadBuilderError, EthBuiltPayload, EthPayloadBuilderAttributes, PayloadBuilderHandle, PayloadBuilderService, }; use reth_primitives::{Address, Header, Withdrawals, B256}; use reth_rpc_types::{ engine::{ ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4, PayloadAttributes as EthPayloadAttributes, PayloadId, }, ExecutionPayloadV1, Withdrawal, }; use reth_tracing::{RethTracer, Tracer}; /// A custom payload attributes type. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomPayloadAttributes { /// An inner payload type #[serde(flatten)] pub inner: EthPayloadAttributes, /// A custom field pub custom: u64, } /// Custom error type used in payload attributes validation #[derive(Debug, Error)] pub enum CustomError { #[error("Custom field is not zero")] CustomFieldIsNotZero, } impl PayloadAttributes for CustomPayloadAttributes { fn timestamp(&self) -> u64 { self.inner.timestamp() } fn withdrawals(&self) -> Option<&Vec> { self.inner.withdrawals() } fn parent_beacon_block_root(&self) -> Option { self.inner.parent_beacon_block_root() } fn ensure_well_formed_attributes( &self, chain_spec: &ChainSpec, version: EngineApiMessageVersion, ) -> Result<(), EngineObjectValidationError> { validate_version_specific_fields(chain_spec, version, self.into())?; // custom validation logic - ensure that the custom field is not zero if self.custom == 0 { return Err(EngineObjectValidationError::invalid_params( CustomError::CustomFieldIsNotZero, )) } Ok(()) } } /// New type around the payload builder attributes type #[derive(Clone, Debug, PartialEq, Eq)] pub struct CustomPayloadBuilderAttributes(EthPayloadBuilderAttributes); impl PayloadBuilderAttributes for CustomPayloadBuilderAttributes { type RpcPayloadAttributes = CustomPayloadAttributes; type Error = Infallible; fn try_new(parent: B256, attributes: CustomPayloadAttributes) -> Result { Ok(Self(EthPayloadBuilderAttributes::new(parent, attributes.inner))) } fn payload_id(&self) -> PayloadId { self.0.id } fn parent(&self) -> B256 { self.0.parent } fn timestamp(&self) -> u64 { self.0.timestamp } fn parent_beacon_block_root(&self) -> Option { self.0.parent_beacon_block_root } fn suggested_fee_recipient(&self) -> Address { self.0.suggested_fee_recipient } fn prev_randao(&self) -> B256 { self.0.prev_randao } fn withdrawals(&self) -> &Withdrawals { &self.0.withdrawals } fn cfg_and_block_env( &self, chain_spec: &ChainSpec, parent: &Header, ) -> (CfgEnvWithHandlerCfg, BlockEnv) { self.0.cfg_and_block_env(chain_spec, parent) } } /// Custom engine types - uses a custom payload attributes RPC type, but uses the default /// payload builder attributes type. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[non_exhaustive] pub struct CustomEngineTypes; impl PayloadTypes for CustomEngineTypes { type BuiltPayload = EthBuiltPayload; type PayloadAttributes = CustomPayloadAttributes; type PayloadBuilderAttributes = CustomPayloadBuilderAttributes; } impl EngineTypes for CustomEngineTypes { type ExecutionPayloadV1 = ExecutionPayloadV1; type ExecutionPayloadV2 = ExecutionPayloadEnvelopeV2; type ExecutionPayloadV3 = ExecutionPayloadEnvelopeV3; type ExecutionPayloadV4 = ExecutionPayloadEnvelopeV4; fn validate_version_specific_fields( chain_spec: &ChainSpec, version: EngineApiMessageVersion, payload_or_attrs: PayloadOrAttributes<'_, CustomPayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { validate_version_specific_fields(chain_spec, version, payload_or_attrs) } } #[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; } /// Implement the Node trait for the custom node /// /// This provides a preset configuration for the node impl Node for MyCustomNode where N: FullNodeTypes, { type ComponentsBuilder = ComponentsBuilder< N, EthereumPoolBuilder, CustomPayloadServiceBuilder, EthereumNetworkBuilder, EthereumExecutorBuilder, EthereumConsensusBuilder, >; type AddOns = EthereumAddOns; fn components_builder(&self) -> Self::ComponentsBuilder { ComponentsBuilder::default() .node_types::() .pool(EthereumPoolBuilder::default()) .payload(CustomPayloadServiceBuilder::default()) .network(EthereumNetworkBuilder::default()) .executor(EthereumExecutorBuilder::default()) .consensus(EthereumConsensusBuilder::default()) } } /// A custom payload service builder that supports the custom engine types #[derive(Debug, Default, Clone)] #[non_exhaustive] pub struct CustomPayloadServiceBuilder; impl PayloadServiceBuilder for CustomPayloadServiceBuilder where Node: FullNodeTypes, Pool: TransactionPool + Unpin + 'static, { async fn spawn_payload_service( self, ctx: &BuilderContext, pool: Pool, ) -> eyre::Result> { 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_bytes()); 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 PayloadBuilder for CustomPayloadBuilder where Client: StateProviderFactory, Pool: TransactionPool, { type Attributes = CustomPayloadBuilderAttributes; type BuiltPayload = EthBuiltPayload; fn try_build( &self, args: BuildArguments, ) -> Result, 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( &self, client: &Client, config: PayloadConfig, ) -> Result { let PayloadConfig { initialized_block_env, initialized_cfg, parent_block, extra_data, attributes, chain_spec, } = config; >::build_empty_payload(&reth_ethereum_payload_builder::EthereumPayloadBuilder::default(),client, PayloadConfig { initialized_block_env, initialized_cfg, parent_block, extra_data, attributes: attributes.0, chain_spec }) } } #[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() .build(); // create node config let node_config = NodeConfig::test().with_rpc(RpcServerArgs::default().with_http()).with_chain(spec); let handle = NodeBuilder::new(node_config) .testing_node(tasks.executor()) .launch_node(MyCustomNode::default()) .await .unwrap(); println!("Node started"); handle.node_exit_future.await }