kona_interop/
message.rs

1//! Interop message primitives.
2//!
3//! <https://specs.optimism.io/interop/messaging.html#messaging>
4//! <https://github.com/ethereum-optimism/optimism/blob/34d5f66ade24bd1f3ce4ce7c0a6cfc1a6540eca1/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol>
5
6use alloc::{vec, vec::Vec};
7use alloy_primitives::{Bytes, ChainId, Log, keccak256};
8use alloy_sol_types::{SolEvent, sol};
9use derive_more::{AsRef, Constructor, From};
10use kona_protocol::Predeploys;
11use op_alloy_consensus::OpReceiptEnvelope;
12
13sol! {
14    /// @notice The struct for a pointer to a message payload in a remote (or local) chain.
15    #[derive(Default, Debug, PartialEq, Eq)]
16    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17    struct MessageIdentifier {
18        address origin;
19        uint256 blockNumber;
20        uint256 logIndex;
21        uint256 timestamp;
22        #[cfg_attr(feature = "serde", serde(rename = "chainID"))]
23        uint256 chainId;
24    }
25
26    /// @notice Emitted when a cross chain message is being executed.
27    /// @param payloadHash Hash of message payload being executed.
28    /// @param identifier Encoded Identifier of the message.
29    ///
30    /// Parameter names are derived from the `op-supervisor` JSON field names.
31    /// See the relevant definition in the Optimism repository:
32    /// [Ethereum-Optimism/op-supervisor](https://github.com/ethereum-optimism/optimism/blob/4ba2eb00eafc3d7de2c8ceb6fd83913a8c0a2c0d/op-supervisor/supervisor/types/types.go#L61-L64).
33    #[derive(Default, Debug, PartialEq, Eq)]
34    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35    event ExecutingMessage(bytes32 indexed payloadHash, MessageIdentifier identifier);
36
37    /// @notice Executes a cross chain message on the destination chain.
38    /// @param _id      Identifier of the message.
39    /// @param _target  Target address to call.
40    /// @param _message Message payload to call target with.
41    function executeMessage(
42        MessageIdentifier calldata _id,
43        address _target,
44        bytes calldata _message
45    ) external;
46}
47
48/// A [RawMessagePayload] is the raw payload of an initiating message.
49#[derive(Debug, Clone, From, AsRef, PartialEq, Eq)]
50pub struct RawMessagePayload(Bytes);
51
52impl From<&Log> for RawMessagePayload {
53    fn from(log: &Log) -> Self {
54        let mut data = vec![0u8; log.topics().len() * 32 + log.data.data.len()];
55        for (i, topic) in log.topics().iter().enumerate() {
56            data[i * 32..(i + 1) * 32].copy_from_slice(topic.as_ref());
57        }
58        data[(log.topics().len() * 32)..].copy_from_slice(log.data.data.as_ref());
59        data.into()
60    }
61}
62
63impl From<Vec<u8>> for RawMessagePayload {
64    fn from(data: Vec<u8>) -> Self {
65        Self(Bytes::from(data))
66    }
67}
68
69impl From<executeMessageCall> for ExecutingMessage {
70    fn from(call: executeMessageCall) -> Self {
71        Self { identifier: call._id, payloadHash: keccak256(call._message.as_ref()) }
72    }
73}
74
75/// An [`ExecutingDescriptor`] is a part of the payload to `supervisor_checkAccessList`
76/// Spec: <https://github.com/ethereum-optimism/specs/blob/main/specs/interop/supervisor.md#executingdescriptor>
77#[derive(Default, Debug, PartialEq, Eq, Clone, Constructor)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct ExecutingDescriptor {
80    /// The timestamp used to enforce timestamp [invariant](https://github.com/ethereum-optimism/specs/blob/main/specs/interop/derivation.md#invariants)
81    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
82    pub timestamp: u64,
83    /// The timeout that requests verification to still hold at `timestamp+timeout`
84    /// (message expiry may drop previously valid messages).
85    #[cfg_attr(
86        feature = "serde",
87        serde(
88            default,
89            skip_serializing_if = "Option::is_none",
90            with = "alloy_serde::quantity::opt"
91        )
92    )]
93    pub timeout: Option<u64>,
94    /// Chain ID of the chain that the message was executed on.
95    #[cfg_attr(
96        feature = "serde",
97        serde(
98            default,
99            rename = "chainID",
100            skip_serializing_if = "Option::is_none",
101            with = "alloy_serde::quantity::opt"
102        )
103    )]
104    pub chain_id: Option<ChainId>,
105}
106
107/// A wrapper type for [ExecutingMessage] containing the chain ID of the chain that the message was
108/// executed on.
109#[derive(Debug)]
110pub struct EnrichedExecutingMessage {
111    /// The inner [ExecutingMessage].
112    pub inner: ExecutingMessage,
113    /// The chain ID of the chain that the message was executed on.
114    pub executing_chain_id: u64,
115    /// The timestamp of the block that the executing message was included in.
116    pub executing_timestamp: u64,
117}
118
119impl EnrichedExecutingMessage {
120    /// Create a new [EnrichedExecutingMessage] from an [ExecutingMessage] and a chain ID.
121    pub const fn new(
122        inner: ExecutingMessage,
123        executing_chain_id: u64,
124        executing_timestamp: u64,
125    ) -> Self {
126        Self { inner, executing_chain_id, executing_timestamp }
127    }
128}
129
130/// Extracts all [ExecutingMessage] events from list of [OpReceiptEnvelope]s.
131///
132/// See [`parse_log_to_executing_message`].
133///
134/// Note: filters out logs that don't contain executing message events.
135pub fn extract_executing_messages(receipts: &[OpReceiptEnvelope]) -> Vec<ExecutingMessage> {
136    receipts.iter().fold(Vec::new(), |mut acc, envelope| {
137        let executing_messages = envelope.logs().iter().filter_map(parse_log_to_executing_message);
138
139        acc.extend(executing_messages);
140        acc
141    })
142}
143
144/// Parses [`Log`]s to [`ExecutingMessage`]s.
145///
146/// See [`parse_log_to_executing_message`] for more details. Return iterator maps 1-1 with input.
147pub fn parse_logs_to_executing_msgs<'a>(
148    logs: impl Iterator<Item = &'a Log>,
149) -> impl Iterator<Item = Option<ExecutingMessage>> {
150    logs.map(parse_log_to_executing_message)
151}
152
153/// Parse [`Log`] to [`ExecutingMessage`], if any.
154///
155/// Max one [`ExecutingMessage`] event can exist per log. Returns `None` if log doesn't contain
156/// executing message event.
157pub fn parse_log_to_executing_message(log: &Log) -> Option<ExecutingMessage> {
158    (log.address == Predeploys::CROSS_L2_INBOX && log.topics().len() == 2)
159        .then(|| ExecutingMessage::decode_log_data(&log.data).ok())
160        .flatten()
161}
162
163#[cfg(test)]
164mod tests {
165    use alloy_primitives::{Address, B256, LogData, U256};
166
167    use super::*;
168
169    // Test the serialization of ExecutingDescriptor
170    #[cfg(feature = "serde")]
171    #[test]
172    fn test_serialize_executing_descriptor() {
173        let descriptor = ExecutingDescriptor {
174            timestamp: 1234567890,
175            timeout: Some(3600),
176            chain_id: Some(1000),
177        };
178        let serialized = serde_json::to_string(&descriptor).unwrap();
179        let expected = r#"{"timestamp":"0x499602d2","timeout":"0xe10","chainID":"0x3e8"}"#;
180        assert_eq!(serialized, expected);
181
182        let deserialized: ExecutingDescriptor = serde_json::from_str(&serialized).unwrap();
183        assert_eq!(descriptor, deserialized);
184    }
185
186    #[cfg(feature = "serde")]
187    #[test]
188    fn test_deserialize_executing_descriptor_missing_chain_id() {
189        let json = r#"{
190            "timestamp": "0x499602d2",
191            "timeout": "0xe10"
192        }"#;
193
194        let expected =
195            ExecutingDescriptor { timestamp: 1234567890, timeout: Some(3600), chain_id: None };
196
197        let deserialized: ExecutingDescriptor = serde_json::from_str(json).unwrap();
198        assert_eq!(deserialized, expected);
199    }
200
201    #[cfg(feature = "serde")]
202    #[test]
203    fn test_deserialize_executing_descriptor_missing_timeout() {
204        let json = r#"{
205            "timestamp": "0x499602d2",
206            "chainID": "0x3e8"
207        }"#;
208
209        let expected =
210            ExecutingDescriptor { timestamp: 1234567890, timeout: None, chain_id: Some(1000) };
211
212        let deserialized: ExecutingDescriptor = serde_json::from_str(json).unwrap();
213        assert_eq!(deserialized, expected);
214    }
215
216    #[test]
217    fn test_parse_logs_to_executing_msgs_iterator() {
218        // One valid, one invalid log
219        let identifier = MessageIdentifier {
220            origin: Address::repeat_byte(0x77),
221            blockNumber: U256::from(200),
222            logIndex: U256::from(3),
223            timestamp: U256::from(777777),
224            chainId: U256::from(12),
225        };
226        let payload_hash = B256::repeat_byte(0x88);
227        let event = ExecutingMessage { payloadHash: payload_hash, identifier };
228        let data = ExecutingMessage::encode_log_data(&event);
229
230        let valid_log = Log { address: Predeploys::CROSS_L2_INBOX, data };
231        let invalid_log = Log {
232            address: Address::repeat_byte(0x99),
233            data: LogData::new_unchecked([B256::ZERO, B256::ZERO].to_vec(), Bytes::default()),
234        };
235
236        let logs = vec![&valid_log, &invalid_log];
237        let mut iter = parse_logs_to_executing_msgs(logs.into_iter());
238        assert_eq!(iter.next().unwrap().unwrap(), event);
239        assert!(iter.next().unwrap().is_none());
240    }
241}