starknet_devnet_core/messaging/
ethereum.rs1#![allow(clippy::expect_used)]
2use std::collections::BTreeMap;
3use std::str::FromStr;
4
5use alloy::hex::ToHexExt;
6use alloy::primitives::{Address, B256, U256};
7use alloy::providers::{Provider, ProviderBuilder};
8use alloy::rpc::types::{BlockNumberOrTag, Filter, Log};
9use alloy::signers::Signer;
10use alloy::signers::local::{LocalSignerError, PrivateKeySigner};
11use alloy::sol;
12use alloy::sol_types::SolEvent;
13use alloy::transports::RpcError;
14use starknet_rs_core::types::{Felt, Hash256};
15use starknet_types::felt::felt_from_prefixed_hex;
16use starknet_types::rpc::contract_address::ContractAddress;
17use starknet_types::rpc::messaging::{MessageToL1, MessageToL2};
18use tracing::{trace, warn};
19use url::Url;
20
21use crate::error::{DevnetResult, Error, MessagingError};
22
23pub struct EthDevnetAccount {
24 pub address: &'static str,
25 pub private_key: &'static str,
26}
27
28pub const ETH_ACCOUNT_DEFAULT: EthDevnetAccount = EthDevnetAccount {
32 address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
33 private_key: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
34};
35
36impl<T> From<RpcError<T>> for Error {
37 fn from(e: RpcError<T>) -> Self {
38 Error::MessagingError(MessagingError::AlloyError(format!(
39 "RpcError: {:?}",
40 e.as_error_resp()
41 )))
42 }
43}
44
45impl From<LocalSignerError> for Error {
46 fn from(e: LocalSignerError) -> Self {
47 Error::MessagingError(MessagingError::AlloyError(format!("LocalSignerError: {e}")))
48 }
49}
50
51sol! {
52 #[sol(rpc)]
53 event LogMessageToL2(
54 address indexed from_address,
55 uint256 indexed to_address,
56 uint256 indexed selector,
57 uint256[] payload,
58 uint256 nonce,
59 uint256 fee
60 );
61}
62sol! {
63 #[sol(rpc)]
64 MockStarknetMessaging,
65 "contracts/l1-l2-artifacts/MockStarknetMessaging.json",
66}
67
68async fn assert_address_contains_any_code(
69 provider: &dyn Provider,
70 address: Address,
71) -> DevnetResult<()> {
72 let messaging_contract_code = provider.get_code_at(address).await.map_err(|e| {
73 Error::MessagingError(MessagingError::AlloyError(format!(
74 "Failed retrieving contract code at address {address}: {e}"
75 )))
76 })?;
77
78 if messaging_contract_code.is_empty() {
79 return Err(Error::MessagingError(MessagingError::AlloyError(format!(
80 "The specified address ({address:#x}) contains no contract"
81 ))));
82 }
83
84 Ok(())
85}
86
87#[derive(Clone)]
88pub struct EthereumMessaging {
90 wallet: PrivateKeySigner,
91 messaging_contract_address: Address,
92 node_url: Url,
93 pub(crate) last_fetched_block: u64,
95 }
98
99impl EthereumMessaging {
100 pub async fn new(
108 rpc_url: &str,
109 contract_address: Option<&str>,
110 deployer_account_private_key: Option<&str>,
111 ) -> DevnetResult<EthereumMessaging> {
112 let node_url: Url = rpc_url.parse().map_err(|e| {
113 Error::MessagingError(MessagingError::AlloyError(format!(
114 "Failed to parse RPC URL '{rpc_url}': {e}"
115 )))
116 })?;
117
118 let provider = ProviderBuilder::new().connect_http(node_url.clone());
119
120 let chain_id = provider.get_chain_id().await?;
121 let last_fetched_block = provider.get_block_number().await?;
122
123 let private_key = match deployer_account_private_key {
124 Some(private_key) => private_key,
125 None => ETH_ACCOUNT_DEFAULT.private_key,
126 };
127
128 let wallet = PrivateKeySigner::from_str(private_key)?.with_chain_id(chain_id.into());
129
130 let mut ethereum = EthereumMessaging {
131 wallet,
132 messaging_contract_address: Address::ZERO,
133 node_url,
134 last_fetched_block,
135 };
136
137 if let Some(address) = contract_address {
138 ethereum.messaging_contract_address = Address::from_str(address).map_err(|e| {
139 Error::MessagingError(MessagingError::AlloyError(format!(
140 "Address {address} can't be parsed from string: {e}",
141 )))
142 })?;
143
144 assert_address_contains_any_code(&provider, ethereum.messaging_contract_address)
145 .await?;
146 } else {
147 let cancellation_delay_seconds = U256::from(60 * 60 * 24);
148 ethereum.messaging_contract_address =
149 ethereum.deploy_messaging_contract(cancellation_delay_seconds).await?;
150 }
151
152 Ok(ethereum)
153 }
154 pub fn node_url(&self) -> String {
156 self.node_url.to_string()
157 }
158
159 pub fn messaging_contract_address(&self) -> Address {
161 self.messaging_contract_address
162 }
163
164 pub async fn fetch_messages(&mut self) -> DevnetResult<Vec<MessageToL2>> {
166 let provider =
167 ProviderBuilder::new().wallet(self.wallet.clone()).connect_http(self.node_url.clone());
168 let chain_latest_block = provider.get_block_number().await?;
169 let to_block = chain_latest_block;
170
171 let from_block = self.last_fetched_block + 1;
173 let mut messages = vec![];
174
175 self.fetch_logs(from_block, to_block).await?.into_iter().for_each(
176 |(block_number, block_logs)| {
177 trace!(
178 "Converting {} logs of block {block_number} into MessageToL2",
179 block_logs.len(),
180 );
181
182 block_logs.into_iter().for_each(|log| match message_to_l2_from_log(log) {
183 Ok(m) => messages.push(m),
184 Err(e) => warn!("Log from L1 node cannot be converted to MessageToL2: {e}"),
185 })
186 },
187 );
188
189 self.last_fetched_block = to_block;
190 Ok(messages)
191 }
192
193 pub async fn send_mock_messages(&self, messages: &[MessageToL1]) -> DevnetResult<()> {
200 if messages.is_empty() {
201 return Ok(());
202 }
203
204 let provider =
205 ProviderBuilder::new().wallet(self.wallet.clone()).connect_http(self.node_url.clone());
206 let contract = MockStarknetMessaging::new(self.messaging_contract_address, provider);
207
208 for message in messages {
209 let message_hash = U256::from_be_bytes(*message.hash().as_bytes());
210 trace!("Sending message to L1: [{:064x}]", message_hash);
211
212 let from_address = felt_to_u256(message.from_address.into());
213 let to_address = felt_to_u256(message.to_address.clone().into());
214 let payload = message.payload.iter().map(|f| felt_to_u256(*f)).collect::<Vec<_>>();
215
216 let tx = contract
217 .mockSendMessageFromL2(from_address, to_address, payload)
218 .send()
219 .await
220 .map_err(|e| {
221 Error::MessagingError(MessagingError::AlloyError(format!(
222 "Failed to send mock message from L2: {e}"
223 )))
224 })?;
225 match tx.get_receipt().await {
227 Ok(receipt) => trace!(
228 "Message {message_hash:064x} sent on L1 with transaction hash {:#x}",
229 receipt.transaction_hash
230 ),
231 Err(_) => {
232 return Err(Error::MessagingError(MessagingError::AlloyError(format!(
233 "No receipt found for the tx of message hash: {message_hash:064x}",
234 ))));
235 }
236 }
237 }
238
239 Ok(())
240 }
241
242 async fn fetch_logs(
254 &self,
255 from_block: u64,
256 to_block: u64,
257 ) -> DevnetResult<BTreeMap<u64, Vec<Log>>> {
258 trace!("Fetching logs for blocks {} - {}.", from_block, to_block);
259
260 let mut block_to_logs = BTreeMap::<u64, Vec<Log>>::new();
261
262 let provider = ProviderBuilder::new().connect_http(self.node_url.clone());
263
264 let log_msg_to_l2_topic =
266 B256::from_str("0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b")
267 .map_err(|err| {
268 Error::MessagingError(MessagingError::ConversionError(err.to_string()))
269 })?;
270
271 let filter = Filter::new()
272 .from_block(BlockNumberOrTag::Number(from_block))
273 .to_block(BlockNumberOrTag::Number(to_block))
274 .address(self.messaging_contract_address)
275 .event_signature(log_msg_to_l2_topic);
276
277 let logs = provider.get_logs(&filter).await?;
278
279 for log in logs {
280 if let Some(block_number) = log.block_number {
281 block_to_logs.entry(block_number).or_default().push(log);
282 }
283 }
284
285 Ok(block_to_logs)
286 }
287
288 pub async fn deploy_messaging_contract(
295 &self,
296 cancellation_delay_seconds: U256,
297 ) -> DevnetResult<Address> {
298 let provider =
299 ProviderBuilder::new().wallet(self.wallet.clone()).connect_http(self.node_url.clone());
300 let contract = MockStarknetMessaging::deploy(provider, cancellation_delay_seconds)
301 .await
302 .map_err(|e| {
303 Error::MessagingError(MessagingError::AlloyError(format!(
304 "Failed deploying MockStarknetMessaging contract: {e}"
305 )))
306 })?;
307
308 Ok(*contract.address())
309 }
310}
311
312pub fn message_to_l2_from_log(log: Log) -> DevnetResult<MessageToL2> {
318 let l1_transaction_hash = log.transaction_hash.map(|h| Hash256::from_bytes(*h));
319
320 let decoded = LogMessageToL2::decode_log(&log.inner).map_err(|e| {
321 Error::MessagingError(MessagingError::AlloyError(format!("Log parsing failed {e}")))
322 })?;
323
324 let from_address = address_to_felt(&decoded.from_address)?;
325 let contract_address = ContractAddress::new(u256_to_felt(&decoded.to_address)?)?;
326 let entry_point_selector = u256_to_felt(&decoded.selector)?;
327 let nonce = u256_to_felt(&decoded.nonce)?;
328 let paid_fee_on_l1 = u256_to_felt(&decoded.fee)?;
329 let payload = decoded.payload.iter().map(u256_to_felt).collect::<Result<_, _>>()?;
330
331 Ok(MessageToL2 {
332 l1_transaction_hash,
333 l2_contract_address: contract_address,
334 entry_point_selector,
335 l1_contract_address: ContractAddress::new(from_address)?,
336 payload,
337 paid_fee_on_l1,
338 nonce,
339 })
340}
341
342fn u256_to_felt(v: &U256) -> DevnetResult<Felt> {
348 Ok(Felt::from_bytes_be(&v.to_be_bytes()))
349}
350
351fn felt_to_u256(f: Felt) -> U256 {
357 U256::from_be_bytes(f.to_bytes_be())
358}
359
360fn address_to_felt(address: &Address) -> DevnetResult<Felt> {
366 Ok(felt_from_prefixed_hex(&format!("0x{}", address.encode_hex()))?)
367}
368
369#[cfg(test)]
370mod tests {
371
372 use super::*;
373
374 #[test]
375 fn test_message_to_l2_from_log() {
376 let from_address = "0x000000000000000000000000be3C44c09bc1a3566F3e1CA12e5AbA0fA4Ca72Be";
380 let to_address = "0x039dc79e64f4bb3289240f88e0bae7d21735bef0d1a51b2bf3c4730cb16983e1";
381 let selector = "0x02f15cff7b0eed8b9beb162696cf4e3e0e35fa7032af69cd1b7d2ac67a13f40f";
382 let nonce = 783082_u128;
383 let fee = 30000_u128;
384
385 let payload_buf = hex::decode("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000bf2ea0000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002").unwrap();
387
388 let payload: Vec<Felt> = vec![1.into(), 2.into()];
389
390 let inner = alloy::primitives::Log {
391 address: Address::from_str("0xde29d060D45901Fb19ED6C6e959EB22d8626708e").unwrap(),
392 data: alloy::primitives::LogData::new_unchecked(
393 vec![
394 B256::from_str(
395 "0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b",
396 )
397 .unwrap(),
398 B256::from_str(from_address).unwrap(),
399 B256::from_str(to_address).unwrap(),
400 B256::from_str(selector).unwrap(),
401 ],
402 payload_buf.clone().into(),
403 ),
404 };
405
406 let expected_message = MessageToL2 {
407 l1_transaction_hash: None,
408 l1_contract_address: ContractAddress::new(
409 felt_from_prefixed_hex(from_address).unwrap(),
410 )
411 .unwrap(),
412 l2_contract_address: ContractAddress::new(felt_from_prefixed_hex(to_address).unwrap())
413 .unwrap(),
414 entry_point_selector: felt_from_prefixed_hex(selector).unwrap(),
415 payload,
416 nonce: nonce.into(),
417 paid_fee_on_l1: fee.into(),
418 };
419 let log = Log { inner, block_number: None, transaction_hash: None, ..Default::default() };
420
421 let message = message_to_l2_from_log(log).unwrap();
422
423 assert_eq!(message, expected_message);
424 }
425}