test: complete mine block test in e2e testsuite (#14849)

Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
This commit is contained in:
Federico Gimenez
2025-03-11 11:49:44 +01:00
committed by GitHub
parent 4ed233730e
commit eaa45abb98
12 changed files with 488 additions and 256 deletions

3
Cargo.lock generated
View File

@@ -7348,12 +7348,15 @@ dependencies = [
"reth-node-api",
"reth-node-builder",
"reth-node-core",
"reth-node-ethereum",
"reth-payload-builder",
"reth-payload-builder-primitives",
"reth-payload-primitives",
"reth-primitives",
"reth-primitives-traits",
"reth-provider",
"reth-rpc-api",
"reth-rpc-builder",
"reth-rpc-eth-api",
"reth-rpc-layer",
"reth-rpc-server-types",

View File

@@ -33,6 +33,9 @@ reth-network-peers.workspace = true
reth-engine-local.workspace = true
reth-tasks.workspace = true
reth-primitives.workspace = true
reth-node-ethereum.workspace = true
reth-primitives-traits.workspace = true
reth-rpc-builder.workspace = true
revm.workspace = true

View File

@@ -1,7 +1,7 @@
//! Utilities for end-to-end tests.
use node::NodeTestContext;
use reth_chainspec::EthChainSpec;
use reth_chainspec::{ChainSpec, EthChainSpec};
use reth_db::{test_utils::TempDatabase, DatabaseEnv};
use reth_engine_local::LocalPayloadAttributesBuilder;
use reth_network_api::test_utils::PeersHandleProvider;
@@ -9,8 +9,8 @@ use reth_node_builder::{
components::NodeComponentsBuilder,
rpc::{EngineValidatorAddOn, RethRpcAddOns},
EngineNodeLauncher, FullNodeTypesAdapter, Node, NodeAdapter, NodeBuilder, NodeComponents,
NodeConfig, NodeHandle, NodeTypesWithDBAdapter, NodeTypesWithEngine, PayloadAttributesBuilder,
PayloadTypes,
NodeConfig, NodeHandle, NodePrimitives, NodeTypesWithDBAdapter, NodeTypesWithEngine,
PayloadAttributesBuilder, PayloadTypes,
};
use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs};
use reth_provider::providers::{BlockchainProvider, NodeTypesForProvider};
@@ -44,7 +44,7 @@ pub async fn setup<N>(
num_nodes: usize,
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
attributes_generator: impl Fn(u64) -> <<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadBuilderAttributes + Copy + 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(Vec<NodeHelperType<N>>, TaskManager, Wallet)>
where
N: Default + Node<TmpNodeAdapter<N>> + NodeTypesForProvider + NodeTypesWithEngine,
@@ -108,29 +108,16 @@ pub async fn setup_engine<N>(
num_nodes: usize,
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
attributes_generator: impl Fn(u64) -> <<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadBuilderAttributes + Copy + 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
TaskManager,
Wallet,
)>
where
N: Default
+ Node<TmpNodeAdapter<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>
+ NodeTypesWithEngine
+ NodeTypesForProvider,
N::ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>,
Network: PeersHandleProvider,
>,
>,
N::AddOns: RethRpcAddOns<Adapter<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>
+ EngineValidatorAddOn<Adapter<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
LocalPayloadAttributesBuilder<N::ChainSpec>: PayloadAttributesBuilder<
<<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadAttributes,
>,
N: NodeBuilderHelper,
LocalPayloadAttributesBuilder<N::ChainSpec>:
PayloadAttributesBuilder<<N::Engine as PayloadTypes>::PayloadAttributes>,
{
let tasks = TaskManager::current();
let exec = tasks.executor();
@@ -214,3 +201,74 @@ pub type Adapter<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<N, TmpD
/// Type alias for a type of `NodeHelper`
pub type NodeHelperType<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>> =
NodeTestContext<Adapter<N, Provider>, <N as Node<TmpNodeAdapter<N, Provider>>>::AddOns>;
/// Helper trait to simplify bounds when calling setup functions.
pub trait NodeBuilderHelper
where
Self: Default
+ NodeTypesForProvider
+ NodeTypesWithEngine<
Engine: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Primitives: NodePrimitives<
BlockHeader = alloy_consensus::Header,
BlockBody = alloy_consensus::BlockBody<
<Self::Primitives as NodePrimitives>::SignedTx,
>,
>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider,
>,
>,
AddOns: RethRpcAddOns<
Adapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
> + EngineValidatorAddOn<
Adapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
>,
ChainSpec: From<ChainSpec> + Clone,
>,
LocalPayloadAttributesBuilder<Self::ChainSpec>:
PayloadAttributesBuilder<<Self::Engine as PayloadTypes>::PayloadAttributes>,
{
}
impl<T> NodeBuilderHelper for T
where
Self: Default
+ NodeTypesForProvider
+ NodeTypesWithEngine<
Engine: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Primitives: NodePrimitives<
BlockHeader = alloy_consensus::Header,
BlockBody = alloy_consensus::BlockBody<
<Self::Primitives as NodePrimitives>::SignedTx,
>,
>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Components: NodeComponents<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
Network: PeersHandleProvider,
>,
>,
AddOns: RethRpcAddOns<
Adapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
> + EngineValidatorAddOn<
Adapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
>,
ChainSpec: From<ChainSpec> + Clone,
>,
LocalPayloadAttributesBuilder<Self::ChainSpec>:
PayloadAttributesBuilder<<Self::Engine as PayloadTypes>::PayloadAttributes>,
{
}

View File

@@ -6,6 +6,7 @@ use alloy_rpc_types_engine::ForkchoiceState;
use alloy_rpc_types_eth::BlockNumberOrTag;
use eyre::Ok;
use futures_util::Future;
use jsonrpsee::http_client::{transport::HttpBackend, HttpClient};
use reth_chainspec::EthereumHardforks;
use reth_network_api::test_utils::PeersHandleProvider;
use reth_node_api::{
@@ -20,6 +21,7 @@ use reth_provider::{
StageCheckpointReader,
};
use reth_rpc_eth_api::helpers::{EthApiSpec, EthTransactions, TraceExt};
use reth_rpc_layer::AuthClientService;
use reth_stages_types::StageId;
use std::pin::Pin;
use tokio_stream::StreamExt;
@@ -55,7 +57,7 @@ where
/// Creates a new test node
pub async fn new(
node: FullNode<Node, AddOns>,
attributes_generator: impl Fn(u64) -> Engine::PayloadBuilderAttributes + 'static,
attributes_generator: impl Fn(u64) -> Engine::PayloadBuilderAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
Ok(Self {
inner: node.clone(),
@@ -293,4 +295,14 @@ where
let addr = self.inner.rpc_server_handle().http_local_addr().unwrap();
format!("http://{}", addr).parse().unwrap()
}
/// Returns an RPC client.
pub fn rpc_client(&self) -> Option<HttpClient> {
self.inner.rpc_server_handle().http_client()
}
/// Returns an Engine API client.
pub fn engine_api_client(&self) -> HttpClient<AuthClientService<HttpBackend>> {
self.inner.auth_server_handle().http_client()
}
}

View File

@@ -12,14 +12,14 @@ pub struct PayloadTestContext<T: PayloadTypes> {
payload_builder: PayloadBuilderHandle<T>,
pub timestamp: u64,
#[debug(skip)]
attributes_generator: Box<dyn Fn(u64) -> T::PayloadBuilderAttributes>,
attributes_generator: Box<dyn Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync>,
}
impl<T: PayloadTypes> PayloadTestContext<T> {
/// Creates a new payload helper
pub async fn new(
payload_builder: PayloadBuilderHandle<T>,
attributes_generator: impl Fn(u64) -> T::PayloadBuilderAttributes + 'static,
attributes_generator: impl Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
let payload_events = payload_builder.subscribe().await?;
let payload_event_stream = payload_events.into_stream();

View File

@@ -2,11 +2,17 @@
use crate::testsuite::Environment;
use alloy_eips::BlockId;
use alloy_primitives::{BlockNumber, B256};
use alloy_rpc_types_engine::{ExecutionPayload, PayloadAttributes};
use alloy_primitives::{Address, BlockNumber, Bytes, B256};
use alloy_rpc_types_engine::{
ExecutionPayload, ForkchoiceState, PayloadAttributes, PayloadStatusEnum,
};
use alloy_rpc_types_eth::{Block, Header, Receipt, Transaction};
use eyre::Result;
use futures_util::future::BoxFuture;
use std::future::Future;
use reth_node_api::EngineTypes;
use reth_rpc_api::clients::{EngineApiClient, EthApiClient};
use std::{future::Future, marker::PhantomData};
use tracing::debug;
/// An action that can be performed on an instance.
///
@@ -40,7 +46,6 @@ impl<I: 'static> ActionBox<I> {
/// This allows using closures directly as actions with `.with_action(async move |env| {...})`.
impl<I, F, Fut> Action<I> for F
where
I: Send + Sync + 'static,
F: FnMut(&Environment<I>) -> Fut + Send + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
@@ -58,10 +63,7 @@ pub struct MineBlock {
pub transactions: Vec<Vec<u8>>,
}
impl<I> Action<I> for MineBlock
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for MineBlock {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
// 1. Create a new payload with the given transactions
@@ -76,41 +78,102 @@ where
/// Mine a single block with the given transactions and verify the block was created
/// successfully.
#[derive(Debug)]
pub struct AssertMineBlock {
pub struct AssertMineBlock<Engine> {
/// The node index to mine
pub node_idx: usize,
/// Transactions to include in the block
pub transactions: Vec<Vec<u8>>,
pub transactions: Vec<Bytes>,
/// Expected block hash (optional)
pub expected_hash: Option<B256>,
/// Tracks engine type
_phantom: PhantomData<Engine>,
}
impl<I> Action<I> for AssertMineBlock
impl<Engine> AssertMineBlock<Engine> {
/// Create a new `AssertMineBlock` action
pub fn new(node_idx: usize, transactions: Vec<Bytes>, expected_hash: Option<B256>) -> Self {
Self { node_idx, transactions, expected_hash, _phantom: Default::default() }
}
}
impl<Engine> Action<Engine> for AssertMineBlock<Engine>
where
I: Send + Sync + 'static,
Engine: EngineTypes,
Engine::PayloadAttributes: From<PayloadAttributes>,
{
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
fn execute<'a>(&'a mut self, env: &'a Environment<Engine>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
// 1. Create a new payload with the given transactions
// 2. Execute forkchoiceUpdated with the new payload
// 3. Verify the block was created successfully
// 4. If expected_hash is provided, verify the block hash matches
/*
* Example assertion code (would actually fetch the real hash):
if let Some(expected_hash) = self.expected_hash {
let actual_hash = B256::ZERO;
if actual_hash != expected_hash {
return Err(eyre!(
"Block hash mismatch: expected {}, got {}",
expected_hash,
actual_hash
));
}
if self.node_idx >= env.node_clients.len() {
return Err(eyre::eyre!("Node index out of bounds: {}", self.node_idx));
}
*/
Ok(())
let node_client = &env.node_clients[self.node_idx];
let rpc_client = &node_client.rpc;
let engine_client = &node_client.engine;
// get the latest block to use as parent
let latest_block =
EthApiClient::<Transaction, Block, Receipt, Header>::block_by_number(
rpc_client,
alloy_eips::BlockNumberOrTag::Latest,
false,
)
.await?;
let latest_block = latest_block.ok_or_else(|| eyre::eyre!("Latest block not found"))?;
let parent_hash = latest_block.header.hash;
debug!("Latest block hash: {parent_hash}");
// create a simple forkchoice state with the latest block as head
let fork_choice_state = ForkchoiceState {
head_block_hash: parent_hash,
safe_block_hash: parent_hash,
finalized_block_hash: parent_hash,
};
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// create payload attributes for the new block
let payload_attributes = PayloadAttributes {
timestamp,
prev_randao: B256::random(),
suggested_fee_recipient: Address::random(),
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
let engine_payload_attributes: Engine::PayloadAttributes = payload_attributes.into();
let fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
engine_client,
fork_choice_state,
Some(engine_payload_attributes),
)
.await?;
debug!("FCU result: {:?}", fcu_result);
// check if we got a valid payload ID
match fcu_result.payload_status.status {
PayloadStatusEnum::Valid => {
if let Some(payload_id) = fcu_result.payload_id {
debug!("Got payload ID: {payload_id}");
// get the payload that was built
let _engine_payload =
EngineApiClient::<Engine>::get_payload_v3(engine_client, payload_id)
.await?;
Ok(())
} else {
Err(eyre::eyre!("No payload ID returned from forkchoiceUpdated"))
}
}
_ => Err(eyre::eyre!("Payload status not valid: {:?}", fcu_result.payload_status)),
}
})
}
}
@@ -121,15 +184,30 @@ pub struct SubmitTransaction {
/// The node index to submit to
pub node_idx: usize,
/// The raw transaction bytes
pub raw_tx: Vec<u8>,
pub raw_tx: Bytes,
}
impl<I> Action<I> for SubmitTransaction
where
I: Send + Sync + 'static,
{
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
impl<I: Sync> Action<I> for SubmitTransaction {
fn execute<'a>(&'a mut self, env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
if self.node_idx >= env.node_clients.len() {
return Err(eyre::eyre!("Node index out of bounds: {}", self.node_idx));
}
let node_client = &env.node_clients[self.node_idx];
let rpc_client = &node_client.rpc;
let tx_hash =
EthApiClient::<Transaction, Block, Receipt, Header>::send_raw_transaction(
rpc_client,
self.raw_tx.clone(),
)
.await?;
debug!("Transaction submitted with hash: {}", tx_hash);
Ok(())
})
}
}
@@ -142,30 +220,7 @@ pub struct CreatePayload {
pub attributes: PayloadAttributes,
}
impl<I> Action<I> for CreatePayload
where
I: Send + Sync + 'static,
{
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
}
/// Execute forkchoiceUpdated with the given state.
#[derive(Debug)]
pub struct ForkchoiceUpdated {
/// The node index to use
pub node_idx: usize,
/// Forkchoice state (head, safe, finalized block hashes)
pub state: (B256, B256, B256),
/// Payload attributes (optional)
pub attributes: Option<PayloadAttributes>,
}
impl<I> Action<I> for ForkchoiceUpdated
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for CreatePayload {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -180,10 +235,7 @@ pub struct NewPayload {
pub payload: ExecutionPayload,
}
impl<I> Action<I> for NewPayload
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for NewPayload {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -200,10 +252,7 @@ pub struct GetBlock {
pub block_hash: Option<B256>,
}
impl<I> Action<I> for GetBlock
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for GetBlock {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -220,10 +269,7 @@ pub struct GetTransactionCount {
pub block_id: BlockId,
}
impl<I> Action<I> for GetTransactionCount
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for GetTransactionCount {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -242,10 +288,7 @@ pub struct GetStorageAt {
pub block_id: BlockId,
}
impl<I> Action<I> for GetStorageAt
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for GetStorageAt {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -262,10 +305,7 @@ pub struct Call {
pub block_id: BlockId,
}
impl<I> Action<I> for Call
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for Call {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -280,10 +320,7 @@ pub struct WaitForBlocks {
pub blocks: u64,
}
impl<I> Action<I> for WaitForBlocks
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for WaitForBlocks {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
@@ -300,10 +337,7 @@ pub struct ChainReorg {
pub new_blocks: Vec<Vec<u8>>,
}
impl<I> Action<I> for ChainReorg
where
I: Send + Sync + 'static,
{
impl<I> Action<I> for ChainReorg {
fn execute<'a>(&'a mut self, _env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
// 1. Reorg the chain to the specified depth
@@ -328,16 +362,14 @@ impl<I> Sequence<I> {
}
}
impl<I> Action<I> for Sequence<I>
where
I: Send + Sync + 'static,
{
impl<I: Sync + 'static> Action<I> for Sequence<I> {
fn execute<'a>(&'a mut self, env: &'a Environment<I>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
// Execute each action in sequence
for action in &mut self.actions {
action.execute(env).await?;
}
futures_util::future::try_join_all(
self.actions.iter_mut().map(|action| action.execute(env)),
)
.await?;
Ok(())
})

View File

@@ -0,0 +1 @@
{"config":{"chainId":1,"homesteadBlock":0,"daoForkSupport":true,"eip150Block":0,"eip155Block":0,"eip158Block":0,"byzantiumBlock":0,"constantinopleBlock":0,"petersburgBlock":0,"istanbulBlock":0,"muirGlacierBlock":0,"berlinBlock":0,"londonBlock":0,"arrowGlacierBlock":0,"grayGlacierBlock":0,"shanghaiTime":0,"cancunTime":0,"terminalTotalDifficulty":"0x0","terminalTotalDifficultyPassed":true},"nonce":"0x0","timestamp":"0x0","extraData":"0x00","gasLimit":"0x1c9c380","difficulty":"0x0","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","coinbase":"0x0000000000000000000000000000000000000000","alloc":{"0x14dc79964da2c08b23698b3d3cc7ca32193d9955":{"balance":"0xd3c21bcecceda1000000"},"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65":{"balance":"0xd3c21bcecceda1000000"},"0x1cbd3b2770909d4e10f157cabc84c7264073c9ec":{"balance":"0xd3c21bcecceda1000000"},"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f":{"balance":"0xd3c21bcecceda1000000"},"0x2546bcd3c84621e976d8185a91a922ae77ecec30":{"balance":"0xd3c21bcecceda1000000"},"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc":{"balance":"0xd3c21bcecceda1000000"},"0x70997970c51812dc3a010c7d01b50e0d17dc79c8":{"balance":"0xd3c21bcecceda1000000"},"0x71be63f3384f5fb98995898a86b02fb2426c5788":{"balance":"0xd3c21bcecceda1000000"},"0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199":{"balance":"0xd3c21bcecceda1000000"},"0x90f79bf6eb2c4f870365e785982e1f101e93b906":{"balance":"0xd3c21bcecceda1000000"},"0x976ea74026e726554db657fa54763abd0c3a0aa9":{"balance":"0xd3c21bcecceda1000000"},"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc":{"balance":"0xd3c21bcecceda1000000"},"0x9c41de96b2088cdc640c6182dfcf5491dc574a57":{"balance":"0xd3c21bcecceda1000000"},"0xa0ee7a142d267c1f36714e4a8f75612f20a79720":{"balance":"0xd3c21bcecceda1000000"},"0xbcd4042de499d14e55001ccbb24a551f3b954096":{"balance":"0xd3c21bcecceda1000000"},"0xbda5747bfd65f08deb54cb465eb87d40e51b197e":{"balance":"0xd3c21bcecceda1000000"},"0xcd3b766ccdd6ae721141f452c550ca635964ce71":{"balance":"0xd3c21bcecceda1000000"},"0xdd2fd4581271e230360230f9337d5c0430bf44c0":{"balance":"0xd3c21bcecceda1000000"},"0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097":{"balance":"0xd3c21bcecceda1000000"},"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266":{"balance":"0xd3c21bcecceda1000000"},"0xfabb0ac9d68b0b445fb7357272ff202c5651694a":{"balance":"0xd3c21bcecceda1000000"}},"number":"0x0"}

View File

@@ -1,87 +1,35 @@
//! Example tests using the test suite framework.
use crate::testsuite::{
actions::{
AssertMineBlock, Call, ForkchoiceUpdated, GetTransactionCount, MineBlock, SubmitTransaction,
},
actions::AssertMineBlock,
setup::{NetworkSetup, Setup},
TestBuilder,
};
use alloy_eips::{BlockId, BlockNumberOrTag};
use alloy_primitives::B256;
use eyre::Result;
use reth_chainspec::{ChainSpecBuilder, MAINNET};
use reth_node_ethereum::{EthEngineTypes, EthereumNode};
use std::sync::Arc;
#[tokio::test]
#[ignore = "empty testsuite impls"]
async fn test_testsuite_submit_transaction_and_advance_block() -> Result<()> {
async fn test_testsuite_assert_mine_block() -> Result<()> {
reth_tracing::init_test_tracing();
let setup = Setup::default()
.with_chain_spec(Arc::new(
ChainSpecBuilder::default().chain(MAINNET.chain).cancun_activated().build(),
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("assets/genesis.json")).unwrap())
.cancun_activated()
.build(),
))
.with_network(NetworkSetup::single_node());
let test = TestBuilder::new(())
let test = TestBuilder::new()
.with_setup(setup)
.with_action(SubmitTransaction { node_idx: 0, raw_tx: vec![] })
.with_action(AssertMineBlock {
node_idx: 0,
transactions: vec![],
expected_hash: Some(B256::ZERO),
});
.with_action(AssertMineBlock::<EthEngineTypes>::new(0, vec![], Some(B256::ZERO)));
test.run().await
}
#[tokio::test]
#[ignore = "empty testsuite impls"]
async fn test_testsuite_chain_reorg() -> Result<()> {
let setup = Setup::default()
.with_chain_spec(Arc::new(
ChainSpecBuilder::default().chain(MAINNET.chain).cancun_activated().build(),
))
.with_network(NetworkSetup::multi_node(2));
let test = TestBuilder::new(())
.with_setup(setup)
.with_action(MineBlock { node_idx: 0, transactions: vec![] })
.with_action(ForkchoiceUpdated {
node_idx: 1,
state: (
B256::ZERO, // head block hash
B256::ZERO, // safe block hash
B256::ZERO, // finalized block hash
),
attributes: None,
})
.with_action(GetTransactionCount {
node_idx: 1,
address: B256::ZERO,
block_id: BlockId::Number(BlockNumberOrTag::Latest),
});
test.run().await
}
#[tokio::test]
#[ignore = "empty testsuite impls"]
async fn test_testsuite_complex_scenario() -> Result<()> {
let setup = Setup::default()
.with_chain_spec(Arc::new(
ChainSpecBuilder::default().chain(MAINNET.chain).cancun_activated().build(),
))
.with_network(NetworkSetup::multi_node(3));
let test = TestBuilder::new(())
.with_setup(setup)
.with_action(SubmitTransaction { node_idx: 0, raw_tx: vec![] })
.with_action(MineBlock { node_idx: 0, transactions: vec![] })
.with_action(Call {
node_idx: 1,
request: Default::default(),
block_id: BlockId::Number(BlockNumberOrTag::Latest),
});
test.run().await
test.run::<EthereumNode>().await?;
Ok(())
}

View File

@@ -1,8 +1,16 @@
//! Utilities for running e2e tests against a node or a network of nodes.
use actions::{Action, ActionBox};
use crate::{
testsuite::actions::{Action, ActionBox},
NodeBuilderHelper, PayloadAttributesBuilder,
};
use eyre::Result;
use jsonrpsee::http_client::{transport::HttpBackend, HttpClient};
use reth_engine_local::LocalPayloadAttributesBuilder;
use reth_node_api::{NodeTypesWithEngine, PayloadTypes};
use reth_rpc_layer::AuthClientService;
use setup::Setup;
use std::marker::PhantomData;
pub mod actions;
pub mod setup;
@@ -10,71 +18,47 @@ pub mod setup;
#[cfg(test)]
mod examples;
/// A runner performs operations on an environment.
/// Client handles for both regular RPC and Engine API endpoints
#[derive(Debug)]
pub struct Runner<I> {
/// The environment containing the node(s) to test
env: Environment<I>,
}
impl<I: 'static> Runner<I> {
/// Create a new test runner with the given environment
pub fn new(instance: I) -> Self {
Self { env: Environment { instance, ctx: () } }
}
/// Execute an action
pub async fn execute(&mut self, action: ActionBox<I>) -> Result<()> {
action.execute(&self.env).await
}
/// Execute a sequence of actions
pub async fn run_actions(&mut self, actions: Vec<ActionBox<I>>) -> Result<()> {
for action in actions {
self.execute(action).await?;
}
Ok(())
}
/// Run a complete test scenario with setup and actions
pub async fn run_scenario(
&mut self,
setup: Option<Setup>,
actions: Vec<ActionBox<I>>,
) -> Result<()> {
if let Some(setup) = setup {
setup.apply(&mut self.env).await?;
}
self.run_actions(actions).await
}
pub struct NodeClient {
/// Regular JSON-RPC client
pub rpc: HttpClient,
/// Engine API client
pub engine: HttpClient<AuthClientService<HttpBackend>>,
}
/// Represents a test environment.
#[derive(Debug)]
pub struct Environment<I> {
/// The instance against which we can run tests.
pub instance: I,
/// Context.
pub ctx: (),
/// Combined clients with both RPC and Engine API endpoints
pub node_clients: Vec<NodeClient>,
/// Tracks instance generic.
_phantom: PhantomData<I>,
}
impl<I> Default for Environment<I> {
fn default() -> Self {
Self { node_clients: vec![], _phantom: Default::default() }
}
}
/// Builder for creating test scenarios
#[allow(missing_debug_implementations)]
#[derive(Default)]
pub struct TestBuilder<I> {
instance: I,
setup: Option<Setup>,
setup: Option<Setup<I>>,
actions: Vec<ActionBox<I>>,
env: Environment<I>,
}
impl<I: 'static> TestBuilder<I> {
/// Create a new test builder
pub fn new(instance: I) -> Self {
Self { instance, setup: None, actions: Vec::new() }
pub fn new() -> Self {
Self { setup: None, actions: Vec::new(), env: Default::default() }
}
/// Set the test setup
pub fn with_setup(mut self, setup: Setup) -> Self {
pub fn with_setup(mut self, setup: Setup<I>) -> Self {
self.setup = Some(setup);
self
}
@@ -84,13 +68,44 @@ impl<I: 'static> TestBuilder<I> {
where
A: Action<I>,
{
self.actions.push(ActionBox::new(action));
self.actions.push(ActionBox::<I>::new(action));
self
}
/// Add multiple actions to the test
pub fn with_actions<II, A>(mut self, actions: II) -> Self
where
II: IntoIterator<Item = A>,
A: Action<I>,
{
self.actions.extend(actions.into_iter().map(ActionBox::new));
self
}
/// Run the test scenario
pub async fn run(self) -> Result<()> {
let mut runner = Runner::new(self.instance);
runner.run_scenario(self.setup, self.actions).await
pub async fn run<N>(mut self) -> Result<()>
where
N: NodeBuilderHelper,
LocalPayloadAttributesBuilder<N::ChainSpec>: PayloadAttributesBuilder<
<<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadAttributes,
>,
{
let mut setup = self.setup.take();
if let Some(ref mut s) = setup {
s.apply::<N>(&mut self.env).await?;
}
let actions = std::mem::take(&mut self.actions);
for action in actions {
action.execute(&self.env).await?;
}
// explicitly drop the setup to shutdown the nodes
// after all actions have completed
drop(setup);
Ok(())
}
}

View File

@@ -1,16 +1,29 @@
//! Test setup utilities for configuring the initial state.
use crate::testsuite::Environment;
use eyre::Result;
use crate::{setup_engine, testsuite::Environment, NodeBuilderHelper, PayloadAttributesBuilder};
use alloy_eips::BlockNumberOrTag;
use alloy_primitives::B256;
use alloy_rpc_types_engine::PayloadAttributes;
use alloy_rpc_types_eth::{Block as RpcBlock, Header, Receipt, Transaction};
use eyre::{eyre, Result};
use reth_chainspec::ChainSpec;
use reth_engine_local::LocalPayloadAttributesBuilder;
use reth_node_api::{NodeTypesWithEngine, PayloadTypes};
use reth_node_core::primitives::RecoveredBlock;
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_primitives::Block;
use reth_rpc_api::clients::EthApiClient;
use revm::state::EvmState;
use std::sync::Arc;
use std::{marker::PhantomData, sync::Arc};
use tokio::{
sync::mpsc,
time::{sleep, Duration},
};
use tracing::{debug, error};
/// Configuration for setting up a test environment
#[derive(Debug, Default)]
pub struct Setup {
/// Configuration for setting upa test environment
#[derive(Debug)]
pub struct Setup<I> {
/// Chain specification to use
pub chain_spec: Option<Arc<ChainSpec>>,
/// Genesis block to use
@@ -21,9 +34,39 @@ pub struct Setup {
pub state: Option<EvmState>,
/// Network configuration
pub network: NetworkSetup,
/// Shutdown channel to stop nodes when setup is dropped
shutdown_tx: Option<mpsc::Sender<()>>,
/// Is this setup in dev mode
pub is_dev: bool,
/// Tracks instance generic.
_phantom: PhantomData<I>,
}
impl Setup {
impl<I> Default for Setup<I> {
fn default() -> Self {
Self {
chain_spec: None,
genesis: None,
blocks: Vec::new(),
state: None,
network: NetworkSetup::default(),
shutdown_tx: None,
is_dev: true,
_phantom: Default::default(),
}
}
}
impl<I> Drop for Setup<I> {
fn drop(&mut self) {
// Send shutdown signal if the channel exists
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.try_send(());
}
}
}
impl<I> Setup<I> {
/// Create a new setup with default values
pub fn new() -> Self {
Self::default()
@@ -65,15 +108,120 @@ impl Setup {
self
}
/// Set dev mode
pub fn with_dev_mode(mut self, is_dev: bool) -> Self {
self.is_dev = is_dev;
self
}
/// Apply the setup to the environment
pub async fn apply<I>(&self, _env: &mut Environment<I>) -> Result<()> {
// Apply chain spec, genesis, blocks, state, and network configuration
// This would involve setting up the node(s) with the specified configuration
// and ensuring it's in the desired state before running tests
pub async fn apply<N>(&mut self, env: &mut Environment<I>) -> Result<()>
where
N: NodeBuilderHelper,
LocalPayloadAttributesBuilder<N::ChainSpec>: PayloadAttributesBuilder<
<<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadAttributes,
>,
{
let chain_spec =
self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
// For each block in self.blocks, replay it on the node
let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1);
// Set up the network connections if multiple nodes
self.shutdown_tx = Some(shutdown_tx);
let is_dev = self.is_dev;
let node_count = self.network.node_count;
let attributes_generator = move |timestamp| {
let attributes = PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
<<N as NodeTypesWithEngine>::Engine as PayloadTypes>::PayloadBuilderAttributes::from(
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
)
};
let result = setup_engine::<N>(
node_count,
Arc::<N::ChainSpec>::new((*chain_spec).clone().into()),
is_dev,
attributes_generator,
)
.await;
let mut node_clients = Vec::new();
match result {
Ok((nodes, executor, _wallet)) => {
// create HTTP clients for each node's RPC and Engine API endpoints
for node in &nodes {
let rpc = node
.rpc_client()
.ok_or_else(|| eyre!("Failed to create HTTP RPC client for node"))?;
let engine = node.engine_api_client();
node_clients.push(crate::testsuite::NodeClient { rpc, engine });
}
// spawn a separate task just to handle the shutdown
tokio::spawn(async move {
// keep nodes and executor in scope to ensure they're not dropped
let _nodes = nodes;
let _executor = executor;
// Wait for shutdown signal
let _ = shutdown_rx.recv().await;
// nodes and executor will be dropped here when the test completes
});
}
Err(e) => {
error!("Failed to setup nodes: {}", e);
return Err(eyre!("Failed to setup nodes: {}", e));
}
}
if node_clients.is_empty() {
return Err(eyre!("No nodes were created"));
}
// wait for all nodes to be ready to accept RPC requests before proceeding
for (idx, client) in node_clients.iter().enumerate() {
let mut retry_count = 0;
const MAX_RETRIES: usize = 5;
let mut last_error = None;
while retry_count < MAX_RETRIES {
match EthApiClient::<Transaction, RpcBlock, Receipt, Header>::block_by_number(
&client.rpc,
BlockNumberOrTag::Latest,
false,
)
.await
{
Ok(_) => {
debug!("Node {idx} RPC endpoint is ready");
break;
}
Err(e) => {
last_error = Some(e);
retry_count += 1;
debug!(
"Node {idx} RPC endpoint not ready, retry {retry_count}/{MAX_RETRIES}"
);
sleep(Duration::from_millis(500)).await;
}
}
}
if retry_count == MAX_RETRIES {
return Err(eyre!("Failed to connect to node {idx} RPC endpoint after {MAX_RETRIES} retries: {:?}", last_error));
}
}
env.node_clients = node_clients;
// TODO: For each block in self.blocks, replay it on the node
Ok(())
}
@@ -88,18 +236,16 @@ pub struct Genesis {}
pub struct NetworkSetup {
/// Number of nodes to create
pub node_count: usize,
/// Whether to disable discovery
pub disable_discovery: bool,
}
impl NetworkSetup {
/// Create a new network setup with a single node
pub fn single_node() -> Self {
Self { node_count: 1, disable_discovery: true }
Self { node_count: 1 }
}
/// Create a new network setup with multiple nodes
pub fn multi_node(count: usize) -> Self {
Self { node_count: count, disable_discovery: true }
Self { node_count: count }
}
}

View File

@@ -381,6 +381,12 @@ impl From<Genesis> for OpChainSpec {
}
}
impl From<ChainSpec> for OpChainSpec {
fn from(value: ChainSpec) -> Self {
Self { inner: value }
}
}
#[derive(Default, Debug)]
struct OpGenesisInfo {
optimism_chain_info: op_alloy_rpc_types::OpChainInfo,

View File

@@ -142,6 +142,14 @@ impl<T: Decodable2718 + Send + Sync + Debug> PayloadBuilderAttributes
}
}
impl<OpTransactionSigned> From<EthPayloadBuilderAttributes>
for OpPayloadBuilderAttributes<OpTransactionSigned>
{
fn from(value: EthPayloadBuilderAttributes) -> Self {
Self { payload_attributes: value, ..Default::default() }
}
}
/// Contains the built payload.
#[derive(Debug, Clone)]
pub struct OpBuiltPayload<N: NodePrimitives = OpPrimitives> {