diff --git a/Cargo.lock b/Cargo.lock index 8b570cf2cd3..362a2ad5c32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7235,6 +7235,7 @@ dependencies = [ "reth-db", "reth-db-api", "reth-db-common", + "reth-discv5", "reth-downloaders", "reth-engine-util", "reth-errors", diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index ca31910c019..f903b957ba1 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -120,6 +120,8 @@ revm-primitives.workspace = true reth-db-common.workspace = true reth-db = { workspace = true, features = ["mdbx", "test-utils"] } serial_test.workspace = true +reth-discv5.workspace = true +reth-transaction-pool = { workspace = true, features = ["test-utils"] } [features] default = ["jemalloc"] diff --git a/bin/reth/tests/commands/bitfinity_node_it.rs b/bin/reth/tests/commands/bitfinity_node_it.rs index 8c9de89f861..10fa546407e 100644 --- a/bin/reth/tests/commands/bitfinity_node_it.rs +++ b/bin/reth/tests/commands/bitfinity_node_it.rs @@ -3,7 +3,7 @@ //! use super::utils::*; -use did::keccak; +use did::keccak::{self, keccak_hash}; use eth_server::{EthImpl, EthServer}; use ethereum_json_rpc_client::CertifiedResult; use ethereum_json_rpc_client::{reqwest::ReqwestClient, EthJsonRpcClient}; @@ -19,6 +19,7 @@ use reth::{ use reth_consensus::FullConsensus; use reth_db::DatabaseEnv; use reth_db::{init_db, test_utils::tempdir_path}; +use reth_discv5::discv5::enr::secp256k1::{Keypair, Secp256k1}; use reth_network::NetworkHandle; use reth_node_api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}; use reth_node_builder::components::Components; @@ -28,15 +29,18 @@ use reth_node_ethereum::node::EthereumEngineValidatorBuilder; use reth_node_ethereum::{ BasicBlockExecutorProvider, EthEvmConfig, EthExecutionStrategyFactory, EthereumNode, }; +use reth_primitives::{Transaction, TransactionSigned}; +use reth_primitives_traits::constants::MINIMUM_GAS_LIMIT; use reth_provider::providers::BlockchainProvider; use reth_rpc::EthApi; use reth_tasks::TaskManager; use reth_transaction_pool::blobstore::DiskFileBlobStore; +use reth_transaction_pool::test_utils::MockTransaction; use reth_transaction_pool::{ CoinbaseTipOrdering, EthPooledTransaction, EthTransactionValidator, Pool, TransactionValidationTaskExecutor, }; -use revm_primitives::{hex, Address, U256}; +use revm_primitives::{hex, Address, B256, U256}; use std::{net::SocketAddr, str::FromStr, sync::Arc}; #[tokio::test] @@ -179,26 +183,55 @@ async fn bitfinity_test_node_forward_ic_get_genesis_balances() { } #[tokio::test] -async fn bitfinity_test_node_forward_send_raw_transaction_requests() { +async fn bitfinity_test_node_validates_raw_transactions() { // Arrange let _log = init_logs(); let eth_server = EthImpl::new(); + let txs_list = eth_server.received_tx_hashes.clone(); let (_server, eth_server_address) = mock_eth_server_start(EthServer::into_rpc(eth_server)).await; let (reth_client, _reth_node) = start_reth_node(Some(format!("http://{}", eth_server_address)), None).await; - // Create a random transaction - let mut tx = [0u8; 256]; - rand::thread_rng().fill_bytes(&mut tx); - let expected_tx_hash = keccak::keccak_hash(format!("0x{}", hex::encode(tx)).as_bytes()); + // Create a transaction + let mock_tx = MockTransaction::legacy(); + + // Tx with incorrect data should be stopped by reth. + let tx = sign_tx_with_random_key_pair(mock_tx.clone().into()); + let raw_tx = alloy_rlp::encode(&tx); + let result = reth_client.send_raw_transaction_bytes(&raw_tx).await; + assert!(result.is_err()); + assert!(txs_list.read().await.is_empty()); + + // Tx with correct data should pass to the Eth. + let mut mock_tx = mock_tx.with_gas_limit(MINIMUM_GAS_LIMIT); + let eth_chain_id = reth_client.get_chain_id().await.unwrap(); + match &mut mock_tx { + MockTransaction::Legacy { chain_id, .. } => *chain_id = Some(eth_chain_id), + _ => unreachable!(), + }; + let tx = sign_tx_with_random_key_pair(mock_tx.clone().into()); + let raw_tx = alloy_rlp::encode(&tx); + let result = reth_client.send_raw_transaction_bytes(&raw_tx).await; + let expected_hash = keccak_hash(&raw_tx); + assert_eq!(txs_list.read().await.first().unwrap(), &expected_hash.0); + assert_eq!(result.unwrap(), expected_hash); +} - // Act - let result = reth_client.send_raw_transaction_bytes(&tx).await; +fn sign_tx_with_random_key_pair(tx: Transaction) -> TransactionSigned { + let secp = Secp256k1::new(); + let key_pair = Keypair::new(&secp, &mut rand::thread_rng()); + sign_tx_with_key_pair(key_pair, tx) +} - // Assert - assert_eq!(result.unwrap(), expected_tx_hash); +fn sign_tx_with_key_pair(key_pair: Keypair, tx: Transaction) -> TransactionSigned { + let signature = reth_primitives::sign_message( + B256::from_slice(&key_pair.secret_bytes()[..]), + tx.signature_hash(), + ) + .unwrap(); + TransactionSigned::new(tx, signature, Default::default()) } /// Start a local reth node @@ -207,7 +240,88 @@ async fn start_reth_node( import_data: Option, ) -> ( EthJsonRpcClient, - NodeHandle, BlockchainProvider>>>, Components, BlockchainProvider>>>, reth_network::EthNetworkPrimitives, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider, Arc>>, RpcAddOns, BlockchainProvider>>>, Components, BlockchainProvider>>>, reth_network::EthNetworkPrimitives, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider, Arc>>, EthApi>>, Pool>>, EthPooledTransaction>>, CoinbaseTipOrdering, DiskFileBlobStore>, NetworkHandle, EthEvmConfig>, EthereumEngineValidatorBuilder>>, + NodeHandle< + NodeAdapter< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + Components< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + reth_network::EthNetworkPrimitives, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + EthEvmConfig, + BasicBlockExecutorProvider, + Arc, + >, + >, + RpcAddOns< + NodeAdapter< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + Components< + FullNodeTypesAdapter< + EthereumNode, + Arc, + BlockchainProvider>>, + >, + reth_network::EthNetworkPrimitives, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + EthEvmConfig, + BasicBlockExecutorProvider, + Arc, + >, + >, + EthApi< + BlockchainProvider>>, + Pool< + TransactionValidationTaskExecutor< + EthTransactionValidator< + BlockchainProvider< + NodeTypesWithDBAdapter>, + >, + EthPooledTransaction, + >, + >, + CoinbaseTipOrdering, + DiskFileBlobStore, + >, + NetworkHandle, + EthEvmConfig, + >, + EthereumEngineValidatorBuilder, + >, + >, ) { let tasks = TaskManager::current(); @@ -273,11 +387,14 @@ async fn mock_eth_server_start(methods: impl Into) -> (ServerHandle, So /// Eth server mock for local testing pub mod eth_server { + use std::sync::Arc; + use alloy_rlp::Bytes; use did::keccak; use ethereum_json_rpc_client::CertifiedResult; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; - use revm_primitives::{Address, B256, U256}; + use revm_primitives::{hex, Address, B256, U256}; + use tokio::sync::RwLock; #[rpc(server, namespace = "eth")] pub trait Eth { @@ -311,12 +428,18 @@ pub mod eth_server { pub gas_price: u128, /// Current max priority fee per gas pub max_priority_fee_per_gas: u128, + /// List of received transactions hashes. + pub received_tx_hashes: Arc>>, } impl EthImpl { /// Create a new Eth server implementation pub fn new() -> Self { - Self { gas_price: rand::random(), max_priority_fee_per_gas: rand::random() } + Self { + gas_price: rand::random(), + max_priority_fee_per_gas: rand::random(), + received_tx_hashes: Default::default(), + } } } @@ -337,7 +460,9 @@ pub mod eth_server { } async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult { + let tx = hex::decode(&tx).unwrap(); let hash = keccak::keccak_hash(&tx); + self.received_tx_hashes.write().await.push(hash.0); Ok(hash.into()) } diff --git a/crates/rpc/rpc-eth-api/src/helpers/bitfinity_evm_rpc.rs b/crates/rpc/rpc-eth-api/src/helpers/bitfinity_evm_rpc.rs index dd18583d5bf..38d58aa2f07 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/bitfinity_evm_rpc.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/bitfinity_evm_rpc.rs @@ -2,13 +2,18 @@ use std::sync::Arc; +use alloy_consensus::Transaction; +use alloy_rlp::Decodable; use did::{Block, H256}; use ethereum_json_rpc_client::CertifiedResult; use ethereum_json_rpc_client::{reqwest::ReqwestClient, EthJsonRpcClient}; use futures::Future; use jsonrpsee::core::RpcResult; -use reth_chainspec::ChainSpec; -use reth_rpc_server_types::result::internal_rpc_err; +use reth_chainspec::{ChainSpec, EthChainSpec}; +use reth_primitives::TransactionSigned; +use reth_primitives_traits::constants::MINIMUM_GAS_LIMIT; +use reth_primitives_traits::SignedTransaction; +use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err}; use revm_primitives::{Address, Bytes, B256, U256}; /// Proxy to the Bitfinity EVM RPC. @@ -54,6 +59,14 @@ pub trait BitfinityEvmRpc { fn send_raw_transaction(&self, tx: Bytes) -> impl Future> + Send { let chain_spec = self.chain_spec(); async move { + let typed_tx = TransactionSigned::decode(&mut tx.as_ref()).map_err(|e| { + invalid_params_rpc_err(format!( + "failed to decode eth_sendRawTransaction input {tx}: {e}" + )) + })?; + + validate_raw_transaction(&typed_tx, &chain_spec)?; + let (rpc_url, client) = get_client(&chain_spec)?; let tx_hash = client.send_raw_transaction_bytes(&tx).await.map_err(|e| { @@ -116,3 +129,38 @@ fn get_client(chain_spec: &ChainSpec) -> RpcResult<(&String, EthJsonRpcClient RpcResult<()> { + // Check chain id + if tx.chain_id() != Some(chain_spec.chain_id()) { + return Err(invalid_params_rpc_err(format!( + "expected chain id == {}", + chain_spec.chain_id() + ))); + } + + // Check signature correctness + if tx.recover_signer().is_none() { + return Err(invalid_params_rpc_err( + "transaction signature verification failed".to_string(), + )); + } + + // Check signature malleability + did::transaction::Signature::check_malleability(&tx.signature.s().into()) + .map_err(|e| invalid_params_rpc_err(format!("signature malleability check failed: {e}")))?; + + // Check min gas limit + if tx.gas_limit() < MINIMUM_GAS_LIMIT { + return Err(invalid_params_rpc_err(format!( + "expected gas limit greater or equal to {MINIMUM_GAS_LIMIT}, found: {}", + tx.gas_limit() + ))); + } + + Ok(()) +}