Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bin/reth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
153 changes: 139 additions & 14 deletions bin/reth/tests/commands/bitfinity_node_it.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -207,7 +240,88 @@ async fn start_reth_node(
import_data: Option<ImportData>,
) -> (
EthJsonRpcClient<ReqwestClient>,
NodeHandle<NodeAdapter<FullNodeTypesAdapter<EthereumNode, Arc<DatabaseEnv>, BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>>, Components<FullNodeTypesAdapter<EthereumNode, Arc<DatabaseEnv>, BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>>, reth_network::EthNetworkPrimitives, Pool<TransactionValidationTaskExecutor<EthTransactionValidator<BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>, EthPooledTransaction>>, CoinbaseTipOrdering<EthPooledTransaction>, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider<EthExecutionStrategyFactory>, Arc<dyn FullConsensus>>>, RpcAddOns<NodeAdapter<FullNodeTypesAdapter<EthereumNode, Arc<DatabaseEnv>, BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>>, Components<FullNodeTypesAdapter<EthereumNode, Arc<DatabaseEnv>, BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>>, reth_network::EthNetworkPrimitives, Pool<TransactionValidationTaskExecutor<EthTransactionValidator<BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>, EthPooledTransaction>>, CoinbaseTipOrdering<EthPooledTransaction>, DiskFileBlobStore>, EthEvmConfig, BasicBlockExecutorProvider<EthExecutionStrategyFactory>, Arc<dyn FullConsensus>>>, EthApi<BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>, Pool<TransactionValidationTaskExecutor<EthTransactionValidator<BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>, EthPooledTransaction>>, CoinbaseTipOrdering<EthPooledTransaction>, DiskFileBlobStore>, NetworkHandle, EthEvmConfig>, EthereumEngineValidatorBuilder>>,
NodeHandle<
NodeAdapter<
FullNodeTypesAdapter<
EthereumNode,
Arc<DatabaseEnv>,
BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>,
>,
Components<
FullNodeTypesAdapter<
EthereumNode,
Arc<DatabaseEnv>,
BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>,
>,
reth_network::EthNetworkPrimitives,
Pool<
TransactionValidationTaskExecutor<
EthTransactionValidator<
BlockchainProvider<
NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>,
>,
EthPooledTransaction,
>,
>,
CoinbaseTipOrdering<EthPooledTransaction>,
DiskFileBlobStore,
>,
EthEvmConfig,
BasicBlockExecutorProvider<EthExecutionStrategyFactory>,
Arc<dyn FullConsensus>,
>,
>,
RpcAddOns<
NodeAdapter<
FullNodeTypesAdapter<
EthereumNode,
Arc<DatabaseEnv>,
BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>,
>,
Components<
FullNodeTypesAdapter<
EthereumNode,
Arc<DatabaseEnv>,
BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>,
>,
reth_network::EthNetworkPrimitives,
Pool<
TransactionValidationTaskExecutor<
EthTransactionValidator<
BlockchainProvider<
NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>,
>,
EthPooledTransaction,
>,
>,
CoinbaseTipOrdering<EthPooledTransaction>,
DiskFileBlobStore,
>,
EthEvmConfig,
BasicBlockExecutorProvider<EthExecutionStrategyFactory>,
Arc<dyn FullConsensus>,
>,
>,
EthApi<
BlockchainProvider<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>,
Pool<
TransactionValidationTaskExecutor<
EthTransactionValidator<
BlockchainProvider<
NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>,
>,
EthPooledTransaction,
>,
>,
CoinbaseTipOrdering<EthPooledTransaction>,
DiskFileBlobStore,
>,
NetworkHandle,
EthEvmConfig,
>,
EthereumEngineValidatorBuilder,
>,
>,
) {
let tasks = TaskManager::current();

Expand Down Expand Up @@ -273,11 +387,14 @@ async fn mock_eth_server_start(methods: impl Into<Methods>) -> (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 {
Expand Down Expand Up @@ -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<RwLock<Vec<B256>>>,
}

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(),
}
}
}

Expand All @@ -337,7 +460,9 @@ pub mod eth_server {
}

async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult<B256> {
let tx = hex::decode(&tx).unwrap();
let hash = keccak::keccak_hash(&tx);
self.received_tx_hashes.write().await.push(hash.0);
Ok(hash.into())
}

Expand Down
52 changes: 50 additions & 2 deletions crates/rpc/rpc-eth-api/src/helpers/bitfinity_evm_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -54,6 +59,14 @@ pub trait BitfinityEvmRpc {
fn send_raw_transaction(&self, tx: Bytes) -> impl Future<Output = RpcResult<B256>> + 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| {
Expand Down Expand Up @@ -116,3 +129,38 @@ fn get_client(chain_spec: &ChainSpec) -> RpcResult<(&String, EthJsonRpcClient<Re

Ok((rpc_url, client))
}

/// Validates:
/// - chain id
/// - signature
/// - gas limit
fn validate_raw_transaction(tx: &TransactionSigned, chain_spec: &ChainSpec) -> 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(())
}
Loading