//! 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: use alloy_chains::Chain; use jsonrpsee::http_client::HttpClient; use reth::builder::spawn_node; 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_rpc_api::{EngineApiClient, EthApiClient}; use reth_rpc_types::{ engine::{ForkchoiceState, PayloadAttributes as EthPayloadAttributes, PayloadId}, withdrawal::Withdrawal, }; use serde::{Deserialize, Serialize}; use std::convert::Infallible; use thiserror::Error; /// 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<(), AttributesValidationError> { 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(AttributesValidationError::invalid_params(CustomError::CustomFieldIsNotZero)) } Ok(()) } } /// Newtype 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) { // 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) } } /// Custom engine types - uses a custom payload attributes RPC type, but uses the default /// payload builder attributes type. #[derive(Clone, Debug, Default, Deserialize)] #[non_exhaustive] pub struct CustomEngineTypes; impl EngineTypes for CustomEngineTypes { type PayloadAttributes = CustomPayloadAttributes; type PayloadBuilderAttributes = CustomPayloadBuilderAttributes; type BuiltPayload = EthBuiltPayload; fn validate_version_specific_fields( chain_spec: &ChainSpec, version: EngineApiMessageVersion, payload_or_attrs: PayloadOrAttributes<'_, CustomPayloadAttributes>, ) -> Result<(), AttributesValidationError> { validate_version_specific_fields(chain_spec, version, payload_or_attrs) } } #[tokio::main] async fn main() -> eyre::Result<()> { // this launches a test node with http let rpc_args = RpcServerArgs::default().with_http(); // 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(); let genesis_hash = spec.genesis_hash(); // create node config let node_config = NodeConfig::test().with_rpc(rpc_args).with_chain(spec); let (handle, _manager) = spawn_node(node_config).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(); // 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 = >::fork_choice_updated_v2( &client, forkchoice_state, Some(payload_attributes), ) .await; assert!(res.is_ok()); Ok(()) }