starknet_devnet_core/messaging/
mod.rs

1//! Messaging module.
2//!
3//! This module contains code related to messaging feature.
4//! The messaging is composed of two major actors:
5//!   1. The Starknet sequencer, which is in charge of gathering messages from L1 and executing
6//!      them.
7//!   2. The Starknet Core Contract, an Ethereum contract, that is generating the logs to send
8//!      message to L2 and computing/ref-counting messages hashes for messages sent to L1.
9//!
10//! Being a devnet, this project is embedding an Ethereum contract (MockStarknetMessaging)
11//! that mocks the behavior of the Starknet Core Contract by adding a method to manually
12//! increment the ref-counting of message hashes.
13//! This ensures that messages can be consumed on L1 without actually waiting for the
14//! proof to be generated (at it is done on Starknet in production).
15//!
16//! # Receive message from L1
17//! The Starknet sequencer (the devnet being the sequencer in this project)
18//! is in charge of fetching the logs from Starknet Core Contract from Ethereum network.
19//! In this project, the logs are emitted by the MockStarknetMessaging contract method
20//! `sendMessageToL2`.
21//! Once a log is gathered, a `L1HandlerTransaction` is executed internally, without
22//! being signed by any account.
23//!
24//! # Send message to L1
25//! To send messages to L1, any Starknet contract can use the `send_message_to_l1` syscall.
26//! This will have the effect of adding, in the transaction output, the content
27//! of the message.
28//! By collecting those messages from transactions output, the devnet
29//! uses the mocked functionality of manually incrementing the ref-count of a message
30//! to make it available for consumption on L1.
31//! This is done my sending a transaction to the Ethereum node, to the MockStarknetMessaging
32//! contract (`mockSendMessageFromL2` entrypoint).
33use std::collections::HashMap;
34
35use alloy::primitives::B256;
36use starknet_rs_core::types::{ExecutionResult, Felt, Hash256};
37use starknet_types::rpc::block::BlockId;
38use starknet_types::rpc::messaging::{MessageToL1, MessageToL2};
39
40use crate::StarknetBlock;
41use crate::error::{DevnetResult, Error, MessagingError};
42use crate::starknet::Starknet;
43use crate::traits::HashIdentified;
44
45pub mod ethereum;
46pub use ethereum::EthereumMessaging;
47
48#[derive(Default)]
49pub struct MessagingBroker {
50    /// The ethereum broker to send transaction / call contracts using alloy.
51    pub(crate) ethereum: Option<EthereumMessaging>,
52    /// The last local (starknet) block for which messages have been collected
53    /// and sent.
54    pub last_local_block: u64,
55    /// A local queue of `MessageToL1` hashes generated by cairo contracts.
56    /// For each time a message is supposed to be sent to L1, it is stored in this
57    /// queue. The user may consume those messages using `consume_message_from_l2`
58    /// to actually test `MessageToL1` emitted without running L1 node.
59    pub l2_to_l1_messages_hashes: HashMap<B256, u64>,
60    /// This list of messages that will be sent to L1 node at the next `postman/flush`.
61    pub l2_to_l1_messages_to_flush: Vec<MessageToL1>,
62    /// Mapping of L1 transaction hash to a chronological sequence of generated L2 transactions.
63    pub l1_to_l2_tx_hashes: HashMap<B256, Vec<Felt>>,
64}
65
66impl MessagingBroker {
67    /// Configures the ethereum broker.
68    ///
69    /// # Arguments
70    ///
71    /// * `ethereum_messaging` - The `EthereumMessaging` to use as broker.
72    pub fn configure_ethereum(&mut self, ethereum_messaging: EthereumMessaging) {
73        self.ethereum = Some(ethereum_messaging);
74    }
75
76    /// Returns the url of the ethereum node currently in used, or `None` otherwise.
77    pub fn ethereum_url(&self) -> Option<String> {
78        self.ethereum.as_ref().map(|m| m.node_url())
79    }
80
81    /// Returns a reference to the ethereum instance if configured, an error otherwise.
82    pub fn ethereum_ref(&self) -> DevnetResult<&EthereumMessaging> {
83        self.ethereum.as_ref().ok_or(Error::MessagingError(MessagingError::NotConfigured))
84    }
85
86    /// Returns a mutable reference to the ethereum instance if configured, an error otherwise.
87    pub fn ethereum_mut(&mut self) -> DevnetResult<&mut EthereumMessaging> {
88        self.ethereum.as_mut().ok_or(Error::MessagingError(MessagingError::NotConfigured))
89    }
90}
91
92impl Starknet {
93    /// Configures the messaging from the given L1 node parameters.
94    /// Calling this function multiple time will overwrite the previous
95    /// configuration, if any.
96    ///
97    /// # Arguments
98    ///
99    /// * `rpc_url` - The L1 node RPC URL.
100    /// * `contract_address` - The messaging contract address deployed on L1 node.
101    /// * `deployer_account_private_key` - The private key of the funded account on L1 node to
102    ///   perform the role of signer.
103    pub async fn configure_messaging(
104        &mut self,
105        rpc_url: &str,
106        contract_address: Option<&str>,
107        deployer_account_private_key: Option<&str>,
108    ) -> DevnetResult<String> {
109        tracing::trace!("Configuring messaging: {}", rpc_url);
110
111        self.messaging.configure_ethereum(
112            EthereumMessaging::new(rpc_url, contract_address, deployer_account_private_key).await?,
113        );
114
115        Ok(format!("0x{:x}", self.messaging.ethereum_ref()?.messaging_contract_address()))
116    }
117
118    /// Retrieves the ethereum node URL, if configured.
119    pub fn get_ethereum_url(&self) -> Option<String> {
120        self.messaging.ethereum_url()
121    }
122
123    /// Sets the latest local block processed by messaging.
124    pub fn set_latest_local_block(&mut self, latest_local_block: u64) {
125        self.messaging.last_local_block = latest_local_block;
126    }
127
128    /// Collects all messages found between
129    /// the current messaging latest block and the Latest Starknet block,
130    /// including both blocks.
131    /// This function register the messages in two fashions:
132    /// 1. Add each message to the `l2_to_l1_messages_to_flush`.
133    /// 2. Increment the counter for the hash of each message into `l2_to_l1_messages_hashes`.
134    ///
135    /// Returns all the messages currently collected and not flushed.
136    pub async fn collect_messages_to_l1(&mut self) -> DevnetResult<Vec<MessageToL1>> {
137        let from_block = if self.messaging.last_local_block != 0 {
138            self.messaging.last_local_block
139        } else {
140            self.blocks.starting_block_number
141        };
142
143        let pre_confirmed_block_number = self.blocks.pre_confirmed_block.block_number().0;
144        // if pre_confirmed_block_number is 0, there is no L2 confirmed block to process
145        if pre_confirmed_block_number == 0 {
146            return Ok(vec![]);
147        }
148
149        let to_block = pre_confirmed_block_number - 1;
150
151        match self
152            .blocks
153            .get_blocks(Some(BlockId::Number(from_block)), Some(BlockId::Number(to_block)))
154        {
155            Ok(blocks) => {
156                let mut messages = vec![];
157
158                let mut last_processed_block: u64 = 0;
159                for block in blocks {
160                    messages.extend(self.get_block_messages(block)?);
161                    last_processed_block = block.header.block_header_without_hash.block_number.0;
162                }
163
164                for message in &messages {
165                    let hash = B256::new(*message.hash().as_bytes());
166                    let count = self.messaging.l2_to_l1_messages_hashes.entry(hash).or_insert(0);
167                    *count += 1;
168                }
169
170                // +1 to avoid latest block to be processed twice.
171                self.messaging.last_local_block = last_processed_block + 1;
172
173                self.messaging.l2_to_l1_messages_to_flush.extend(messages);
174
175                Ok(self.messaging.l2_to_l1_messages_to_flush.clone())
176            }
177            Err(Error::NoBlock) => {
178                // We're 1 block ahead of latest block, no messages can be collected.
179                Ok(self.messaging.l2_to_l1_messages_to_flush.clone())
180            }
181            Err(e) => Err(e),
182        }
183    }
184
185    /// Sends (flush) all the messages in `l2_to_l1_messages_to_flush` to L1 node.
186    /// Returns the list of sent messages.
187    pub async fn send_messages_to_l1(&mut self) -> DevnetResult<Vec<MessageToL1>> {
188        let ethereum = self.messaging.ethereum_ref()?;
189        ethereum.send_mock_messages(&self.messaging.l2_to_l1_messages_to_flush).await?;
190
191        let messages = self.messaging.l2_to_l1_messages_to_flush.clone();
192        self.messaging.l2_to_l1_messages_to_flush.clear();
193
194        Ok(messages)
195    }
196
197    /// Consumes a `MessageToL1` that is registered in `l2_to_l1_messages`.
198    /// If the count related to the message is hash is already 0, an error is returned,
199    /// the message's hash otherwise.
200    ///
201    /// # Arguments
202    ///
203    /// * `message` - The message to consume.
204    pub async fn consume_l2_to_l1_message(
205        &mut self,
206        message: &MessageToL1,
207    ) -> DevnetResult<Hash256> {
208        // Ensure latest messages are collected before consuming the message.
209        self.collect_messages_to_l1().await?;
210
211        let hash = B256::new(*message.hash().as_bytes());
212        let count = self.messaging.l2_to_l1_messages_hashes.entry(hash).or_insert(0);
213
214        if *count > 0 {
215            *count -= 1;
216            Ok(message.hash())
217        } else {
218            Err(Error::MessagingError(MessagingError::MessageToL1NotPresent(hash.to_string())))
219        }
220    }
221
222    /// Fetches all messages from L1 and converts the ethereum log into `MessageToL2`.
223    pub async fn fetch_messages_to_l2(&mut self) -> DevnetResult<Vec<MessageToL2>> {
224        let ethereum = self.messaging.ethereum_mut()?;
225        let messages = ethereum.fetch_messages().await?;
226        Ok(messages)
227    }
228
229    /// Collects all messages for all the transactions of the given block.
230    ///
231    /// # Arguments
232    ///
233    /// * `block` - The block from which messages are collected.
234    fn get_block_messages(&self, block: &StarknetBlock) -> DevnetResult<Vec<MessageToL1>> {
235        let mut messages = vec![];
236
237        block.get_transactions().iter().for_each(|transaction_hash| {
238            if let Ok(transaction) =
239                self.transactions.get_by_hash(*transaction_hash).ok_or(Error::NoTransaction)
240            {
241                // As we will send the messages to L1 node, we don't want to include
242                // the messages of reverted transactions.
243                if let ExecutionResult::Succeeded = transaction.execution_result {
244                    messages.extend(transaction.get_l2_to_l1_messages())
245                }
246            }
247        });
248
249        Ok(messages)
250    }
251}