e2e eth node tests (#7075)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
Luca Provini
2024-04-01 07:56:26 -04:00
committed by GitHub
parent 51aea14484
commit 1b9a768754
8 changed files with 273 additions and 2 deletions

View File

@ -16,3 +16,5 @@ futures-util.workspace = true
eyre.workspace = true
tokio.workspace = true
serde_json.workspace = true
rand.workspace = true
secp256k1.workspace = true

View File

@ -17,7 +17,7 @@ async fn can_run_dev_node() -> eyre::Result<()> {
// create node config
let node_config = NodeConfig::test()
.dev()
.with_rpc(RpcServerArgs::default().with_http())
.with_rpc(RpcServerArgs::default().with_http().with_unused_ports())
.with_chain(custom_chain());
let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)

View File

@ -0,0 +1,123 @@
use crate::test_suite::TestSuite;
use futures_util::StreamExt;
use reth::{
builder::{NodeBuilder, NodeHandle},
payload::EthPayloadBuilderAttributes,
providers::{BlockReaderIdExt, CanonStateSubscriptions},
rpc::{
api::EngineApiClient,
eth::EthTransactions,
types::engine::{ExecutionPayloadEnvelopeV3, ForkchoiceState, PayloadAttributes},
},
tasks::TaskManager,
};
use reth_node_core::{args::RpcServerArgs, node_config::NodeConfig};
use reth_node_ethereum::{EthEngineTypes, EthereumNode};
use reth_primitives::{Address, BlockNumberOrTag, B256};
use std::time::{SystemTime, UNIX_EPOCH};
#[tokio::test]
async fn can_run_eth_node() -> eyre::Result<()> {
let tasks = TaskManager::current();
let test_suite = TestSuite::new();
// Node setup
let node_config = NodeConfig::test()
.with_chain(test_suite.chain_spec.clone())
.with_rpc(RpcServerArgs::default().with_http());
let NodeHandle { mut node, node_exit_future: _ } = NodeBuilder::new(node_config)
.testing_node(tasks.executor())
.node(EthereumNode::default())
.launch()
.await?;
// setup engine api events and payload service events
let mut notifications = node.provider.canonical_state_stream();
let payload_events = node.payload_builder.subscribe().await?;
// push tx into pool via RPC server
let eth_api = node.rpc_registry.eth_api();
let transfer_tx = test_suite.transfer_tx();
eth_api.send_raw_transaction(transfer_tx.envelope_encoded()).await?;
// trigger new payload building draining the pool
let eth_attr = eth_payload_attributes();
let payload_id = node.payload_builder.new_payload(eth_attr.clone()).await?;
// resolve best payload via engine api
let client = node.auth_server_handle().http_client();
EngineApiClient::<EthEngineTypes>::get_payload_v3(&client, payload_id).await?;
let mut payload_event_stream = payload_events.into_stream();
// first event is the payload attributes
let first_event = payload_event_stream.next().await.unwrap()?;
if let reth::payload::Events::Attributes(attr) = first_event {
assert_eq!(eth_attr.timestamp, attr.timestamp);
} else {
panic!("Expect first event as payload attributes.")
}
// second event is built payload
let second_event = payload_event_stream.next().await.unwrap()?;
if let reth::payload::Events::BuiltPayload(payload) = second_event {
// setup payload for submission
let envelope_v3 = ExecutionPayloadEnvelopeV3::from(payload);
let payload_v3 = envelope_v3.execution_payload;
// submit payload to engine api
let submission = EngineApiClient::<EthEngineTypes>::new_payload_v3(
&client,
payload_v3,
vec![],
eth_attr.parent_beacon_block_root.unwrap(),
)
.await?;
assert!(submission.is_valid());
// get latest valid hash from blockchain tree
let hash = submission.latest_valid_hash.unwrap();
// trigger forkchoice update via engine api to commit the block to the blockchain
let fcu = EngineApiClient::<EthEngineTypes>::fork_choice_updated_v2(
&client,
ForkchoiceState {
head_block_hash: hash,
safe_block_hash: hash,
finalized_block_hash: hash,
},
None,
)
.await?;
assert!(fcu.is_valid());
// get head block from notifications stream and verify the tx has been pushed to the pool
// is actually present in the canonical block
let head = notifications.next().await.unwrap();
let tx = head.tip().transactions().next().unwrap();
assert_eq!(tx.hash(), transfer_tx.hash);
// make sure the block hash we submitted via FCU engine api is the new latest block using an
// RPC call
let latest_block = node.provider.block_by_number_or_tag(BlockNumberOrTag::Latest)?.unwrap();
assert_eq!(latest_block.hash_slow(), hash);
} else {
panic!("Expect a built payload event.");
}
Ok(())
}
fn eth_payload_attributes() -> EthPayloadBuilderAttributes {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let attributes = PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
}

View File

@ -1,3 +1,7 @@
mod dev;
mod eth;
mod test_suite;
fn main() {}

View File

@ -0,0 +1,131 @@
use reth_primitives::{
keccak256, revm_primitives::fixed_bytes, sign_message, AccessList, Address, Bytes, ChainConfig,
ChainSpec, Genesis, GenesisAccount, Transaction, TransactionKind, TransactionSigned, TxEip1559,
B256, U256,
};
use secp256k1::{PublicKey, Secp256k1, SecretKey};
use std::{collections::BTreeMap, sync::Arc};
/// Helper struct to customize the chain spec during e2e tests
pub struct TestSuite {
pub account: Account,
pub chain_spec: Arc<ChainSpec>,
}
impl TestSuite {
/// Creates a new e2e test suit with a random account and a custom chain spec
pub fn new() -> Self {
let account = Account::new();
let chain_spec = TestSuite::chain_spec(&account);
Self { account, chain_spec }
}
/// Returns the raw transfer transaction
pub fn transfer_tx(&self) -> TransactionSigned {
self.account.transfer_tx()
}
/// Creates a custom chain spec and allocates the initial balance to the given account
fn chain_spec(account: &Account) -> Arc<ChainSpec> {
let sk = B256::from_slice(&account.secret_key.secret_bytes());
let mut alloc = BTreeMap::new();
let genesis_acc = GenesisAccount {
balance: U256::from(1_000_000_000_000_000_000_000_000u128),
code: None,
storage: None,
nonce: Some(0),
private_key: Some(sk),
};
alloc.insert(account.pubkey, genesis_acc);
let genesis = Genesis {
nonce: 0,
timestamp: 0,
extra_data: fixed_bytes!("00").into(),
gas_limit: 30_000_000,
difficulty: U256::from(0),
mix_hash: B256::ZERO,
coinbase: Address::ZERO,
alloc,
number: Some(0),
config: TestSuite::chain_config(),
base_fee_per_gas: None,
blob_gas_used: None,
excess_blob_gas: None,
};
Arc::new(genesis.into())
}
fn chain_config() -> ChainConfig {
let chain_config = r#"
{
"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": 0,
"terminalTotalDifficultyPassed": true
}
"#;
serde_json::from_str(chain_config).unwrap()
}
}
/// The main account used for the e2e tests
pub struct Account {
pubkey: Address,
secret_key: SecretKey,
}
impl Account {
/// Creates a new account from a random secret key and pub key
fn new() -> Self {
let (secret_key, pubkey) = Account::random();
Self { pubkey, secret_key }
}
/// Generates a random secret key and pub key
fn random() -> (SecretKey, Address) {
let secret_key = SecretKey::new(&mut rand::thread_rng());
let secp = Secp256k1::new();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let hash = keccak256(&public_key.serialize_uncompressed()[1..]);
let pubkey = Address::from_slice(&hash[12..]);
(secret_key, pubkey)
}
/// Creates a new transfer transaction
pub fn transfer_tx(&self) -> TransactionSigned {
let tx = Transaction::Eip1559(TxEip1559 {
chain_id: 1,
nonce: 0,
gas_limit: 21000,
to: TransactionKind::Call(Address::random()),
value: U256::from(1000),
input: Bytes::default(),
max_fee_per_gas: 875000000,
max_priority_fee_per_gas: 0,
access_list: AccessList::default(),
});
Account::sign_transaction(&self.secret_key, tx)
}
/// Helper function to sign a transaction
fn sign_transaction(secret_key: &SecretKey, transaction: Transaction) -> TransactionSigned {
let tx_signature_hash = transaction.signature_hash();
let signature =
sign_message(B256::from_slice(secret_key.as_ref()), tx_signature_hash).unwrap();
TransactionSigned::from_transaction_and_signature(transaction, signature)
}
}

View File

@ -114,6 +114,7 @@ pub mod noop;
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils;
pub use events::Events;
pub use payload::{EthBuiltPayload, EthPayloadBuilderAttributes};
pub use reth_rpc_types::engine::PayloadId;
pub use service::{PayloadBuilderHandle, PayloadBuilderService, PayloadStore};