+///
+/// [MessageIdentifier]: crate::MessageIdentifier
+#[derive(Debug)]
+pub struct MessageGraph {
+ /// The horizon timestamp is the highest timestamp of all blocks containing [ExecutingMessage]s
+ /// within the graph.
+ ///
+ /// [ExecutingMessage]: crate::ExecutingMessage
+ horizon_timestamp: u64,
+ /// The edges within the graph.
+ ///
+ /// These are derived from the transactions within the blocks.
+ messages: Vec,
+ /// The data provider for the graph. Required for fetching headers, receipts and remote
+ /// messages within history during resolution.
+ provider: P,
+}
+
+impl MessageGraph
+where
+ P: InteropProvider,
+{
+ /// Derives the edges from the blocks within the graph by scanning all receipts within the
+ /// blocks and searching for [ExecutingMessage]s.
+ ///
+ /// [ExecutingMessage]: crate::ExecutingMessage
+ pub async fn derive(blocks: &[(u64, Sealed)], provider: P) -> MessageGraphResult {
+ info!(
+ target: "message-graph",
+ "Deriving message graph from {} blocks.",
+ blocks.len()
+ );
+
+ // Get the highest timestamp from the blocks. This serves as the horizon timestamp for the
+ // graph.
+ let horizon_timestamp = blocks
+ .iter()
+ .map(|(_, header)| header.inner().timestamp)
+ .max()
+ .ok_or(MessageGraphError::EmptyDependencySet)?;
+
+ let mut messages = Vec::with_capacity(blocks.len());
+ for (chain_id, header) in blocks.iter() {
+ let receipts = provider.receipts_by_hash(*chain_id, header.hash()).await?;
+ let executing_messages = extract_executing_messages(receipts.as_slice());
+
+ messages.extend(
+ executing_messages
+ .into_iter()
+ .map(|message| EnrichedExecutingMessage::new(message, *chain_id)),
+ );
+ }
+
+ info!(
+ target: "message-graph",
+ "Derived {} executing messages from {} blocks.",
+ messages.len(),
+ blocks.len()
+ );
+ Ok(Self { horizon_timestamp, messages, provider })
+ }
+
+ /// Checks the validity of all messages within the graph.
+ pub async fn resolve(mut self) -> MessageGraphResult<()> {
+ info!(
+ target: "message-graph",
+ "Checking the message graph for invalid messages."
+ );
+
+ // Reduce the graph to remove all valid messages.
+ self.reduce().await?;
+
+ // Check if the graph is now empty. If not, there are invalid messages.
+ if !self.messages.is_empty() {
+ // Collect the chain IDs for all blocks containing invalid messages.
+ let mut bad_block_chain_ids =
+ self.messages.into_iter().map(|e| e.executing_chain_id).collect::>();
+ bad_block_chain_ids.dedup_by(|a, b| a == b);
+
+ warn!(
+ target: "message-graph",
+ "Failed to reduce the message graph entirely. Invalid messages found in chains {}",
+ bad_block_chain_ids
+ .iter()
+ .map(|id| alloc::format!("{}", id))
+ .collect::>()
+ .join(", ")
+ );
+
+ // Return an error with the chain IDs of the blocks containing invalid messages.
+ return Err(MessageGraphError::InvalidMessages(bad_block_chain_ids));
+ }
+
+ Ok(())
+ }
+
+ /// Attempts to remove as many edges from the graph as possible by resolving the dependencies
+ /// of each message. If a message cannot be resolved, it is considered invalid. After this
+ /// function is called, any outstanding messages are invalid.
+ async fn reduce(&mut self) -> MessageGraphResult<()> {
+ // Create a new vector to store invalid edges
+ let mut invalid_messages = Vec::with_capacity(self.messages.len());
+
+ // Prune all valid edges.
+ for message in core::mem::take(&mut self.messages) {
+ if let Err(e) = self.check_single_dependency(&message).await {
+ warn!(
+ target: "message-graph",
+ "Invalid ExecutingMessage found - relayed on chain {} with message hash {}.",
+ message.executing_chain_id,
+ hex::encode(message.inner.msgHash)
+ );
+ warn!("Invalid message error: {}", e);
+ invalid_messages.push(message);
+ }
+ }
+
+ info!(
+ target: "message-graph",
+ "Successfully reduced the message graph. {} invalid messages found.",
+ invalid_messages.len()
+ );
+
+ // Replace the old edges with the filtered list
+ self.messages = invalid_messages;
+
+ Ok(())
+ }
+
+ /// Checks the dependency of a single [EnrichedExecutingMessage]. If the message's dependencies
+ /// are unavailable, the message is considered invalid and an [Err] is returned.
+ async fn check_single_dependency(
+ &self,
+ message: &EnrichedExecutingMessage,
+ ) -> MessageGraphResult<()> {
+ // ChainID Invariant: The chain id of the initiating message MUST be in the dependency set
+ // This is enforced implicitly by the graph constructor and the provider.
+
+ // Timestamp invariant: The timestamp at the time of inclusion of the initiating message
+ // MUST be less than or equal to the timestamp of the executing message as well as greater
+ // than or equal to the Interop Start Timestamp.
+ if message.inner.id.timestamp.saturating_to::() > self.horizon_timestamp {
+ // TODO(interop): Also need to check for the interop start timestamp. Requires
+ // `RollupConfig`s for each chain.
+ return Err(MessageGraphError::MessageInFuture(
+ self.horizon_timestamp,
+ message.inner.id.timestamp.saturating_to(),
+ ));
+ }
+
+ // Fetch the header & receipts for the message's claimed origin block on the remote chain.
+ let remote_header = self
+ .provider
+ .header_by_number(
+ message.inner.id.chainId.saturating_to(),
+ message.inner.id.blockNumber.saturating_to(),
+ )
+ .await?;
+ let remote_receipts = self
+ .provider
+ .receipts_by_number(
+ message.inner.id.chainId.saturating_to(),
+ message.inner.id.blockNumber.saturating_to(),
+ )
+ .await?;
+
+ // Find the log that matches the message's claimed log index. Note that the
+ // log index is global to the block, so we chain the full block's logs together
+ // to find it.
+ let remote_log = remote_receipts
+ .iter()
+ .flat_map(|receipt| receipt.logs())
+ .nth(message.inner.id.logIndex.saturating_to())
+ .ok_or(MessageGraphError::RemoteMessageNotFound(
+ message.inner.id.chainId.to(),
+ message.inner.msgHash,
+ ))?;
+
+ // Validate the message's origin is correct.
+ if remote_log.address != message.inner.id.origin {
+ return Err(MessageGraphError::InvalidMessageOrigin(
+ message.inner.id.origin,
+ remote_log.address,
+ ));
+ }
+
+ // Validate that the message hash is correct.
+ let remote_message = RawMessagePayload::from(remote_log);
+ let remote_message_hash = keccak256(remote_message.as_ref());
+ if remote_message_hash != message.inner.msgHash {
+ return Err(MessageGraphError::InvalidMessageHash(
+ message.inner.msgHash,
+ remote_message_hash,
+ ));
+ }
+
+ // Validate that the timestamp of the block header containing the log is correct.
+ if remote_header.timestamp != message.inner.id.timestamp.saturating_to::() {
+ return Err(MessageGraphError::InvalidMessageTimestamp(
+ message.inner.id.timestamp.saturating_to::(),
+ remote_header.timestamp,
+ ));
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::MessageGraph;
+ use crate::{test_util::SuperchainBuilder, MessageGraphError};
+ use alloy_primitives::{hex, keccak256, Address};
+
+ const MESSAGE: [u8; 4] = hex!("deadbeef");
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into());
+ superchain.chain(2).add_executing_message(keccak256(MESSAGE), 0, 1, 0);
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ graph.resolve().await.unwrap();
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_cyclical_graph() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into()).add_executing_message(
+ keccak256(MESSAGE),
+ 1,
+ 2,
+ 0,
+ );
+ superchain
+ .chain(2)
+ .add_executing_message(keccak256(MESSAGE), 0, 1, 0)
+ .add_initiating_message(MESSAGE.into());
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ graph.resolve().await.unwrap();
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph_remote_message_not_found() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1);
+ superchain.chain(2).add_executing_message(keccak256(MESSAGE), 0, 1, 0);
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2]));
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph_invalid_chain_id() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into());
+ superchain.chain(2).add_executing_message(keccak256(MESSAGE), 0, 2, 0);
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2]));
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph_invalid_log_index() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into());
+ superchain.chain(2).add_executing_message(keccak256(MESSAGE), 1, 1, 0);
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2]));
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph_invalid_message_hash() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into());
+ superchain.chain(2).add_executing_message(keccak256(hex!("0badc0de")), 0, 1, 0);
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2]));
+ }
+
+ #[tokio::test]
+ async fn test_derive_and_reduce_simple_graph_invalid_origin_address() {
+ let mut superchain = SuperchainBuilder::new(0);
+
+ superchain.chain(1).add_initiating_message(MESSAGE.into());
+ superchain.chain(2).add_executing_message_with_origin(
+ keccak256(MESSAGE),
+ Address::left_padding_from(&[0x01]),
+ 0,
+ 1,
+ 0,
+ );
+
+ let (headers, provider) = superchain.build();
+
+ let graph = MessageGraph::derive(headers.as_slice(), provider).await.unwrap();
+ assert_eq!(graph.resolve().await.unwrap_err(), MessageGraphError::InvalidMessages(vec![2]));
+ }
+}
diff --git a/crates/interop/src/lib.rs b/crates/interop/src/lib.rs
new file mode 100644
index 00000000..be4da320
--- /dev/null
+++ b/crates/interop/src/lib.rs
@@ -0,0 +1,37 @@
+#![doc = include_str!("../README.md")]
+#![doc(
+ html_logo_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/square.png",
+ html_favicon_url = "https://raw.githubusercontent.com/op-rs/kona/main/assets/favicon.ico"
+)]
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+#![cfg_attr(not(test), warn(unused_crate_dependencies))]
+#![cfg_attr(not(any(test, feature = "arbitrary")), no_std)]
+
+extern crate alloc;
+
+mod graph;
+pub use graph::MessageGraph;
+
+mod message;
+pub use message::{
+ extract_executing_messages, EnrichedExecutingMessage, ExecutingMessage, MessageIdentifier,
+ RawMessagePayload,
+};
+
+mod constants;
+pub use constants::{CROSS_L2_INBOX_ADDRESS, MESSAGE_EXPIRY_WINDOW, SUPER_ROOT_VERSION};
+
+mod traits;
+pub use traits::InteropProvider;
+
+mod errors;
+pub use errors::{
+ InteropProviderError, InteropProviderResult, MessageGraphError, MessageGraphResult,
+ SuperRootError, SuperRootResult,
+};
+
+mod super_root;
+pub use super_root::{OutputRootWithChain, SuperRoot};
+
+#[cfg(test)]
+mod test_util;
diff --git a/crates/interop/src/message.rs b/crates/interop/src/message.rs
new file mode 100644
index 00000000..d5c756d6
--- /dev/null
+++ b/crates/interop/src/message.rs
@@ -0,0 +1,114 @@
+//! Interop message primitives.
+//!
+//!
+//!
+
+use crate::constants::CROSS_L2_INBOX_ADDRESS;
+use alloc::{vec, vec::Vec};
+use alloy_primitives::{keccak256, Bytes, Log};
+use alloy_sol_types::{sol, SolEvent};
+use op_alloy_consensus::OpReceiptEnvelope;
+
+sol! {
+ /// @notice The struct for a pointer to a message payload in a remote (or local) chain.
+ #[derive(Default, Debug, PartialEq, Eq)]
+ struct MessageIdentifier {
+ address origin;
+ uint256 blockNumber;
+ uint256 logIndex;
+ uint256 timestamp;
+ uint256 chainId;
+ }
+
+ /// @notice Emitted when a cross chain message is being executed.
+ /// @param msgHash Hash of message payload being executed.
+ /// @param id Encoded Identifier of the message.
+ #[derive(Default, Debug, PartialEq, Eq)]
+ event ExecutingMessage(bytes32 indexed msgHash, MessageIdentifier id);
+
+ /// @notice Executes a cross chain message on the destination chain.
+ /// @param _id Identifier of the message.
+ /// @param _target Target address to call.
+ /// @param _message Message payload to call target with.
+ function executeMessage(
+ MessageIdentifier calldata _id,
+ address _target,
+ bytes calldata _message
+ ) external;
+}
+
+/// A [RawMessagePayload] is the raw payload of an initiating message.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RawMessagePayload(Bytes);
+
+impl From<&Log> for RawMessagePayload {
+ fn from(log: &Log) -> Self {
+ let mut data = vec![0u8; log.topics().len() * 32 + log.data.data.len()];
+ for (i, topic) in log.topics().iter().enumerate() {
+ data[i * 32..(i + 1) * 32].copy_from_slice(topic.as_ref());
+ }
+ data[(log.topics().len() * 32)..].copy_from_slice(log.data.data.as_ref());
+ data.into()
+ }
+}
+
+impl From> for RawMessagePayload {
+ fn from(data: Vec) -> Self {
+ Self(Bytes::from(data))
+ }
+}
+
+impl From for RawMessagePayload {
+ fn from(bytes: Bytes) -> Self {
+ Self(bytes)
+ }
+}
+
+impl From for Bytes {
+ fn from(payload: RawMessagePayload) -> Self {
+ payload.0
+ }
+}
+
+impl AsRef<[u8]> for RawMessagePayload {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+impl From for ExecutingMessage {
+ fn from(call: executeMessageCall) -> Self {
+ Self { id: call._id, msgHash: keccak256(call._message.as_ref()) }
+ }
+}
+
+/// A wrapper type for [ExecutingMessage] containing the chain ID of the chain that the message was
+/// executed on.
+#[derive(Debug)]
+pub struct EnrichedExecutingMessage {
+ /// The inner [ExecutingMessage].
+ pub inner: ExecutingMessage,
+ /// The chain ID of the chain that the message was executed on.
+ pub executing_chain_id: u64,
+}
+
+impl EnrichedExecutingMessage {
+ /// Create a new [EnrichedExecutingMessage] from an [ExecutingMessage] and a chain ID.
+ pub const fn new(inner: ExecutingMessage, executing_chain_id: u64) -> Self {
+ Self { inner, executing_chain_id }
+ }
+}
+
+/// Extracts all [ExecutingMessage] logs from a list of [OpReceiptEnvelope]s.
+pub fn extract_executing_messages(receipts: &[OpReceiptEnvelope]) -> Vec {
+ receipts.iter().fold(Vec::new(), |mut acc, envelope| {
+ let executing_messages = envelope.logs().iter().filter_map(|log| {
+ (log.address == CROSS_L2_INBOX_ADDRESS && log.topics().len() == 2)
+ .then(|| ExecutingMessage::decode_log_data(&log.data, true).ok())
+ .flatten()
+ });
+
+ acc.extend(executing_messages);
+ acc
+ })
+}
diff --git a/crates/interop/src/super_root.rs b/crates/interop/src/super_root.rs
new file mode 100644
index 00000000..a8d3c802
--- /dev/null
+++ b/crates/interop/src/super_root.rs
@@ -0,0 +1,205 @@
+//! The [SuperRoot] type.
+//!
+//! Represents a snapshot of the state of the superchain at a given integer timestamp.
+
+use crate::{
+ errors::{SuperRootError, SuperRootResult},
+ SUPER_ROOT_VERSION,
+};
+use alloc::vec::Vec;
+use alloy_primitives::{keccak256, B256, U256};
+use alloy_rlp::{Buf, BufMut};
+
+/// The [SuperRoot] is the snapshot of the superchain at a given timestamp.
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))]
+pub struct SuperRoot {
+ /// The timestamp of the superchain snapshot, in seconds.
+ pub timestamp: u64,
+ /// The chain IDs and output root commitments of all chains within the dependency set.
+ pub output_roots: Vec,
+}
+
+impl SuperRoot {
+ /// Create a new [SuperRoot] with the given timestamp and output roots.
+ pub fn new(timestamp: u64, mut output_roots: Vec) -> Self {
+ // Guarantee that the output roots are sorted by chain ID.
+ output_roots.sort_by_key(|r| r.chain_id);
+ Self { timestamp, output_roots }
+ }
+
+ /// Decodes a [SuperRoot] from the given buffer.
+ pub fn decode(buf: &mut &[u8]) -> SuperRootResult {
+ if buf.is_empty() {
+ return Err(SuperRootError::UnexpectedLength);
+ }
+
+ let version = buf[0];
+ if version != SUPER_ROOT_VERSION {
+ return Err(SuperRootError::InvalidVersionByte);
+ }
+ buf.advance(1);
+
+ if buf.len() < 8 {
+ return Err(SuperRootError::UnexpectedLength);
+ }
+ let timestamp = u64::from_be_bytes(buf[0..8].try_into().unwrap());
+ buf.advance(8);
+
+ let mut output_roots = Vec::new();
+ while !buf.is_empty() {
+ if buf.len() < 64 {
+ return Err(SuperRootError::UnexpectedLength);
+ }
+
+ let chain_id = U256::from_be_bytes::<32>(buf[0..32].try_into().unwrap());
+ buf.advance(32);
+ let output_root = B256::from_slice(&buf[0..32]);
+ buf.advance(32);
+ output_roots.push(OutputRootWithChain::new(chain_id.to(), output_root));
+ }
+
+ Ok(Self { timestamp, output_roots })
+ }
+
+ /// Encode the [SuperRoot] into the given buffer.
+ pub fn encode(&self, out: &mut dyn BufMut) {
+ out.put_u8(SUPER_ROOT_VERSION);
+
+ out.put_u64(self.timestamp);
+ for output_root in &self.output_roots {
+ out.put_slice(U256::from(output_root.chain_id).to_be_bytes::<32>().as_slice());
+ out.put_slice(output_root.output_root.as_slice());
+ }
+ }
+
+ /// Returns the encoded length of the [SuperRoot].
+ pub fn encoded_length(&self) -> usize {
+ 1 + 8 + 64 * self.output_roots.len()
+ }
+
+ /// Hashes the encoded [SuperRoot] using [keccak256].
+ pub fn hash(&self) -> B256 {
+ let mut rlp_buf = Vec::with_capacity(self.encoded_length());
+ self.encode(&mut rlp_buf);
+ keccak256(&rlp_buf)
+ }
+}
+
+/// A wrapper around an output root hash with the chain ID it belongs to.
+#[derive(Debug, Clone, Eq, PartialEq)]
+#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))]
+pub struct OutputRootWithChain {
+ /// The chain ID of the output root.
+ pub chain_id: u64,
+ /// The output root hash.
+ pub output_root: B256,
+}
+
+impl OutputRootWithChain {
+ /// Create a new [OutputRootWithChain] with the given chain ID and output root hash.
+ pub const fn new(chain_id: u64, output_root: B256) -> Self {
+ Self { chain_id, output_root }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::{errors::SuperRootError, SUPER_ROOT_VERSION};
+
+ use super::{OutputRootWithChain, SuperRoot};
+ use alloy_primitives::{b256, B256};
+ use arbitrary::Arbitrary;
+ use rand::Rng;
+
+ #[test]
+ fn test_super_root_sorts_outputs() {
+ let super_root = SuperRoot::new(
+ 10,
+ vec![
+ (OutputRootWithChain::new(3, B256::default())),
+ (OutputRootWithChain::new(2, B256::default())),
+ (OutputRootWithChain::new(1, B256::default())),
+ ],
+ );
+
+ assert!(super_root.output_roots.is_sorted_by_key(|r| r.chain_id));
+ }
+
+ #[test]
+ fn test_super_root_empty_buf() {
+ let buf: Vec = Vec::new();
+ assert_eq!(
+ SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
+ SuperRootError::UnexpectedLength
+ );
+ }
+
+ #[test]
+ fn test_super_root_invalid_version() {
+ let buf = vec![0xFF];
+ assert_eq!(
+ SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
+ SuperRootError::InvalidVersionByte
+ );
+ }
+
+ #[test]
+ fn test_super_root_invalid_length_at_timestamp() {
+ let buf = vec![SUPER_ROOT_VERSION, 0x00];
+ assert_eq!(
+ SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
+ SuperRootError::UnexpectedLength
+ );
+ }
+
+ #[test]
+ fn test_super_root_invalid_length_malformed_output_roots() {
+ let buf = [&[SUPER_ROOT_VERSION], 64u64.to_be_bytes().as_ref(), &[0xbe, 0xef]].concat();
+ assert_eq!(
+ SuperRoot::decode(&mut buf.as_slice()).unwrap_err(),
+ SuperRootError::UnexpectedLength
+ );
+ }
+
+ #[test]
+ fn test_static_hash_super_root() {
+ const EXPECTED: B256 =
+ b256!("0980033cbf4337f614a2401ab7efbfdc66ab647812f1c98d891d92ddfb376541");
+
+ let super_root = SuperRoot::new(
+ 10,
+ vec![
+ (OutputRootWithChain::new(1, B256::default())),
+ (OutputRootWithChain::new(2, B256::default())),
+ ],
+ );
+ assert_eq!(super_root.hash(), EXPECTED);
+ }
+
+ #[test]
+ fn test_static_super_root_roundtrip() {
+ let super_root = SuperRoot::new(
+ 10,
+ vec![
+ (OutputRootWithChain::new(1, B256::default())),
+ (OutputRootWithChain::new(2, B256::default())),
+ ],
+ );
+
+ let mut rlp_buf = Vec::with_capacity(super_root.encoded_length());
+ super_root.encode(&mut rlp_buf);
+ assert_eq!(super_root, SuperRoot::decode(&mut rlp_buf.as_slice()).unwrap());
+ }
+
+ #[test]
+ fn test_arbitrary_super_root_roundtrip() {
+ let mut bytes = [0u8; 1024];
+ rand::thread_rng().fill(bytes.as_mut_slice());
+ let super_root = SuperRoot::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
+
+ let mut rlp_buf = Vec::with_capacity(super_root.encoded_length());
+ super_root.encode(&mut rlp_buf);
+ assert_eq!(super_root, SuperRoot::decode(&mut rlp_buf.as_slice()).unwrap());
+ }
+}
diff --git a/crates/interop/src/test_util.rs b/crates/interop/src/test_util.rs
new file mode 100644
index 00000000..ab6a1627
--- /dev/null
+++ b/crates/interop/src/test_util.rs
@@ -0,0 +1,197 @@
+//! Test utilities for `kona-interop`.
+
+#![allow(missing_docs, unreachable_pub)]
+
+use crate::{
+ errors::InteropProviderResult, traits::InteropProvider, ExecutingMessage, MessageIdentifier,
+ CROSS_L2_INBOX_ADDRESS,
+};
+use alloy_consensus::{Header, Receipt, ReceiptWithBloom, Sealed};
+use alloy_primitives::{map::HashMap, Address, Bytes, Log, LogData, B256, U256};
+use alloy_sol_types::{SolEvent, SolValue};
+use async_trait::async_trait;
+use op_alloy_consensus::OpReceiptEnvelope;
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct MockInteropProvider {
+ pub headers: HashMap>>,
+ pub receipts: HashMap>>,
+}
+
+impl MockInteropProvider {
+ pub const fn new(
+ headers: HashMap>>,
+ receipts: HashMap>>,
+ ) -> Self {
+ Self { headers, receipts }
+ }
+}
+
+#[async_trait]
+impl InteropProvider for MockInteropProvider {
+ /// Fetch a [Header] by its number.
+ async fn header_by_number(&self, chain_id: u64, number: u64) -> InteropProviderResult {
+ Ok(self
+ .headers
+ .get(&chain_id)
+ .and_then(|headers| headers.get(&number))
+ .unwrap()
+ .inner()
+ .clone())
+ }
+
+ /// Fetch a [Header] by its hash.
+ async fn header_by_hash(&self, chain_id: u64, hash: B256) -> InteropProviderResult {
+ Ok(self
+ .headers
+ .get(&chain_id)
+ .and_then(|headers| headers.values().find(|header| header.hash() == hash))
+ .unwrap()
+ .inner()
+ .clone())
+ }
+
+ /// Fetch all receipts for a given block by number.
+ async fn receipts_by_number(
+ &self,
+ chain_id: u64,
+ number: u64,
+ ) -> InteropProviderResult> {
+ Ok(self.receipts.get(&chain_id).and_then(|receipts| receipts.get(&number)).unwrap().clone())
+ }
+
+ /// Fetch all receipts for a given block by hash.
+ async fn receipts_by_hash(
+ &self,
+ chain_id: u64,
+ block_hash: B256,
+ ) -> InteropProviderResult> {
+ Ok(self
+ .receipts
+ .get(&chain_id)
+ .and_then(|receipts| {
+ let headers = self.headers.get(&chain_id).unwrap();
+ let number =
+ headers.values().find(|header| header.hash() == block_hash).unwrap().number;
+ receipts.get(&number)
+ })
+ .unwrap()
+ .clone())
+ }
+}
+
+pub struct SuperchainBuilder {
+ chains: HashMap,
+ timestamp: u64,
+}
+
+pub struct ChainBuilder {
+ header: Header,
+ receipts: Vec,
+}
+
+impl SuperchainBuilder {
+ pub fn new(timestamp: u64) -> Self {
+ Self { chains: HashMap::new(), timestamp }
+ }
+
+ pub fn chain(&mut self, chain_id: u64) -> &mut ChainBuilder {
+ self.chains.entry(chain_id).or_insert_with(|| ChainBuilder::new(self.timestamp))
+ }
+
+ /// Builds the scenario into the format needed for testing
+ pub fn build(self) -> (Vec<(u64, Sealed)>, MockInteropProvider) {
+ let mut headers_map = HashMap::new();
+ let mut receipts_map = HashMap::new();
+ let mut sealed_headers = Vec::new();
+
+ for (chain_id, chain) in self.chains {
+ let header = chain.header;
+ let header_hash = header.hash_slow();
+ let sealed_header = header.seal(header_hash);
+
+ let mut chain_headers = HashMap::new();
+ chain_headers.insert(0, sealed_header.clone());
+ headers_map.insert(chain_id, chain_headers);
+
+ let mut chain_receipts = HashMap::new();
+ chain_receipts.insert(0, chain.receipts);
+ receipts_map.insert(chain_id, chain_receipts);
+
+ sealed_headers.push((chain_id, sealed_header));
+ }
+
+ (sealed_headers, MockInteropProvider::new(headers_map, receipts_map))
+ }
+}
+
+impl ChainBuilder {
+ pub fn new(timestamp: u64) -> Self {
+ Self { header: Header { timestamp, ..Default::default() }, receipts: Vec::new() }
+ }
+
+ pub fn add_initiating_message(&mut self, message_data: Bytes) -> &mut Self {
+ let receipt = OpReceiptEnvelope::Eip1559(ReceiptWithBloom {
+ receipt: Receipt {
+ logs: vec![Log {
+ address: Address::ZERO,
+ data: LogData::new(vec![], message_data).unwrap(),
+ }],
+ ..Default::default()
+ },
+ ..Default::default()
+ });
+ self.receipts.push(receipt);
+ self
+ }
+
+ pub fn add_executing_message(
+ &mut self,
+ message_hash: B256,
+ origin_log_index: u64,
+ origin_chain_id: u64,
+ origin_timestamp: u64,
+ ) -> &mut Self {
+ self.add_executing_message_with_origin(
+ message_hash,
+ Address::ZERO,
+ origin_log_index,
+ origin_chain_id,
+ origin_timestamp,
+ )
+ }
+
+ pub fn add_executing_message_with_origin(
+ &mut self,
+ message_hash: B256,
+ origin_address: Address,
+ origin_log_index: u64,
+ origin_chain_id: u64,
+ origin_timestamp: u64,
+ ) -> &mut Self {
+ let receipt = OpReceiptEnvelope::Eip1559(ReceiptWithBloom {
+ receipt: Receipt {
+ logs: vec![Log {
+ address: CROSS_L2_INBOX_ADDRESS,
+ data: LogData::new(
+ vec![ExecutingMessage::SIGNATURE_HASH, message_hash],
+ MessageIdentifier {
+ origin: origin_address,
+ blockNumber: U256::ZERO,
+ logIndex: U256::from(origin_log_index),
+ timestamp: U256::from(origin_timestamp),
+ chainId: U256::from(origin_chain_id),
+ }
+ .abi_encode()
+ .into(),
+ )
+ .unwrap(),
+ }],
+ ..Default::default()
+ },
+ ..Default::default()
+ });
+ self.receipts.push(receipt);
+ self
+ }
+}
diff --git a/crates/interop/src/traits.rs b/crates/interop/src/traits.rs
new file mode 100644
index 00000000..dde157ae
--- /dev/null
+++ b/crates/interop/src/traits.rs
@@ -0,0 +1,33 @@
+//! Traits for the `kona-interop` crate.
+
+use crate::errors::InteropProviderResult;
+use alloc::{boxed::Box, vec::Vec};
+use alloy_consensus::Header;
+use alloy_primitives::B256;
+use async_trait::async_trait;
+use op_alloy_consensus::OpReceiptEnvelope;
+
+/// Describes the interface of the interop data provider. This provider is multiplexed over several
+/// chains, with each method consuming a chain ID to determine the target chain.
+#[async_trait]
+pub trait InteropProvider {
+ /// Fetch a [Header] by its number.
+ async fn header_by_number(&self, chain_id: u64, number: u64) -> InteropProviderResult;
+
+ /// Fetch a [Header] by its hash.
+ async fn header_by_hash(&self, chain_id: u64, hash: B256) -> InteropProviderResult;
+
+ /// Fetch all receipts for a given block by number.
+ async fn receipts_by_number(
+ &self,
+ chain_id: u64,
+ number: u64,
+ ) -> InteropProviderResult>;
+
+ /// Fetch all receipts for a given block by hash.
+ async fn receipts_by_hash(
+ &self,
+ chain_id: u64,
+ block_hash: B256,
+ ) -> InteropProviderResult>;
+}