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}