Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom beacon withdrawals example #9497

Merged
merged 4 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ members = [
"examples/rpc-db/",
"examples/stateful-precompile/",
"examples/txpool-tracing/",
"examples/custom-beacon-withdrawals",
"testing/ef-tests/",
"testing/testing-utils",
]
Expand Down
26 changes: 26 additions & 0 deletions examples/custom-beacon-withdrawals/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "example-custom-beacon-withdrawals"
version = "0.0.0"
publish = false
edition.workspace = true
license.workspace = true

[dependencies]
reth.workspace = true
reth-node-ethereum.workspace = true
reth-evm-ethereum.workspace = true
reth-chainspec.workspace = true
reth-evm.workspace = true
reth-primitives.workspace = true

alloy-sol-macro = "0.8.9"
alloy-sol-types.workspace = true
alloy-eips.workspace = true
alloy-consensus.workspace = true

eyre.workspace = true

[features]
optimism = [
"reth-primitives/optimism"
]
286 changes: 286 additions & 0 deletions examples/custom-beacon-withdrawals/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
//! Example for how to modify a block post-execution step. It credits beacon withdrawals with a
//! custom mechanism instead of minting native tokens

#![cfg_attr(not(test), warn(unused_crate_dependencies))]

use alloy_eips::eip7685::Requests;
use alloy_sol_macro::sol;
use alloy_sol_types::SolCall;
#[cfg(feature = "optimism")]
use reth::revm::primitives::OptimismFields;
use reth::{
api::{ConfigureEvm, ConfigureEvmEnv, NodeTypesWithEngine},
builder::{components::ExecutorBuilder, BuilderContext, FullNodeTypes},
cli::Cli,
providers::ProviderError,
revm::{
interpreter::Host,
primitives::{Env, TransactTo, TxEnv},
Database, DatabaseCommit, Evm, State,
},
};
use reth_chainspec::{ChainSpec, EthereumHardforks};
use reth_evm::execute::{
BlockExecutionError, BlockExecutionStrategy, BlockExecutionStrategyFactory, ExecuteOutput,
InternalBlockExecutionError,
};
use reth_evm_ethereum::EthEvmConfig;
use reth_node_ethereum::{node::EthereumAddOns, BasicBlockExecutorProvider, EthereumNode};
use reth_primitives::{
revm_primitives::{
address, Address, BlockEnv, Bytes, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, U256,
},
BlockWithSenders, Receipt, Withdrawal,
};
use std::{fmt::Display, sync::Arc};

pub const SYSTEM_ADDRESS: Address = address!("fffffffffffffffffffffffffffffffffffffffe");
pub const WITHDRAWALS_ADDRESS: Address = address!("4200000000000000000000000000000000000000");

fn main() {
Cli::parse_args()
.run(|builder, _| async move {
let handle = builder
// use the default ethereum node types
.with_types::<EthereumNode>()
// Configure the components of the node
// use default ethereum components but use our custom pool
.with_components(
EthereumNode::components().executor(CustomExecutorBuilder::default()),
)
.with_add_ons(EthereumAddOns::default())
.launch()
.await?;

handle.wait_for_node_exit().await
})
.unwrap();
}

/// A custom executor builder
#[derive(Debug, Default, Clone, Copy)]
#[non_exhaustive]
pub struct CustomExecutorBuilder;

impl<Types, Node> ExecutorBuilder<Node> for CustomExecutorBuilder
where
Types: NodeTypesWithEngine<ChainSpec = ChainSpec>,
Node: FullNodeTypes<Types = Types>,
{
type EVM = EthEvmConfig;
type Executor = BasicBlockExecutorProvider<CustomExecutorStrategyFactory>;

async fn build_evm(
self,
ctx: &BuilderContext<Node>,
) -> eyre::Result<(Self::EVM, Self::Executor)> {
let chain_spec = ctx.chain_spec();
let evm_config = EthEvmConfig::new(ctx.chain_spec());
let strategy_factory =
CustomExecutorStrategyFactory { chain_spec, evm_config: evm_config.clone() };
let executor = BasicBlockExecutorProvider::new(strategy_factory);

Ok((evm_config, executor))
}
}

#[derive(Clone)]
pub struct CustomExecutorStrategyFactory {
/// The chainspec
chain_spec: Arc<ChainSpec>,
/// How to create an EVM.
evm_config: EthEvmConfig,
}

impl BlockExecutionStrategyFactory for CustomExecutorStrategyFactory {
type Strategy<DB: Database<Error: Into<ProviderError> + Display>> = CustomExecutorStrategy<DB>;

fn create_strategy<DB>(&self, db: DB) -> Self::Strategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
let state =
State::builder().with_database(db).with_bundle_update().without_state_clear().build();
CustomExecutorStrategy {
state,
chain_spec: self.chain_spec.clone(),
evm_config: self.evm_config.clone(),
}
}
}

