From 954f64a8c591f4caa36827797fb43d5a7cdaae07 Mon Sep 17 00:00:00 2001 From: azurwastaken Date: Tue, 20 Aug 2024 14:12:09 +0200 Subject: [PATCH] feat(client): l1<>l2 messaging crate (#220) Co-authored-by: 0xevolve --- CHANGELOG.md | 1 + Cargo.lock | 100 ++++- crates/client/db/src/l1_db.rs | 127 ++++++ crates/client/db/src/lib.rs | 8 + crates/client/eth/Cargo.toml | 4 + crates/client/eth/README.md | 39 ++ crates/client/eth/src/client.rs | 1 + crates/client/eth/src/l1_messaging.rs | 541 ++++++++++++++++++++++++++ crates/client/eth/src/lib.rs | 1 + crates/client/eth/src/state_update.rs | 8 +- crates/client/eth/src/utils.rs | 4 + 11 files changed, 828 insertions(+), 6 deletions(-) create mode 100644 crates/client/db/src/l1_db.rs create mode 100644 crates/client/eth/README.md create mode 100644 crates/client/eth/src/l1_messaging.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf94a292..ca9b5016b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release +- feat: Added l1->l2 messaging - test: add unitests primitives - tests: add tests for the rpcs endpoints - fix: pending contract storage not stored properly diff --git a/Cargo.lock b/Cargo.lock index 992268a96..9a080858a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3412,6 +3412,8 @@ dependencies = [ "alloy", "anyhow", "bitvec", + "blockifier", + "bytes", "dc-db", "dc-metrics", "dotenv", @@ -3431,6 +3433,8 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tracing", + "tracing-test", "url", ] @@ -4286,7 +4290,7 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata", + "regex-automata 0.4.7", "regex-syntax 0.8.4", ] @@ -4828,7 +4832,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata", + "regex-automata 0.4.7", "same-file", "walkdir", "winapi-util", @@ -5218,7 +5222,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata", + "regex-automata 0.4.7", ] [[package]] @@ -5387,6 +5391,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matrixmultiply" version = "0.2.4" @@ -6346,10 +6359,19 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.7", "regex-syntax 0.8.4", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + [[package]] name = "regex-automata" version = "0.4.7" @@ -7098,6 +7120,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -7810,6 +7841,16 @@ dependencies = [ "thiserror-impl-no-std", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -8103,6 +8144,57 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.66", ] [[package]] diff --git a/crates/client/db/src/l1_db.rs b/crates/client/db/src/l1_db.rs new file mode 100644 index 000000000..0c7f97b57 --- /dev/null +++ b/crates/client/db/src/l1_db.rs @@ -0,0 +1,127 @@ +use rocksdb::WriteOptions; +use serde::{Deserialize, Serialize}; +use starknet_api::core::Nonce; + +use crate::error::DbError; +use crate::{Column, DatabaseExt, DeoxysBackend, DeoxysStorageError}; + +type Result = std::result::Result; + +pub const LAST_SYNCED_L1_EVENT_BLOCK: &[u8] = b"LAST_SYNCED_L1_EVENT_BLOCK"; + +/// Struct to store block number and event_index where L1->L2 Message occured +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LastSyncedEventBlock { + pub block_number: u64, + pub event_index: u64, +} + +impl LastSyncedEventBlock { + /// Create a new LastSyncedBlock with block number and event index + pub fn new(block_number: u64, event_index: u64) -> Self { + LastSyncedEventBlock { block_number, event_index } + } +} + +/// We add method in DeoxysBackend to be able to handle L1->L2 messaging related data +impl DeoxysBackend { + /// Retrieves the last stored L1 block data that contains a message from the database. + /// + /// This function attempts to fetch the data of the last messaging-related block from the database. + /// If a block is found, it is retrieved, deserialized, and returned. + /// Otherwise, a `LastSyncedEventBlock` instance with the `block_number` and `event_index` set to 0 is returned. + /// + /// # Returns + /// + /// - `Ok(Some(LastSyncedEventBlock))` - If the last synced L1 block with a messaging event is found + /// and successfully deserialized. + /// - `Ok(Some(LastSyncedEventBlock::new(0, 0)))` - If no such block exists in the database. + /// - `Err(e)` - If there is an error accessing the database or deserializing the block. + /// + /// # Errors + /// + /// This function returns an error if: + /// - There is a failure in interacting with the database. + /// - The block's deserialization fails. + /// + /// # Example + /// + /// let last_synced_event_block = match backend.messaging_last_synced_l1_block_with_event() { + /// Ok(Some(blk)) => blk, + /// Ok(None) => unreachable!("Should never be None"), + /// Err(e) => { + /// tracing::error!("⟠ Madara Messaging DB unavailable: {:?}", e); + /// return Err(e.into()); + /// } + /// }; + /// + /// # Panics + /// + /// This function does not panic. + pub fn messaging_last_synced_l1_block_with_event(&self) -> Result> { + let messaging_column = self.db.get_column(Column::L1Messaging); + let Some(res) = self.db.get_cf(&messaging_column, LAST_SYNCED_L1_EVENT_BLOCK)? else { + return Ok(Some(LastSyncedEventBlock::new(0, 0))); + }; + let res = bincode::deserialize(&res)?; + Ok(Some(res)) + } + + /// This function inserts a new block into the messaging column. + /// + /// This function retrieves the messaging column and inserts a `LastSyncedEventBlock` + /// into it. + /// + /// # Arguments + /// + /// - `last_synced_event_block`: The `LastSyncedEventBlock` instance representing the most recent + /// synced L1 block with a messaging event. + /// + /// # Returns + /// + /// - `Ok(())` if the data is correctly inserted into the database. + /// - `Err(e)` if there is an error accessing the database or serializing the data. + /// + /// # Errors + /// + /// This function returns an error if: + /// - There is a failure in interacting with the database. + /// - The block's serialization fails. + /// + /// # Example + /// + /// let block_sent = LastSyncedEventBlock::new(l1_block_number.unwrap(), event_index.unwrap()); + /// backend.messaging_update_last_synced_l1_block_with_event(block_sent)?; + /// + /// # Panics + /// + /// This function does not panic. + pub fn messaging_update_last_synced_l1_block_with_event( + &self, + last_synced_event_block: LastSyncedEventBlock, + ) -> Result<(), DbError> { + let messaging_column = self.db.get_column(Column::L1Messaging); + let mut writeopts = WriteOptions::default(); // todo move that in db + writeopts.disable_wal(true); + self.db.put_cf_opt( + &messaging_column, + LAST_SYNCED_L1_EVENT_BLOCK, + bincode::serialize(&last_synced_event_block)?, + &writeopts, + )?; + Ok(()) + } + + pub fn has_l1_messaging_nonce(&self, nonce: Nonce) -> Result { + let nonce_column = self.db.get_column(Column::L1MessagingNonce); + Ok(self.db.get_pinned_cf(&nonce_column, bincode::serialize(&nonce)?)?.is_some()) + } + + pub fn set_l1_messaging_nonce(&self, nonce: Nonce) -> Result<(), DbError> { + let nonce_column = self.db.get_column(Column::L1MessagingNonce); + let mut writeopts = WriteOptions::default(); + writeopts.disable_wal(true); + self.db.put_cf_opt(&nonce_column, bincode::serialize(&nonce)?, /* empty value */ [], &writeopts)?; + Ok(()) + } +} diff --git a/crates/client/db/src/lib.rs b/crates/client/db/src/lib.rs index 1b07a44f4..807dbf200 100644 --- a/crates/client/db/src/lib.rs +++ b/crates/client/db/src/lib.rs @@ -25,6 +25,7 @@ pub mod class_db; pub mod contract_db; pub mod db_block_id; pub mod db_metrics; +pub mod l1_db; pub mod storage_updates; pub use error::{DeoxysStorageError, TrieType}; @@ -164,6 +165,9 @@ pub enum Column { BonsaiClassesTrie, BonsaiClassesFlat, BonsaiClassesLog, + + L1Messaging, + L1MessagingNonce, } impl fmt::Debug for Column { @@ -207,6 +211,8 @@ impl Column { BonsaiClassesTrie, BonsaiClassesFlat, BonsaiClassesLog, + L1Messaging, + L1MessagingNonce, PendingContractToClassHashes, PendingContractToNonces, PendingContractStorage, @@ -242,6 +248,8 @@ impl Column { ContractToNonces => "contract_to_nonces", ContractClassHashes => "contract_class_hashes", ContractStorage => "contract_storage", + L1Messaging => "l1_messaging", + L1MessagingNonce => "l1_messaging_nonce", PendingContractToClassHashes => "pending_contract_to_class_hashes", PendingContractToNonces => "pending_contract_to_nonces", PendingContractStorage => "pending_contract_storage", diff --git a/crates/client/eth/Cargo.toml b/crates/client/eth/Cargo.toml index 6388f4246..06922aebe 100644 --- a/crates/client/eth/Cargo.toml +++ b/crates/client/eth/Cargo.toml @@ -30,6 +30,8 @@ starknet_api = { workspace = true } alloy = { workspace = true, features = ["node-bindings"] } anyhow = "1.0.75" bitvec = { workspace = true } +blockifier = { workspace = true } +bytes = "1.6.0" futures = { workspace = true, default-features = true } log = { workspace = true } serde = { workspace = true, default-features = true } @@ -41,6 +43,7 @@ tokio = { workspace = true, features = [ "test-util", "signal", ] } +tracing = "0.1.40" url = { workspace = true } [dev-dependencies] @@ -49,3 +52,4 @@ once_cell = { workspace = true } tempfile = { workspace = true } dotenv = { workspace = true } prometheus = { workspace = true } +tracing-test = "0.2.5" diff --git a/crates/client/eth/README.md b/crates/client/eth/README.md new file mode 100644 index 000000000..a763ec1b7 --- /dev/null +++ b/crates/client/eth/README.md @@ -0,0 +1,39 @@ +# L1<>L2 messaging + +A few notes on the current design: + +- There is no guarantee that L1->L2 messages will be executed in a centralized context. +- There is no ordering guarantee, nonces are solely used + +- [x] Create a stream of LogMessageToL2 events +- [x] Get last synced block from Messaging DB +- [x] Consume the stream and log event +- [ ] Process message + - [x] Parse tx fee + - [x] Parse transaction from event + - [x] Check if message has already been processed + - [x] Build transaction + - [Waiting for mempool] Submit tx to mempool + - [x] Update Messaging DB +- [x] Handle Message Cancellation + +## Tests + +- E2E test #1 + + - Launch Anvil Node + - Launch Worker + - Send L1->L2 message + - Assert that event is emitted on L1 + - Assert that even was caught by the worker with correct data + - Assert the tx hash computed by the worker is correct + - Assert that the tx has been included in the mempool + - Assert that DB was correctly updated (last synced block & nonce) + - Assert that the tx was correctly executed + +- E2E test #2 + + - Should fail if we try to send multiple messages with same nonces + +- E2E test #3 + - Message Cancellation diff --git a/crates/client/eth/src/client.rs b/crates/client/eth/src/client.rs index 99198b52d..345b2e9bc 100644 --- a/crates/client/eth/src/client.rs +++ b/crates/client/eth/src/client.rs @@ -41,6 +41,7 @@ impl L1BlockMetrics { // The official starknet core contract ^ sol!( #[sol(rpc)] + #[derive(Debug)] StarknetCoreContract, "src/abis/starknet_core.json" ); diff --git a/crates/client/eth/src/l1_messaging.rs b/crates/client/eth/src/l1_messaging.rs new file mode 100644 index 000000000..87a9a3a72 --- /dev/null +++ b/crates/client/eth/src/l1_messaging.rs @@ -0,0 +1,541 @@ +use alloy::eips::BlockNumberOrTag; +use anyhow::Context; +use futures::StreamExt; +use std::sync::Arc; + +use crate::client::StarknetCoreContract::LogMessageToL2; +use crate::client::{EthereumClient, StarknetCoreContract}; +use crate::utils::u256_to_felt; +use alloy::primitives::{keccak256, FixedBytes, U256}; +use alloy::sol_types::SolValue; +use blockifier::transaction::transactions::L1HandlerTransaction as BlockifierL1HandlerTransaction; +use dc_db::{l1_db::LastSyncedEventBlock, DeoxysBackend}; +use dp_utils::channel_wait_or_graceful_shutdown; +use starknet_api::core::{ChainId, ContractAddress, EntryPointSelector, Nonce}; +use starknet_api::transaction::{ + Calldata, Fee, L1HandlerTransaction, Transaction, TransactionHash, TransactionVersion, +}; +use starknet_api::transaction_hash::get_transaction_hash; +use starknet_types_core::felt::Felt; + +impl EthereumClient { + /// Get cancellation status of an L1 to L2 message + /// + /// This function query the core contract to know if a L1->L2 message has been cancelled + /// # Arguments + /// + /// - msg_hash : Hash of L1 to L2 message + /// + /// # Return + /// + /// - A felt representing a timestamp : + /// - 0 if the message has not been cancelled + /// - timestamp of the cancellation if it has been cancelled + /// - An Error if the call fail + pub async fn get_l1_to_l2_message_cancellations(&self, msg_hash: FixedBytes<32>) -> anyhow::Result { + //l1ToL2MessageCancellations + let cancellation_timestamp = self.l1_core_contract.l1ToL2MessageCancellations(msg_hash).call().await?; + u256_to_felt(cancellation_timestamp._0) + } +} + +pub async fn sync(backend: &DeoxysBackend, client: &EthereumClient, chain_id: &ChainId) -> anyhow::Result<()> { + tracing::info!("⟠ Starting L1 Messages Syncing..."); + + let last_synced_event_block = match backend.messaging_last_synced_l1_block_with_event() { + Ok(Some(blk)) => blk, + Ok(None) => { + unreachable!("Should never be None") + } + Err(e) => { + tracing::error!("⟠ Madara Messaging DB unavailable: {:?}", e); + return Err(e.into()); + } + }; + let event_filter = client.l1_core_contract.event_filter::(); + let mut event_stream = event_filter + .from_block(last_synced_event_block.block_number) + .select(BlockNumberOrTag::Finalized) + .watch() + .await + .context("Failed to watch event filter")? + .into_stream(); + + while let Some(event_result) = channel_wait_or_graceful_shutdown(event_stream.next()).await { + if let Ok((event, meta)) = event_result { + tracing::info!( + "⟠ Processing L1 Message from block: {:?}, transaction_hash: {:?}, log_index: {:?}, fromAddress: {:?}", + meta.block_number, + meta.transaction_hash, + meta.log_index, + event.fromAddress + ); + + // Check if cancellation was initiated + let event_hash = get_l1_to_l2_msg_hash(&event)?; + tracing::info!("⟠ Checking for cancelation, event hash : {:?}", event_hash); + let cancellation_timestamp = client.get_l1_to_l2_message_cancellations(event_hash).await?; + if cancellation_timestamp != Felt::ZERO { + tracing::info!("⟠ L1 Message was cancelled in block at timestamp : {:?}", cancellation_timestamp); + let tx_nonce = Nonce(u256_to_felt(event.nonce)?); + // cancelled message nonce should be inserted to avoid reprocessing + match backend.has_l1_messaging_nonce(tx_nonce) { + Ok(false) => { + backend.set_l1_messaging_nonce(tx_nonce)?; + } + Ok(true) => {} + Err(e) => { + tracing::error!("⟠ Unexpected DB error: {:?}", e); + return Err(e.into()); + } + }; + continue; + } + + match process_l1_message(backend, &event, &meta.block_number, &meta.log_index, chain_id).await { + Ok(Some(tx_hash)) => { + tracing::info!( + "⟠ L1 Message from block: {:?}, transaction_hash: {:?}, log_index: {:?} submitted, \ + transaction hash on L2: {:?}", + meta.block_number, + meta.transaction_hash, + meta.log_index, + tx_hash + ); + } + Ok(None) => {} + Err(e) => { + tracing::error!( + "⟠ Unexpected error while processing L1 Message from block: {:?}, transaction_hash: {:?}, \ + log_index: {:?}, error: {:?}", + meta.block_number, + meta.transaction_hash, + meta.log_index, + e + ) + } + } + } + } + + Ok(()) +} + +async fn process_l1_message( + backend: &DeoxysBackend, + event: &LogMessageToL2, + l1_block_number: &Option, + event_index: &Option, + chain_id: &ChainId, +) -> anyhow::Result> { + let transaction = parse_handle_l1_message_transaction(event)?; + let tx_nonce = transaction.nonce; + + // Ensure that L1 message has not been executed + match backend.has_l1_messaging_nonce(tx_nonce) { + Ok(false) => { + backend.set_l1_messaging_nonce(tx_nonce)?; + } + Ok(true) => { + tracing::debug!("⟠ Event already processed: {:?}", transaction); + return Ok(None); + } + Err(e) => { + tracing::error!("⟠ Unexpected DB error: {:?}", e); + return Err(e.into()); + } + }; + + let tx_hash = get_transaction_hash(&Transaction::L1Handler(transaction.clone()), chain_id, &transaction.version)?; + let blockifier_transaction: BlockifierL1HandlerTransaction = + BlockifierL1HandlerTransaction { tx: transaction.clone(), tx_hash, paid_fee_on_l1: Fee(event.fee.try_into()?) }; + + // TODO: submit tx to mempool + + // TODO: remove unwraps + let block_sent = LastSyncedEventBlock::new(l1_block_number.unwrap(), event_index.unwrap()); + backend.messaging_update_last_synced_l1_block_with_event(block_sent)?; + + // TODO: replace by tx hash from mempool + Ok(Some(blockifier_transaction.tx_hash)) +} + +pub fn parse_handle_l1_message_transaction(event: &LogMessageToL2) -> anyhow::Result { + // L1 from address. + let from_address = u256_to_felt(event.fromAddress.into_word().into())?; + + // L2 contract to call. + let contract_address = u256_to_felt(event.toAddress)?; + + // Function of the contract to call. + let entry_point_selector = u256_to_felt(event.selector)?; + + // L1 message nonce. + let nonce = u256_to_felt(event.nonce)?; + + let event_payload = event.payload.clone().into_iter().map(u256_to_felt).collect::>>()?; + + let calldata: Calldata = { + let mut calldata: Vec<_> = Vec::with_capacity(event.payload.len() + 1); + calldata.push(from_address); + calldata.extend(event_payload); + + Calldata(Arc::new(calldata)) + }; + + Ok(L1HandlerTransaction { + nonce: Nonce(nonce), + contract_address: ContractAddress(contract_address.try_into()?), + entry_point_selector: EntryPointSelector(entry_point_selector), + calldata, + version: TransactionVersion(Felt::ZERO), + }) +} + +/// Computes the message hashed with the given event data +fn get_l1_to_l2_msg_hash(event: &LogMessageToL2) -> anyhow::Result> { + let data = ( + [0u8; 12], + event.fromAddress.0 .0, + event.toAddress, + event.nonce, + event.selector, + U256::from(event.payload.len()), + event.payload.clone(), + ); + Ok(keccak256(data.abi_encode_packed())) +} + +#[cfg(test)] +mod tests { + + use std::{sync::Arc, time::Duration}; + + use super::Felt; + use crate::{ + client::{ + EthereumClient, L1BlockMetrics, + StarknetCoreContract::{self, LogMessageToL2}, + }, + l1_messaging::get_l1_to_l2_msg_hash, + utils::felt_to_u256, + }; + use alloy::{ + hex::FromHex, + node_bindings::{Anvil, AnvilInstance}, + primitives::{Address, U256}, + providers::{ProviderBuilder, RootProvider}, + sol, + transports::http::{Client, Http}, + }; + use dc_db::DatabaseService; + use dc_metrics::MetricsService; + use dp_block::chain_config::ChainConfig; + use rstest::*; + use starknet_api::core::Nonce; + use tempfile::TempDir; + use tracing_test::traced_test; + use url::Url; + + use crate::l1_messaging::sync; + + use self::DummyContract::DummyContractInstance; + + struct TestRunner { + #[allow(dead_code)] + anvil: AnvilInstance, // Not used but needs to stay in scope otherwise it will be dropped + chain_config: Arc, + db_service: Arc, + dummy_contract: DummyContractInstance, RootProvider>>, + eth_client: EthereumClient, + } + + // LogMessageToL2 from https://etherscan.io/tx/0x21980d6674d33e50deee43c6c30ef3b439bd148249b4539ce37b7856ac46b843 + // bytecode is compiled DummyContractBasicTestCase + sol!( + #[derive(Debug)] + #[sol(rpc, bytecode="6080604052348015600e575f80fd5b506108258061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061004a575f3560e01c80634185df151461004e57806390985ef9146100585780639be446bf14610076578063af56443a146100a6575b5f80fd5b6100566100c2565b005b61006061013b565b60405161006d9190610488565b60405180910390f35b610090600480360381019061008b91906104cf565b6101ac565b60405161009d9190610512565b60405180910390f35b6100c060048036038101906100bb9190610560565b6101d8565b005b5f6100cb6101f3565b905080604001518160200151825f015173ffffffffffffffffffffffffffffffffffffffff167fdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b846060015185608001518660a0015160405161013093929190610642565b60405180910390a450565b5f806101456101f3565b9050805f015173ffffffffffffffffffffffffffffffffffffffff1681602001518260800151836040015184606001515185606001516040516020016101909695949392919061072a565b6040516020818303038152906040528051906020012091505090565b5f805f9054906101000a900460ff166101c5575f6101cb565b6366b4f1055b63ffffffff169050919050565b805f806101000a81548160ff02191690831515021790555050565b6101fb610429565b5f73ae0ee0a63a2ce6baeeffe56e7714fb4efe48d41990505f7f073314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b8290505f7f01b64b1b3b690b43b9b514fb81377518f4039cd3e4f4914d8a6bdf01d679fb1990505f600767ffffffffffffffff81111561027757610276610795565b5b6040519080825280602002602001820160405280156102a55781602001602082028036833780820191505090505b5090506060815f815181106102bd576102bc6107c2565b5b60200260200101818152505062195091816001815181106102e1576102e06107c2565b5b60200260200101818152505065231594f0c7ea81600281518110610308576103076107c2565b5b60200260200101818152505060058160038151811061032a576103296107c2565b5b602002602001018181525050624554488160048151811061034e5761034d6107c2565b5b60200260200101818152505073bdb193c166cfb7be2e51711c5648ebeef94063bb81600581518110610383576103826107c2565b5b6020026020010181815250507e7d79cd86ba27a2508a9ca55c8b3474ca082bc5173d0467824f07a32e9db888816006815181106103c3576103c26107c2565b5b6020026020010181815250505f662386f26fc1000090505f6040518060c001604052808773ffffffffffffffffffffffffffffffffffffffff16815260200186815260200185815260200184815260200183815260200182815250965050505050505090565b6040518060c001604052805f73ffffffffffffffffffffffffffffffffffffffff1681526020015f81526020015f8152602001606081526020015f81526020015f81525090565b5f819050919050565b61048281610470565b82525050565b5f60208201905061049b5f830184610479565b92915050565b5f80fd5b6104ae81610470565b81146104b8575f80fd5b50565b5f813590506104c9816104a5565b92915050565b5f602082840312156104e4576104e36104a1565b5b5f6104f1848285016104bb565b91505092915050565b5f819050919050565b61050c816104fa565b82525050565b5f6020820190506105255f830184610503565b92915050565b5f8115159050919050565b61053f8161052b565b8114610549575f80fd5b50565b5f8135905061055a81610536565b92915050565b5f60208284031215610575576105746104a1565b5b5f6105828482850161054c565b91505092915050565b5f81519050919050565b5f82825260208201905092915050565b5f819050602082019050919050565b6105bd816104fa565b82525050565b5f6105ce83836105b4565b60208301905092915050565b5f602082019050919050565b5f6105f08261058b565b6105fa8185610595565b9350610605836105a5565b805f5b8381101561063557815161061c88826105c3565b9750610627836105da565b925050600181019050610608565b5085935050505092915050565b5f6060820190508181035f83015261065a81866105e6565b90506106696020830185610503565b6106766040830184610503565b949350505050565b5f819050919050565b610698610693826104fa565b61067e565b82525050565b5f81905092915050565b6106b1816104fa565b82525050565b5f6106c283836106a8565b60208301905092915050565b5f6106d88261058b565b6106e2818561069e565b93506106ed836105a5565b805f5b8381101561071d57815161070488826106b7565b975061070f836105da565b9250506001810190506106f0565b5085935050505092915050565b5f6107358289610687565b6020820191506107458288610687565b6020820191506107558287610687565b6020820191506107658286610687565b6020820191506107758285610687565b60208201915061078582846106ce565b9150819050979650505050505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffdfea2646970667358221220ddc41ccc2cc8b33e1f608fb6cabf9ead1150daa8798e94e03ce9cd61e0d9389164736f6c634300081a0033")] + contract DummyContract { + bool isCanceled; + event LogMessageToL2(address indexed _fromAddress, uint256 indexed _toAddress, uint256 indexed _selector, uint256[] payload, uint256 nonce, uint256 fee); + + struct MessageData { + address fromAddress; + uint256 toAddress; + uint256 selector; + uint256[] payload; + uint256 nonce; + uint256 fee; + } + + function getMessageData() internal pure returns (MessageData memory) { + address fromAddress = address(993696174272377493693496825928908586134624850969); + uint256 toAddress = 3256441166037631918262930812410838598500200462657642943867372734773841898370; + uint256 selector = 774397379524139446221206168840917193112228400237242521560346153613428128537; + uint256[] memory payload = new uint256[](7); + payload[0] = 96; + payload[1] = 1659025; + payload[2] = 38575600093162; + payload[3] = 5; + payload[4] = 4543560; + payload[5] = 1082959358903034162641917759097118582889062097851; + payload[6] = 221696535382753200248526706088340988821219073423817576256483558730535647368; + uint256 nonce = 10000000000000000; + uint256 fee = 0; + + return MessageData(fromAddress, toAddress, selector, payload, nonce, fee); + } + + function fireEvent() public { + MessageData memory data = getMessageData(); + emit LogMessageToL2(data.fromAddress, data.toAddress, data.selector, data.payload, data.nonce, data.fee); + } + + function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) { + return isCanceled ? 1723134213 : 0; + } + + function setIsCanceled(bool value) public { + isCanceled = value; + } + + function getL1ToL2MsgHash() external pure returns (bytes32) { + MessageData memory data = getMessageData(); + return keccak256( + abi.encodePacked( + uint256(uint160(data.fromAddress)), + data.toAddress, + data.nonce, + data.selector, + data.payload.length, + data.payload + ) + ); + } + } + ); + + /// Common setup for tests + /// + /// This test performs the following steps: + /// 1. Sets up test environemment + /// 2. Starts worker + /// 3. Fires a Message event from the dummy contract + /// 4. Waits for event to be processed + /// 5. Assert that the worker handle the event with correct data + /// 6. Assert that the hash computed by the worker is correct + /// 7. TODO : Assert that the tx is succesfully submited to the mempool + /// 8. Assert that the event is successfully pushed to the db + /// 9. TODO : Assert that the tx was correctly executed + #[fixture] + async fn setup_test_env() -> TestRunner { + // Start Anvil instance + let anvil = Anvil::new().block_time(1).chain_id(1337).try_spawn().expect("failed to spawn anvil instance"); + println!("Anvil started and running at `{}`", anvil.endpoint()); + + // Set up chain info + let chain_config = Arc::new(ChainConfig::test_config()); + + // Set up database paths + let temp_dir = TempDir::new().expect("issue while creating temporary directory"); + let base_path = temp_dir.path().join("data"); + let backup_dir = Some(temp_dir.path().join("backups")); + + // Initialize database service + let db = Arc::new( + DatabaseService::new(&base_path, backup_dir, false, chain_config.clone()) + .await + .expect("Failed to create database service"), + ); + + // Set up metrics service + let prometheus_service = MetricsService::new(true, false, 9615).unwrap(); + let l1_block_metrics = L1BlockMetrics::register(&prometheus_service.registry()).unwrap(); + + // Set up provider + let rpc_url: Url = anvil.endpoint().parse().expect("issue while parsing"); + let provider = ProviderBuilder::new().on_http(rpc_url); + + // Set up dummy contract + let contract = DummyContract::deploy(provider.clone()).await.unwrap(); + + let core_contract = StarknetCoreContract::new(*contract.address(), provider.clone()); + + let eth_client = EthereumClient { + provider: Arc::new(provider.clone()), + l1_core_contract: core_contract.clone(), + l1_block_metrics: l1_block_metrics.clone(), + }; + + TestRunner { anvil, chain_config, db_service: db, dummy_contract: contract, eth_client } + } + + /// Test the basic workflow of l1 -> l2 messaging + /// + /// This test performs the following steps: + /// 1. Sets up test environemment + /// 2. Starts worker + /// 3. Fires a Message event from the dummy contract + /// 4. Waits for event to be processed + /// 5. Assert that the worker handle the event with correct data + /// 6. Assert that the hash computed by the worker is correct + /// 7. TODO : Assert that the tx is succesfully submited to the mempool + /// 8. Assert that the event is successfully pushed to the db + /// 9. TODO : Assert that the tx was correctly executed + #[rstest] + #[traced_test] + #[tokio::test] + async fn e2e_test_basic_workflow(#[future] setup_test_env: TestRunner) { + let TestRunner { chain_config, db_service: db, dummy_contract: contract, eth_client, anvil: _anvil } = + setup_test_env.await; + + // Start worker + let worker_handle = { + let db = Arc::clone(&db); + tokio::spawn(async move { sync(db.backend(), ð_client, &chain_config.chain_id).await }) + }; + + let _ = contract.setIsCanceled(false).send().await; + // Send a Event and wait for processing, Panic if fail + let _ = contract.fireEvent().send().await.expect("Failed to fire event"); + tokio::time::sleep(Duration::from_secs(5)).await; + + // Assert that event was caught by the worker with correct data + // TODO: Maybe add some more assert + assert!(logs_contain("fromAddress: 0xae0ee0a63a2ce6baeeffe56e7714fb4efe48d419")); + + // Assert the tx hash computed by the worker is correct + assert!(logs_contain( + format!("event hash : {:?}", contract.getL1ToL2MsgHash().call().await.expect("failed to get hash")._0) + .as_str() + )); + + // TODO : Assert that the tx has been included in the mempool + + // Assert that the event is well stored in db + let last_block = + db.backend().messaging_last_synced_l1_block_with_event().expect("failed to retrieve block").unwrap(); + assert_ne!(last_block.block_number, 0); + let nonce = Nonce(Felt::from_dec_str("10000000000000000").expect("failed to parse nonce string")); + assert!(db.backend().has_l1_messaging_nonce(nonce).unwrap()); + // TODO : Assert that the tx was correctly executed + + // Explicitly cancel the listen task, else it would be running in the background + worker_handle.abort(); + } + + /// Test the workflow of l1 -> l2 messaging with duplicate event + /// + /// This test performs the following steps: + /// 1. Sets up test environemment + /// 2. Starts worker + /// 3. Fires a Message event from the dummy contract + /// 4. Waits for event to be processed + /// 5. Assert that the event is well stored in db + /// 6. Fires a Message with the same event from the dummy contract + /// 7. Assert that the last event stored is the first one + #[rstest] + #[traced_test] + #[tokio::test] + async fn e2e_test_already_processed_event(#[future] setup_test_env: TestRunner) { + let TestRunner { chain_config, db_service: db, dummy_contract: contract, eth_client, anvil: _anvil } = + setup_test_env.await; + + // Start worker + let worker_handle = { + let db = Arc::clone(&db); + tokio::spawn(async move { sync(db.backend(), ð_client, &chain_config.chain_id).await }) + }; + + let _ = contract.setIsCanceled(false).send().await; + let _ = contract.fireEvent().send().await.expect("Failed to fire event"); + tokio::time::sleep(Duration::from_secs(5)).await; + let last_block = + db.backend().messaging_last_synced_l1_block_with_event().expect("failed to retrieve block").unwrap(); + assert_ne!(last_block.block_number, 0); + let nonce = Nonce(Felt::from_dec_str("10000000000000000").expect("failed to parse nonce string")); + assert!(db.backend().has_l1_messaging_nonce(nonce).unwrap()); + + // Send the event a second time + let _ = contract.fireEvent().send().await.expect("Failed to fire event"); + tokio::time::sleep(Duration::from_secs(5)).await; + // Assert that the last event in db is still the same as it is already processed (same nonce) + assert_eq!( + last_block.block_number, + db.backend() + .messaging_last_synced_l1_block_with_event() + .expect("failed to retrieve block") + .unwrap() + .block_number + ); + assert!(logs_contain("Event already processed")); + + worker_handle.abort(); + } + + /// Test the workflow of l1 -> l2 messaging with message cancelled + /// + /// This test performs the following steps: + /// 1. Sets up test environemment + /// 2. Starts worker + /// 3. Fires a Message event from the dummy contract + /// 4. Waits for event to be processed + /// 5. Assert that the event is not stored in db + #[rstest] + #[traced_test] + #[tokio::test] + async fn e2e_test_message_canceled(#[future] setup_test_env: TestRunner) { + let TestRunner { chain_config, db_service: db, dummy_contract: contract, eth_client, anvil: _anvil } = + setup_test_env.await; + + // Start worker + let worker_handle = { + let db = Arc::clone(&db); + tokio::spawn(async move { sync(db.backend(), ð_client, &chain_config.chain_id).await }) + }; + + // Mock cancelled message + let _ = contract.setIsCanceled(true).send().await; + let _ = contract.fireEvent().send().await.expect("Failed to fire event"); + tokio::time::sleep(Duration::from_secs(5)).await; + let last_block = + db.backend().messaging_last_synced_l1_block_with_event().expect("failed to retrieve block").unwrap(); + assert_eq!(last_block.block_number, 0); + let nonce = Nonce(Felt::from_dec_str("10000000000000000").expect("failed to parse nonce string")); + // cancelled message nonce should be inserted to avoid reprocessing + assert!(db.backend().has_l1_messaging_nonce(nonce).unwrap()); + assert!(logs_contain("L1 Message was cancelled in block at timestamp : 0x66b4f105")); + + worker_handle.abort(); + } + + /// Test taken from starknet.rs to ensure consistency + /// https://github.com/xJonathanLEI/starknet-rs/blob/2ddc69479d326ed154df438d22f2d720fbba746e/starknet-core/src/types/msg.rs#L96 + #[test] + fn test_msg_to_l2_hash() { + let msg = get_l1_to_l2_msg_hash(&LogMessageToL2 { + fromAddress: Address::from_hex("c3511006C04EF1d78af4C8E0e74Ec18A6E64Ff9e").unwrap(), + toAddress: felt_to_u256( + Felt::from_hex("0x73314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82").unwrap(), + ), + selector: felt_to_u256( + Felt::from_hex("0x2d757788a8d8d6f21d1cd40bce38a8222d70654214e96ff95d8086e684fbee5").unwrap(), + ), + payload: vec![ + felt_to_u256( + Felt::from_hex("0x689ead7d814e51ed93644bc145f0754839b8dcb340027ce0c30953f38f55d7").unwrap(), + ), + felt_to_u256(Felt::from_hex("0x2c68af0bb140000").unwrap()), + felt_to_u256(Felt::from_hex("0x0").unwrap()), + ], + nonce: U256::from(775628), + fee: U256::ZERO, + }) + .expect("Failed to compute l1 to l2 msg hash"); + + let expected_hash = + <[u8; 32]>::from_hex("c51a543ef9563ad2545342b390b67edfcddf9886aa36846cf70382362fc5fab3").unwrap(); + + assert_eq!(msg.0, expected_hash); + } +} diff --git a/crates/client/eth/src/lib.rs b/crates/client/eth/src/lib.rs index 5fd119649..a1cba6954 100644 --- a/crates/client/eth/src/lib.rs +++ b/crates/client/eth/src/lib.rs @@ -1,4 +1,5 @@ pub mod client; pub mod error; +pub mod l1_messaging; pub mod state_update; pub mod utils; diff --git a/crates/client/eth/src/state_update.rs b/crates/client/eth/src/state_update.rs index 8d3cc37d0..9d282c8b7 100644 --- a/crates/client/eth/src/state_update.rs +++ b/crates/client/eth/src/state_update.rs @@ -102,7 +102,7 @@ mod eth_client_event_subscription_test { use super::*; use std::{sync::Arc, time::Duration}; - use alloy::{providers::ProviderBuilder, sol}; + use alloy::{node_bindings::Anvil, providers::ProviderBuilder, sol}; use dc_db::DatabaseService; use dc_metrics::MetricsService; use dp_block::chain_config::ChainConfig; @@ -141,6 +141,10 @@ mod eth_client_event_subscription_test { #[rstest] #[tokio::test] async fn listen_and_update_state_when_event_fired_works() { + // Start Anvil instance + let anvil = Anvil::new().block_time(1).chain_id(1337).try_spawn().expect("failed to spawn anvil instance"); + println!("Anvil started and running at `{}`", anvil.endpoint()); + // Set up chain info let chain_info = Arc::new(ChainConfig::test_config()); @@ -160,7 +164,7 @@ mod eth_client_event_subscription_test { let prometheus_service = MetricsService::new(true, false, 9615).unwrap(); let l1_block_metrics = L1BlockMetrics::register(&prometheus_service.registry()).unwrap(); - let rpc_url: Url = "http://localhost:8545".parse().expect("issue while parsing"); + let rpc_url: Url = anvil.endpoint().parse().expect("issue while parsing"); let provider = ProviderBuilder::new().on_http(rpc_url); let contract = DummyContract::deploy(provider.clone()).await.unwrap(); diff --git a/crates/client/eth/src/utils.rs b/crates/client/eth/src/utils.rs index ed7a62760..8fc3bf965 100644 --- a/crates/client/eth/src/utils.rs +++ b/crates/client/eth/src/utils.rs @@ -28,6 +28,10 @@ pub fn u256_to_felt(u256: U256) -> anyhow::Result { Ok(felt) } +pub fn felt_to_u256(felt: Felt) -> U256 { + U256::from_be_bytes(felt.to_bytes_be()) +} + pub fn trim_hash(hash: &Felt) -> String { let hash_str = format!("{:#x}", hash); let hash_len = hash_str.len();