starknet_devnet_core/messaging/
ethereum.rs

1#![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
28/// Default account 0 for most used ethereum devnets (at least hardhat and anvil).
29/// Mnemonic: test test test test test test test test test test test junk
30/// Derivation path: m/44'/60'/0'/0/
31pub 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)]
88/// Ethereum related configuration and types.
89pub struct EthereumMessaging {
90    wallet: PrivateKeySigner,
91    messaging_contract_address: Address,
92    node_url: Url,
93    /// This value must be dumped to avoid re-fetching already processed messages.
94    pub(crate) last_fetched_block: u64,
95    // A nonce verification may be added, with a nonce counter here.
96    // If so, it must be dumped too.
97}
98
99impl EthereumMessaging {
100    /// Instantiates a new `EthereumMessaging`.
101    ///
102    /// # Arguments
103    ///
104    /// * `rpc_url` - The L1 node RPC URL.
105    /// * `contract_address` - The messaging contract address deployed on L1 node.
106    /// * `deployer_account_private_key` - The private key of the funded account on L1 node to
107    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    /// Returns the url of the ethereum node currently in used.
155    pub fn node_url(&self) -> String {
156        self.node_url.to_string()
157    }
158
159    /// Returns address of the messaging contract on L1 node.
160    pub fn messaging_contract_address(&self) -> Address {
161        self.messaging_contract_address
162    }
163
164    /// Fetches all the messages that were not already fetched from the L1 node.
165    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        // +1 exclude the latest fetched block the last time this function was called.
172        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    /// Sends the list of given messages to L1. The messages are sent to
194    /// the mocked contract, `mockSendMessageFromL2` entrypoint.
195    ///
196    /// # Arguments
197    ///
198    /// * `messages` - The list of messages to be sent.
199    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            // Wait for transaction receipt
226            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    /// Fetches logs in the given block range and returns a `HashMap` with the list of logs for each
243    /// block number.
244    ///
245    /// There is no pagination on ethereum, and no hard limit on block range.
246    /// Fetching too much blocks may result in RPC request error.
247    /// For this reason, the caller may wisely choose the range.
248    ///
249    /// # Arguments
250    ///
251    /// * `from_block` - The first (included) block of which logs must be fetched.
252    /// * `to_block` - The last (included) block of which logs must be fetched.
253    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        // `sendMessageToL2` topic.
265        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    /// Deploys an instance of the `MockStarknetMessaging` contract and returns it's address.
289    ///
290    /// # Arguments
291    ///
292    /// * `cancellation_delay_seconds` - Cancellation delay in seconds passed to the contract's
293    ///   constructor.
294    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
312/// Converts an ethereum log into a `MessageToL2`.
313///
314/// # Arguments
315///
316/// * `log` - The log to be converted.
317pub 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
342/// Converts an `U256` into a `Felt`.
343///
344/// # Arguments
345///
346/// * `v` - The `U256` to be converted.
347fn u256_to_felt(v: &U256) -> DevnetResult<Felt> {
348    Ok(Felt::from_bytes_be(&v.to_be_bytes()))
349}
350
351/// Converts an `Felt` into a `U256`.
352///
353/// # Arguments
354///
355/// * `f` - The `Felt` to be converted.
356fn felt_to_u256(f: Felt) -> U256 {
357    U256::from_be_bytes(f.to_bytes_be())
358}
359
360/// Converts an `Address` into a `Felt`.
361///
362/// # Arguments
363///
364/// * `address` - The `Address` to be converted.
365fn 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        // Test based on Goerli tx hash:
377        // 0x6182c63599a9638272f1ce5b5cadabece9c81c2d2b8f88ab7a294472b8fce8b
378
379        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        // Payload two values: [1, 2].
386        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}