diff --git a/Makefile b/Makefile index 06d250d67..83c2d4f64 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,9 @@ test-ci: load-env test-target: load-env cargo test --tests --all-features $(TARGET) -- --nocapture +test-target1: load-env + cargo test --package kakarot-rpc --test entry --all-features -- tests::eth_provider::test_transaction_by_hash --exact --show-output + benchmark: cd benchmarks && bun i && bun run benchmark diff --git a/src/bin/hive_chain.rs b/src/bin/hive_chain.rs index 859ae257c..8e8da1e09 100644 --- a/src/bin/hive_chain.rs +++ b/src/bin/hive_chain.rs @@ -75,6 +75,7 @@ async fn main() -> eyre::Result<()> { args.relayer_address, relayer_balance, JsonRpcClient::new(HttpTransport::new(Url::from_str(STARKNET_RPC_URL)?)), + None, ); // Read the rlp file diff --git a/src/client/mod.rs b/src/client/mod.rs index 6833dd33b..cfaea0e87 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -6,7 +6,12 @@ use crate::{ }, providers::{ eth_provider::{ - database::{types::transaction::ExtendedTransaction, Database}, + database::{ + filter, + filter::EthDatabaseFilterBuilder, + types::transaction::{ExtendedTransaction, StoredEthStarknetTransactionHash}, + Database, + }, error::SignatureError, provider::{EthApiResult, EthDataProvider}, TransactionProvider, TxPoolProvider, @@ -26,6 +31,7 @@ use reth_transaction_pool::{ blobstore::NoopBlobStore, AllPoolTransactions, EthPooledTransaction, PoolConfig, PoolTransaction, TransactionOrigin, TransactionPool, }; +use serde_json::to_string; use starknet::providers::Provider; use std::{collections::BTreeMap, sync::Arc}; @@ -170,13 +176,36 @@ where SP: starknet::providers::Provider + Send + Sync, { async fn transaction_by_hash(&self, hash: B256) -> EthApiResult> { - Ok(self + // Try to get the information from: + // 1. The pool if the transaction is in the pool. + // 2. The Ethereum provider if the transaction is not in the pool. + let mut tx = self .pool .get(&hash) .map(|transaction| { TransactionSource::Pool(transaction.transaction.transaction().clone()) .into_transaction::() }) - .or(self.eth_provider.transaction_by_hash(hash).await?)) + .or(self.eth_provider.transaction_by_hash(hash).await?); + + if let Some(ref mut transaction) = tx { + // Fetch the Starknet transaction hash if it exists. + let filter = EthDatabaseFilterBuilder::::default() + .with_tx_hash(&transaction.hash) + .build(); + + let hash_mapping: Option = + self.eth_provider.database().get_one(filter, None).await?; + + // Add the Starknet transaction hash to the transaction fields. + if let Some(hash_mapping) = hash_mapping { + transaction.other.insert( + "starknet_transaction_hash".to_string(), + serde_json::Value::String(hash_mapping.hashes.starknet_hash.to_fixed_hex_string()), + ); + } + } + + Ok(tx) } } diff --git a/src/pool/mempool.rs b/src/pool/mempool.rs index a9c7cc36a..863e96acf 100644 --- a/src/pool/mempool.rs +++ b/src/pool/mempool.rs @@ -152,6 +152,7 @@ impl AccountM account_address, balance, JsonRpcClient::new(HttpTransport::new(KAKAROT_RPC_CONFIG.network_url.clone())), + Some(Arc::new(self.eth_client.eth_provider().database().clone())), ); // Return the locked relayer instance diff --git a/src/providers/eth_provider/database/ethereum.rs b/src/providers/eth_provider/database/ethereum.rs index 6d7620999..9fd445de3 100644 --- a/src/providers/eth_provider/database/ethereum.rs +++ b/src/providers/eth_provider/database/ethereum.rs @@ -7,7 +7,10 @@ use super::{ }, Database, }; -use crate::providers::eth_provider::error::EthApiError; +use crate::providers::eth_provider::{ + database::types::transaction::{EthStarknetHashes, StoredEthStarknetTransactionHash}, + error::EthApiError, +}; use alloy_primitives::{B256, U256}; use alloy_rlp::Encodable; use alloy_rpc_types::{Block, BlockHashOrNumber, BlockTransactions, Header}; @@ -31,6 +34,8 @@ pub trait EthereumTransactionStore { ) -> Result, EthApiError>; /// Upserts the given transaction. async fn upsert_transaction(&self, transaction: ExtendedTransaction) -> Result<(), EthApiError>; + /// Upserts the given transaction hash mapping (Ethereum -> Starknet). + async fn upsert_transaction_hashes(&self, transaction_hashes: EthStarknetHashes) -> Result<(), EthApiError>; } #[async_trait] @@ -58,6 +63,14 @@ impl EthereumTransactionStore for Database { let filter = EthDatabaseFilterBuilder::::default().with_tx_hash(&transaction.hash).build(); Ok(self.update_one(StoredTransaction::from(transaction), filter, true).await?) } + + #[instrument(skip_all, name = "db::upsert_transaction_hashes", err)] + async fn upsert_transaction_hashes(&self, transaction_hashes: EthStarknetHashes) -> Result<(), EthApiError> { + let filter = EthDatabaseFilterBuilder::::default() + .with_tx_hash(&transaction_hashes.eth_hash) + .build(); + Ok(self.update_one(StoredEthStarknetTransactionHash::from(transaction_hashes), filter, true).await?) + } } /// Trait for interacting with a database that stores Ethereum typed @@ -177,6 +190,7 @@ mod tests { use crate::test_utils::mongo::{MongoFuzzer, RANDOM_BYTES_SIZE}; use arbitrary::Arbitrary; use rand::{self, Rng}; + use starknet::core::types::Felt; #[tokio::test(flavor = "multi_thread")] async fn test_ethereum_transaction_store() { @@ -392,4 +406,59 @@ mod tests { // Test retrieving non-existing transaction count by block number assert_eq!(database.transaction_count(rng.gen::().into()).await.unwrap(), None); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_upsert_transaction_hashes() { + // Initialize MongoDB fuzzer + let mut mongo_fuzzer = MongoFuzzer::new(RANDOM_BYTES_SIZE).await; + + // Mock a database with sample data + let database = mongo_fuzzer.mock_database(1).await; + + // Generate random Ethereum and Starknet hashes + let eth_hash = B256::random(); + let starknet_hash = + Felt::from_hex("0x03d937c035c878245caf64531a5756109c53068da139362728feb561405371cb").unwrap(); + + // Define an EthStarknetHashes instance for testing + let transaction_hashes = EthStarknetHashes { eth_hash, starknet_hash }; + + // First, upsert the transaction hash mapping (should insert as it doesn't exist initially) + database + .upsert_transaction_hashes(transaction_hashes.clone()) + .await + .expect("Failed to upsert transaction hash mapping"); + + // Retrieve the inserted transaction hash mapping and verify it matches the inserted values + let filter = + EthDatabaseFilterBuilder::::default().with_tx_hash(ð_hash).build(); + let stored_mapping: Option = + database.get_one(filter.clone(), None).await.expect("Failed to retrieve transaction hash mapping"); + + assert_eq!( + stored_mapping, + Some(StoredEthStarknetTransactionHash::from(transaction_hashes.clone())), + "The transaction hash mapping was not inserted correctly" + ); + + // Now, modify the Starknet hash and upsert the modified transaction hash mapping + let new_starknet_hash = + Felt::from_hex("0x0208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a").unwrap(); + let updated_transaction_hashes = EthStarknetHashes { eth_hash, starknet_hash: new_starknet_hash }; + + database + .upsert_transaction_hashes(updated_transaction_hashes.clone()) + .await + .expect("Failed to update transaction hash mapping"); + + // Retrieve the updated transaction hash mapping and verify it matches the updated values + let updated_mapping: Option = + database.get_one(filter, None).await.expect("Failed to retrieve updated transaction hash mapping"); + + assert_eq!( + updated_mapping, + Some(StoredEthStarknetTransactionHash::from(updated_transaction_hashes)), + "The transaction hash mapping was not updated correctly" + ); + } } diff --git a/src/providers/eth_provider/database/filter.rs b/src/providers/eth_provider/database/filter.rs index 467682ae2..e0d88b4c2 100644 --- a/src/providers/eth_provider/database/filter.rs +++ b/src/providers/eth_provider/database/filter.rs @@ -32,6 +32,28 @@ pub trait LogFiltering { fn address(&self) -> &'static str; } +/// A type used for a mapping between: +/// - An Ethereum transaction hash +/// - A Starknet transaction hash. +#[derive(Debug, Default)] +pub struct EthStarknetTransactionHash; + +impl Display for EthStarknetTransactionHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "hashes") + } +} + +impl TransactionFiltering for EthStarknetTransactionHash { + fn transaction_hash(&self) -> &'static str { + "eth_hash" + } + + fn transaction_index(&self) -> &'static str { + "" + } +} + /// A transaction type used as a target for the filter. #[derive(Debug, Default)] pub struct Transaction; diff --git a/src/providers/eth_provider/database/mod.rs b/src/providers/eth_provider/database/mod.rs index 4b4b6aa04..07de1de20 100644 --- a/src/providers/eth_provider/database/mod.rs +++ b/src/providers/eth_provider/database/mod.rs @@ -5,7 +5,10 @@ pub mod types; use super::error::KakarotError; use crate::providers::eth_provider::database::types::{ - header::StoredHeader, log::StoredLog, receipt::StoredTransactionReceipt, transaction::StoredTransaction, + header::StoredHeader, + log::StoredLog, + receipt::StoredTransactionReceipt, + transaction::{StoredEthStarknetTransactionHash, StoredTransaction}, }; use futures::TryStreamExt; use itertools::Itertools; @@ -235,3 +238,10 @@ impl CollectionName for StoredLog { "logs" } } + +/// Implement [`CollectionName`] for [`StoredEthStarknetTransactionHash`] +impl CollectionName for StoredEthStarknetTransactionHash { + fn collection_name() -> &'static str { + "transaction_hashes" + } +} diff --git a/src/providers/eth_provider/database/types/transaction.rs b/src/providers/eth_provider/database/types/transaction.rs index 66d8442d6..7e4d46f65 100644 --- a/src/providers/eth_provider/database/types/transaction.rs +++ b/src/providers/eth_provider/database/types/transaction.rs @@ -2,6 +2,7 @@ use alloy_primitives::B256; use alloy_rpc_types::Transaction; use alloy_serde::WithOtherFields; use serde::{Deserialize, Serialize}; +use starknet::core::types::Felt; use std::ops::Deref; #[cfg(any(test, feature = "arbitrary", feature = "testing"))] use { @@ -11,9 +12,33 @@ use { reth_primitives::transaction::legacy_parity, reth_testing_utils::generators::{self}, }; + /// Type alias for a transaction with additional fields. pub type ExtendedTransaction = WithOtherFields; +/// A mapping between an Ethereum transaction hash and a Starknet transaction hash. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct StoredEthStarknetTransactionHash { + /// Contains both Ethereum and StarkNet transaction hashes. + #[serde(deserialize_with = "crate::providers::eth_provider::database::types::serde::deserialize_intermediate")] + pub hashes: EthStarknetHashes, +} + +impl From for StoredEthStarknetTransactionHash { + fn from(hashes: EthStarknetHashes) -> Self { + Self { hashes } + } +} + +/// Inner struct that holds the Ethereum and StarkNet transaction hashes. +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +pub struct EthStarknetHashes { + /// The Ethereum transaction hash. + pub eth_hash: B256, + /// The Starknet transaction hash. + pub starknet_hash: Felt, +} + /// A full transaction as stored in the database #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] pub struct StoredTransaction { diff --git a/src/providers/eth_provider/starknet/relayer.rs b/src/providers/eth_provider/starknet/relayer.rs index b090bc237..4314d57c9 100644 --- a/src/providers/eth_provider/starknet/relayer.rs +++ b/src/providers/eth_provider/starknet/relayer.rs @@ -2,6 +2,7 @@ use crate::{ constants::STARKNET_CHAIN_ID, models::transaction::transaction_data_to_starknet_calldata, providers::eth_provider::{ + database::{ethereum::EthereumTransactionStore, types::transaction::EthStarknetHashes, Database}, error::{SignatureError, TransactionError}, provider::EthApiResult, starknet::kakarot_core::{starknet_address, EXECUTE_FROM_OUTSIDE}, @@ -14,7 +15,12 @@ use starknet::{ providers::Provider, signers::{LocalWallet, SigningKey}, }; -use std::{env::var, ops::Deref, str::FromStr, sync::LazyLock}; +use std::{ + env::var, + ops::Deref, + str::FromStr, + sync::{Arc, LazyLock}, +}; /// Signer for all relayers static RELAYER_SIGNER: LazyLock = LazyLock::new(|| { @@ -33,6 +39,8 @@ pub struct Relayer { account: SingleOwnerAccount, /// The balance of the relayer balance: Felt, + /// The database used to store the relayer's transaction hashes map (Ethereum -> Starknet) + database: Option>, } impl Relayer @@ -40,7 +48,7 @@ where SP: Provider + Send + Sync, { /// Create a new relayer with the provided Starknet provider, address, balance. - pub fn new(address: Felt, balance: Felt, provider: SP) -> Self { + pub fn new(address: Felt, balance: Felt, provider: SP, database: Option>) -> Self { let relayer = SingleOwnerAccount::new( provider, RELAYER_SIGNER.clone(), @@ -49,7 +57,7 @@ where ExecutionEncoding::New, ); - Self { account: relayer, balance } + Self { account: relayer, balance, database } } /// Relay the provided Ethereum transaction on the Starknet network. @@ -87,6 +95,17 @@ where let prepared = execution.prepared().map_err(|_| SignatureError::SigningFailure)?; let res = prepared.send().await.map_err(|err| TransactionError::Broadcast(err.into()))?; + // Store a transaction hash mapping from Ethereum to Starknet in the database + + if let Some(database) = &self.database { + database + .upsert_transaction_hashes(EthStarknetHashes { + eth_hash: transaction.hash, + starknet_hash: res.transaction_hash, + }) + .await?; + } + Ok(res.transaction_hash) } diff --git a/src/test_utils/eoa.rs b/src/test_utils/eoa.rs index 6ff87a814..88e8f2d0e 100644 --- a/src/test_utils/eoa.rs +++ b/src/test_utils/eoa.rs @@ -157,10 +157,15 @@ impl KakarotEOA

