mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
e2e eth node tests (#7075)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
@ -16,3 +16,5 @@ futures-util.workspace = true
|
||||
eyre.workspace = true
|
||||
tokio.workspace = true
|
||||
serde_json.workspace = true
|
||||
rand.workspace = true
|
||||
secp256k1.workspace = true
|
||||
|
||||
@ -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)
|
||||
|
||||
123
crates/node-e2e-tests/tests/it/eth.rs
Normal file
123
crates/node-e2e-tests/tests/it/eth.rs
Normal 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)
|
||||
}
|
||||
@ -1,3 +1,7 @@
|
||||
mod dev;
|
||||
|
||||
mod eth;
|
||||
|
||||
mod test_suite;
|
||||
|
||||
fn main() {}
|
||||
|
||||
131
crates/node-e2e-tests/tests/it/test_suite.rs
Normal file
131
crates/node-e2e-tests/tests/it/test_suite.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
|
||||
Reference in New Issue
Block a user