pub struct CustomExecutorStrategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
/// The chainspec
chain_spec: Arc<ChainSpec>,
/// How to create an EVM.
evm_config: EthEvmConfig,
/// Current state for block execution.
state: State<DB>,
}

impl<DB> CustomExecutorStrategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
/// Configures a new evm configuration and block environment for the given block.
///
/// # Caution
///
/// This does not initialize the tx environment.
fn evm_env_for_block(
&self,
header: &alloy_consensus::Header,
total_difficulty: U256,
) -> EnvWithHandlerCfg {
let mut cfg = CfgEnvWithHandlerCfg::new(Default::default(), Default::default());
let mut block_env = BlockEnv::default();
self.evm_config.fill_cfg_and_block_env(&mut cfg, &mut block_env, header, total_difficulty);

EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, Default::default())
}
}

impl<DB> BlockExecutionStrategy<DB> for CustomExecutorStrategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
type Error = BlockExecutionError;

fn apply_pre_execution_changes(
&mut self,
block: &BlockWithSenders,
_total_difficulty: U256,
) -> Result<(), Self::Error> {
// Set state clear flag if the block is after the Spurious Dragon hardfork.
let state_clear_flag =
(*self.chain_spec).is_spurious_dragon_active_at_block(block.header.number);
self.state.set_state_clear_flag(state_clear_flag);

Ok(())
}

fn execute_transactions(
&mut self,
_block: &BlockWithSenders,
_total_difficulty: U256,
) -> Result<ExecuteOutput, Self::Error> {
Ok(ExecuteOutput { receipts: vec![], gas_used: 0 })
}

fn apply_post_execution_changes(
&mut self,
block: &BlockWithSenders,
total_difficulty: U256,
_receipts: &[Receipt],
) -> Result<Requests, Self::Error> {
let env = self.evm_env_for_block(&block.header, total_difficulty);
let mut evm = self.evm_config.evm_with_env(&mut self.state, env);

if let Some(withdrawals) = block.body.withdrawals.as_ref() {
apply_withdrawals_contract_call(withdrawals, &mut evm)?;
}

Ok(Requests::default())
}

fn state_ref(&self) -> &State<DB> {
&self.state
}

fn state_mut(&mut self) -> &mut State<DB> {
&mut self.state
}
}

sol!(
function withdrawals(
uint64[] calldata amounts,
address[] calldata addresses
);
);

/// Applies the post-block call to the withdrawal / deposit contract, using the given block,
/// [`ChainSpec`], EVM.
pub fn apply_withdrawals_contract_call<EXT, DB: Database + DatabaseCommit>(
withdrawals: &[Withdrawal],
evm: &mut Evm<'_, EXT, DB>,
) -> Result<(), BlockExecutionError>
where
DB::Error: std::fmt::Display,
{
// get previous env
let previous_env = Box::new(evm.context.env().clone());

// modify env for pre block call
fill_tx_env_with_system_contract_call(
&mut evm.context.evm.env,
SYSTEM_ADDRESS,
WITHDRAWALS_ADDRESS,
withdrawalsCall {
amounts: withdrawals.iter().map(|w| w.amount).collect::<Vec<_>>(),
addresses: withdrawals.iter().map(|w| w.address).collect::<Vec<_>>(),
}
.abi_encode()
.into(),
);

let mut state = match evm.transact() {
Ok(res) => res.state,
Err(e) => {
evm.context.evm.env = previous_env;
return Err(BlockExecutionError::Internal(InternalBlockExecutionError::Other(
format!("withdrawal contract system call revert: {}", e).into(),
)))
}
};

// Clean-up post system tx context
state.remove(&SYSTEM_ADDRESS);
state.remove(&evm.block().coinbase);
evm.context.evm.db.commit(state);
// re-set the previous env
evm.context.evm.env = previous_env;

Ok(())
}

fn fill_tx_env_with_system_contract_call(
env: &mut Env,
caller: Address,
contract: Address,
data: Bytes,
) {
env.tx = TxEnv {
caller,
transact_to: TransactTo::Call(contract),
// Explicitly set nonce to None so revm does not do any nonce checks
nonce: None,
gas_limit: 30_000_000,
value: U256::ZERO,
data,
// Setting the gas price to zero enforces that no value is transferred as part of the call,
// and that the call will not count against the block's gas limit
gas_price: U256::ZERO,
// The chain ID check is not relevant here and is disabled if set to None
chain_id: None,
// Setting the gas priority fee to None ensures the effective gas price is derived from the
// `gas_price` field, which we need to be zero
gas_priority_fee: None,
access_list: Vec::new(),
// blob fields can be None for this tx
blob_hashes: Vec::new(),
max_fee_per_blob_gas: None,
authorization_list: None,
#[cfg(feature = "optimism")]
optimism: OptimismFields::default(),
};

// ensure the block gas limit is >= the tx
env.block.gas_limit = U256::from(env.tx.gas_limit);

// disable the base fee check for this call by setting the base fee to zero
env.block.basefee = U256::ZERO;
}
Loading