{ let relayer_balance = into_via_try_wrapper!(relayer_balance)?; // Relay the transaction - let starknet_transaction_hash = Relayer::new(self.relayer.address(), relayer_balance, self.starknet_provider()) - .relay_transaction(&tx_signed) - .await - .expect("Failed to relay transaction"); + let starknet_transaction_hash = Relayer::new( + self.relayer.address(), + relayer_balance, + self.starknet_provider(), + Some(Arc::new(self.eth_client.eth_provider().database().clone())), + ) + .relay_transaction(&tx_signed) + .await + .expect("Failed to relay transaction"); watch_tx( self.eth_client.eth_provider().starknet_provider_inner(), @@ -226,10 +231,15 @@ impl KakarotEOA

{ let relayer_balance = into_via_try_wrapper!(relayer_balance)?; // Relay the transaction - let starknet_transaction_hash = Relayer::new(self.relayer.address(), relayer_balance, self.starknet_provider()) - .relay_transaction(&tx_signed) - .await - .expect("Failed to relay transaction"); + let starknet_transaction_hash = Relayer::new( + self.relayer.address(), + relayer_balance, + self.starknet_provider(), + Some(Arc::new(self.eth_client.eth_provider().database().clone())), + ) + .relay_transaction(&tx_signed) + .await + .expect("Failed to relay transaction"); watch_tx( self.eth_client.eth_provider().starknet_provider_inner(), diff --git a/tests/tests/eth_provider.rs b/tests/tests/eth_provider.rs index a4754fbfe..beb0f5bd2 100644 --- a/tests/tests/eth_provider.rs +++ b/tests/tests/eth_provider.rs @@ -18,7 +18,12 @@ use kakarot_rpc::{ models::felt::Felt252Wrapper, providers::eth_provider::{ constant::{MAX_LOGS, STARKNET_MODULUS}, - database::{ethereum::EthereumTransactionStore, types::transaction::StoredTransaction}, + database::{ + ethereum::EthereumTransactionStore, + filter, + filter::EthDatabaseFilterBuilder, + types::transaction::{EthStarknetHashes, StoredEthStarknetTransactionHash, StoredTransaction}, + }, provider::EthereumProvider, starknet::relayer::Relayer, BlockProvider, ChainProvider, GasProvider, LogProvider, ReceiptProvider, StateProvider, TransactionProvider, @@ -747,10 +752,39 @@ async fn test_send_raw_transaction(#[future] katana_empty: Katana, _setup: ()) { let relayer_balance = into_via_try_wrapper!(relayer_balance).expect("Failed to convert balance"); // Relay the transaction - let _ = Relayer::new(katana.eoa.relayer.address(), relayer_balance, &(*(*eth_client.starknet_provider()))) - .relay_transaction(&transaction_signed) + let starknet_hash = Relayer::new( + katana.eoa.relayer.address(), + relayer_balance, + &(*(*eth_client.starknet_provider())), + Some(Arc::new(eth_client.eth_provider().database().clone())), + ) + .relay_transaction(&transaction_signed) + .await + .expect("Failed to relay transaction"); + + // Retrieve the hash mapping from the database (Ethereum -> StarkNet) + // 1. Prepare the filter + let filter = EthDatabaseFilterBuilder::::default() + .with_tx_hash(&transaction_signed.hash) + .build(); + + // 2. Retrieve the hash mapping + let hash_mapping: Option = eth_client + .eth_provider() + .database() + .get_one(filter, None) .await - .expect("Failed to relay transaction"); + .expect("Failed to retrieve updated transaction hash mapping"); + + // 3. Prepare the transaction hashes + let transaction_hashes = EthStarknetHashes { eth_hash: transaction_signed.hash, starknet_hash }; + + // 4. Assert that the hash mapping was inserted correctly + assert_eq!( + hash_mapping, + Some(StoredEthStarknetTransactionHash::from(transaction_hashes)), + "The transaction hash mapping was not inserted correctly" + ); // Retrieve the current size of the mempool let mempool_size_after_send = eth_client.mempool().pool_size(); @@ -990,11 +1024,15 @@ async fn test_send_raw_transaction_pre_eip_155(#[future] katana_empty: Katana, _ let relayer_balance = into_via_try_wrapper!(relayer_balance).expect("Failed to convert balance"); // Relay the transaction - let starknet_transaction_hash = - Relayer::new(katana.eoa.relayer.address(), relayer_balance, &(*(*katana.eth_client.starknet_provider()))) - .relay_transaction(&transaction_signed) - .await - .expect("Failed to relay transaction"); + let starknet_transaction_hash = Relayer::new( + katana.eoa.relayer.address(), + relayer_balance, + &(*(*katana.eth_client.starknet_provider())), + Some(Arc::new(katana.eth_client.eth_provider().database().clone())), + ) + .relay_transaction(&transaction_signed) + .await + .expect("Failed to relay transaction"); watch_tx( eth_provider.starknet_provider_inner(), @@ -1309,8 +1347,29 @@ async fn test_transaction_by_hash(#[future] katana_empty: Katana, _setup: ()) { .await .expect("Failed to insert transaction into the mempool"); + // Add a hash mapping to the database + let starknet_hash = Felt::from_hex("0x0208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a").unwrap(); + let updated_transaction_hashes = EthStarknetHashes { eth_hash: tx_hash, starknet_hash }; + + katana_empty + .eth_client + .eth_provider() + .database() + .upsert_transaction_hashes(updated_transaction_hashes.clone()) + .await + .expect("Failed to update transaction hash mapping"); + // Check if the first transaction is returned correctly by the `transaction_by_hash` method - assert!(katana_empty.eth_client.transaction_by_hash(transaction.transaction().hash).await.unwrap().is_some()); + let tx = katana_empty.eth_client.transaction_by_hash(tx_hash).await.unwrap(); + assert!(tx.is_some()); + + assert_eq!( + *tx.unwrap() + .other + .get("starknet_transaction_hash") + .expect("Expected starknet_transaction_hash in the transaction"), + serde_json::Value::String("0x0208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a".to_string()) + ); // Check if a non-existent transaction returns None assert!(katana_empty.eth_client.transaction_by_hash(B256::random()).await.unwrap().is_none()); @@ -1342,6 +1401,7 @@ async fn test_transaction_by_hash(#[future] katana_empty: Katana, _setup: ()) { katana_empty.eoa.relayer.address(), relayer_balance, &(*(*katana_empty.eth_client.starknet_provider())), + Some(Arc::new(katana_empty.eth_client.eth_provider().database().clone())), ) .relay_transaction(&transaction_signed) .await