Skip to content

Commit

Permalink
feat(rpc): supervisor traits + client (#128)
Browse files Browse the repository at this point in the history
### Description

Introduces the supervisor api to `maili-rpc`.
  • Loading branch information
refcell authored Jan 23, 2025
1 parent 650655f commit 7b67963
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ alloy-network = { version = "0.9.2", default-features = false }
alloy-provider = { version = "0.9.2", default-features = false }
alloy-transport = { version = "0.9.2", default-features = false }
alloy-consensus = { version = "0.9.2", default-features = false }
alloy-rpc-client = { version = "0.9.2", default-features = false }
alloy-rpc-types-eth = { version = "0.9.2", default-features = false }
alloy-rpc-types-engine = { version = "0.9.2", default-features = false }
alloy-network-primitives = { version = "0.9.2", default-features = false }
Expand Down
17 changes: 17 additions & 0 deletions crates/interop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,40 @@ workspace = true

[dependencies]
# Alloy
alloy-rlp.workspace = true
alloy-eips.workspace = true
alloy-sol-types.workspace = true
alloy-consensus.workspace = true
alloy-primitives = { workspace = true, features = ["map"] }

# Misc
thiserror.workspace = true
derive_more = { workspace = true, default-features = false, features = ["from", "as_ref"] }

# `arbitrary` feature
arbitrary = { workspace = true, features = ["derive"], optional = true }

# `serde` feature
serde = { workspace = true, optional = true }

[dev-dependencies]
serde_json.workspace = true
rand = { workspace = true, features = ["small_rng"] }
arbitrary = { workspace = true, features = ["derive"] }

[features]
default = ["serde", "std"]
std = []
serde = [
"dep:serde",
"alloy-eips/serde",
"alloy-primitives/serde",
]
arbitrary = [
"std",
"dep:arbitrary",
"alloy-consensus/arbitrary",
"alloy-eips/arbitrary",
"alloy-primitives/rand",
"alloy-primitives/arbitrary",
]
14 changes: 14 additions & 0 deletions crates/interop/src/derived.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Contains derived types for interop.
use alloy_eips::eip1898::BlockNumHash;

/// A derived ID pair is a pair of block IDs, where Derived (L2) is derived from DerivedFrom (L1).
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct DerivedIdPair {
/// The block ID of the L1 block.
pub derived_from: BlockNumHash,
/// The block ID of the L2 block.
pub derived: BlockNumHash,
}
19 changes: 19 additions & 0 deletions crates/interop/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! Error types for interop.
use thiserror::Error;

/// An error type for the [SuperRoot] struct's serialization and deserialization.
///
/// [SuperRoot]: crate::SuperRoot
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum SuperRootError {
/// Invalid super root version byte
#[error("Invalid super root version byte")]
InvalidVersionByte,
/// Unexpected encoded super root length
#[error("Unexpected encoded super root length")]
UnexpectedLength,
}

/// A [Result] alias for the [SuperRootError] type.
pub type SuperRootResult<T> = core::result::Result<T, SuperRootError>;
11 changes: 10 additions & 1 deletion crates/interop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
)]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(not(any(test, feature = "std")), no_std)]

extern crate alloc;

mod root;
pub use root::{ChainRootInfo, OutputRootWithChain, SuperRoot, SuperRootResponse};

mod errors;
pub use errors::{SuperRootError, SuperRootResult};

mod safety;
pub use safety::SafetyLevel;

Expand All @@ -18,5 +24,8 @@ pub use message::{
MessagePayload,
};

mod derived;
pub use derived::DerivedIdPair;

mod constants;
pub use constants::{CROSS_L2_INBOX_ADDRESS, MESSAGE_EXPIRY_WINDOW, SUPER_ROOT_VERSION};
236 changes: 236 additions & 0 deletions crates/interop/src/root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
//! The [SuperRoot] type.
//!
//! Represents a snapshot of the state of the superchain at a given integer timestamp.
use crate::{SuperRootError, SuperRootResult, SUPER_ROOT_VERSION};
use alloc::vec::Vec;
use alloy_primitives::{keccak256, Bytes, 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<OutputRootWithChain>,
}

impl SuperRoot {
/// Create a new [SuperRoot] with the given timestamp and output roots.
pub fn new(timestamp: u64, mut output_roots: Vec<OutputRootWithChain>) -> 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<Self> {
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)
}
}

/// Chain Root Info
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ChainRootInfo {
/// The chain ID.
#[cfg_attr(feature = "serde", serde(rename = "chainID"))]
pub chain_id: u64,
/// The canonical output root of the latest canonical block at a particular timestamp.
pub canonical: B256,
/// The pending output root.
///
/// This is the output root preimage for the latest block at a particular timestamp prior to
/// validation of executing messages. If the original block was valid, this will be the
/// preimage of the output root from the `canonical` array. If it was invalid, it will be
/// the output root preimage from the optimistic block deposited transaction added to the
/// deposit-only block.
pub pending: Bytes,
}

/// The super root response type.
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct SuperRootResponse {
/// The timestamp of the super root.
pub timestamp: u64,
/// The super root hash.
pub super_root: B256,
/// The chain root info for each chain in the dependency set.
/// It represents the state of the chain at or before the timestamp.
pub chains: Vec<ChainRootInfo>,
}

/// 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::{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<u8> = 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());
}
}
19 changes: 14 additions & 5 deletions crates/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ workspace = true

[dependencies]
# Workspace
maili-protocol = { workspace = true, optional = true, features = ["serde"] }
maili-protocol = { workspace = true, features = ["serde"] }

# Alloy
alloy-eips = { workspace = true, features = ["serde"] }
alloy-primitives = { workspace = true, features = ["map", "rlp", "serde"] }

# rpc
# `interop` feature
async-trait = { workspace = true, optional = true }
maili-interop = { workspace = true, features = ["serde"], optional = true }
alloy-rpc-client = { workspace = true, features = ["reqwest"], optional = true }

# RPC
jsonrpsee = { workspace = true, optional = true }
getrandom = { workspace = true, optional = true } # req for wasm32-unknown-unknown
serde.workspace = true

# misc
# Misc
derive_more = { workspace = true, default-features = false, features = ["display", "from"] }

[dev-dependencies]
Expand All @@ -36,12 +41,16 @@ serde_json.workspace = true
[features]
default = ["std", "jsonrpsee"]
std = [
"maili-protocol?/std",
"maili-protocol/std",
"alloy-eips/std",
"alloy-primitives/std",
]
interop = [
"dep:maili-interop",
"dep:alloy-rpc-client",
"dep:async-trait",
]
jsonrpsee = [
"dep:maili-protocol",
"dep:jsonrpsee",
"dep:getrandom",
]
Expand Down
Loading

0 comments on commit 7b67963

Please sign in to comment.