Files
reth/examples/custom-node/src/main.rs
2024-02-17 10:39:02 +00:00

250 lines
8.2 KiB
Rust

//! 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<Withdrawal>> {
self.inner.withdrawals()
}
fn parent_beacon_block_root(&self) -> Option<B256> {
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<Self, Infallible> {
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<B256> {
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 = <HttpClient as EngineApiClient<CustomEngineTypes>>::fork_choice_updated_v2(
&client,
forkchoice_state,
Some(payload_attributes),
)
.await;
assert!(res.is_ok());
Ok(())
}