Skip to content

Commit

Permalink
add starknet transaction hash to transaction_by_hash (kkrt-labs#1524)
Browse files Browse the repository at this point in the history
* add starknet transaction hash to transaction_by_hash

* clippy

* clean up
  • Loading branch information
tcoratger authored Nov 5, 2024
1 parent 15da170 commit dacd761
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 26 deletions.
1 change: 1 addition & 0 deletions src/bin/hive_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 31 additions & 3 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -170,13 +175,36 @@ where
SP: starknet::providers::Provider + Send + Sync,
{
async fn transaction_by_hash(&self, hash: B256) -> EthApiResult<Option<ExtendedTransaction>> {
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::<reth_rpc::eth::EthTxBuilder>()
})
.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::<filter::EthStarknetTransactionHash>::default()
.with_tx_hash(&transaction.hash)
.build();

let hash_mapping: Option<StoredEthStarknetTransactionHash> =
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)
}
}
1 change: 1 addition & 0 deletions src/pool/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ impl<SP: starknet::providers::Provider + Send + Sync + Clone + 'static> 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
Expand Down
71 changes: 70 additions & 1 deletion src/providers/eth_provider/database/ethereum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -31,6 +34,8 @@ pub trait EthereumTransactionStore {
) -> Result<Vec<ExtendedTransaction>, 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]
Expand Down Expand Up @@ -58,6 +63,14 @@ impl EthereumTransactionStore for Database {
let filter = EthDatabaseFilterBuilder::<filter::Transaction>::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::<filter::EthStarknetTransactionHash>::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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -392,4 +406,59 @@ mod tests {
// Test retrieving non-existing transaction count by block number
assert_eq!(database.transaction_count(rng.gen::<u64>().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::<filter::EthStarknetTransactionHash>::default().with_tx_hash(&eth_hash).build();
let stored_mapping: Option<StoredEthStarknetTransactionHash> =
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<StoredEthStarknetTransactionHash> =
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"
);
}
}
22 changes: 22 additions & 0 deletions src/providers/eth_provider/database/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion src/providers/eth_provider/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -235,3 +238,10 @@ impl CollectionName for StoredLog {
"logs"
}
}

/// Implement [`CollectionName`] for [`StoredEthStarknetTransactionHash`]
impl CollectionName for StoredEthStarknetTransactionHash {
fn collection_name() -> &'static str {
"transaction_hashes"
}
}
25 changes: 25 additions & 0 deletions src/providers/eth_provider/database/types/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Transaction>;

/// 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<EthStarknetHashes> 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 {
Expand Down
25 changes: 22 additions & 3 deletions src/providers/eth_provider/starknet/relayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<LocalWallet> = LazyLock::new(|| {
Expand All @@ -33,14 +39,16 @@ pub struct Relayer<SP: Provider + Send + Sync> {
account: SingleOwnerAccount<SP, LocalWallet>,
/// The balance of the relayer
balance: Felt,
/// The database used to store the relayer's transaction hashes map (Ethereum -> Starknet)
database: Option<Arc<Database>>,
}

impl<SP> Relayer<SP>
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<Arc<Database>>) -> Self {
let relayer = SingleOwnerAccount::new(
provider,
RELAYER_SIGNER.clone(),
Expand All @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down
26 changes: 18 additions & 8 deletions src/test_utils/eoa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,15 @@ impl<P: Provider + Send + Sync + Clone> KakarotEOA<P> {
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(),
Expand Down Expand Up @@ -226,10 +231,15 @@ impl<P: Provider + Send + Sync + Clone> KakarotEOA<P> {
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(),
Expand Down
Loading

0 comments on commit dacd761

Please sign in to comment.