diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index cff3ec64e..7d01b0030 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -36,12 +36,20 @@ jobs: - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - - name: Run tests + - if: matrix.network == 'ethereum' + name: Run tests run: | cargo nextest run \ --locked --features "asm-keccak ${{ matrix.network }}" \ --workspace --exclude examples --exclude ef-tests \ -E "kind(test)" + - if: matrix.network == 'optimism' + name: Run tests + run: | + cargo nextest run \ + --locked --features "asm-keccak ${{ matrix.network }}" \ + --workspace --exclude examples --exclude ef-tests node-e2e-tests \ + -E "kind(test)" sync: name: sync / 100k blocks diff --git a/Cargo.lock b/Cargo.lock index cfdea68fe..72861c1bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4299,10 +4299,12 @@ version = "0.0.0" dependencies = [ "eyre", "futures-util", + "rand 0.8.5", "reth", "reth-node-core", "reth-node-ethereum", "reth-primitives", + "secp256k1 0.27.0", "serde_json", "tokio", ] diff --git a/crates/node-e2e-tests/Cargo.toml b/crates/node-e2e-tests/Cargo.toml index 6210eaa02..9072d4ce1 100644 --- a/crates/node-e2e-tests/Cargo.toml +++ b/crates/node-e2e-tests/Cargo.toml @@ -16,3 +16,5 @@ futures-util.workspace = true eyre.workspace = true tokio.workspace = true serde_json.workspace = true +rand.workspace = true +secp256k1.workspace = true diff --git a/crates/node-e2e-tests/tests/it/dev.rs b/crates/node-e2e-tests/tests/it/dev.rs index 923763038..ef579b1a7 100644 --- a/crates/node-e2e-tests/tests/it/dev.rs +++ b/crates/node-e2e-tests/tests/it/dev.rs @@ -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) diff --git a/crates/node-e2e-tests/tests/it/eth.rs b/crates/node-e2e-tests/tests/it/eth.rs new file mode 100644 index 000000000..633541508 --- /dev/null +++ b/crates/node-e2e-tests/tests/it/eth.rs @@ -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::::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::::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::::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) +} diff --git a/crates/node-e2e-tests/tests/it/main.rs b/crates/node-e2e-tests/tests/it/main.rs index 77e27a5dc..c84aeb0cf 100644 --- a/crates/node-e2e-tests/tests/it/main.rs +++ b/crates/node-e2e-tests/tests/it/main.rs @@ -1,3 +1,7 @@ mod dev; +mod eth; + +mod test_suite; + fn main() {} diff --git a/crates/node-e2e-tests/tests/it/test_suite.rs b/crates/node-e2e-tests/tests/it/test_suite.rs new file mode 100644 index 000000000..e35b2fc3a --- /dev/null +++ b/crates/node-e2e-tests/tests/it/test_suite.rs @@ -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, +} + +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 { + 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) + } +} diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index 7982946cb..345577545 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -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};