diff --git a/Cargo.toml b/Cargo.toml
index 3650482a..80839be1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ rustdoc-args = ["--cfg", "docsrs"]
maili-protocol = { version = "0.1.0", path = "crates/protocol", default-features = false }
maili-provider = { version = "0.1.0", path = "crates/provider", default-features = false }
maili-registry = { version = "0.9.1", path = "crates/registry", default-features = false }
+maili-rpc-types-engine = { version = "0.1.0", path = "crates/rpc-types-engine", default-features = false }
# OP-Alloy
op-alloy-genesis = { version = "0.9.0", default-features = false }
diff --git a/README.md b/README.md
index b66b4e75..0e4a0efa 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ The following crates are provided by `maili`.
- [`maili-protocol`][maili-protocol]
- [`maili-provider`][maili-provider]
- [`maili-registry`][maili-registry]
+- [`maili-rpc-types-engine`][maili-rpc-types-engine]
## Development Status
@@ -53,6 +54,7 @@ Notice, provider crates do not support `no_std` compatibility.
- [`maili-protocol`][maili-protocol]
- [`maili-provider`][maili-provider]
- [`maili-registry`][maili-registry] (note: requires `serde`)
+- [`maili-rpc-types-engine`][maili-rpc-types-engine]
If you would like to add no_std support to a crate,
please make sure to update [scripts/check_no_std.sh][check-no-std].
@@ -86,3 +88,4 @@ shall be dual licensed as above, without any additional terms or conditions.
[maili-protocol]: https://crates.io/crates/maili-protocol
[maili-provider]: https://crates.io/crates/maili-provider
[maili-registry]: https://crates.io/crates/maili-registry
+[maili-rpc-types-engine]: https://crates.io/crates/maili-rpc-types-engine
diff --git a/book/src/links.md b/book/src/links.md
index 90a7c019..3beefd71 100644
--- a/book/src/links.md
+++ b/book/src/links.md
@@ -7,6 +7,7 @@
[maili-protocol]: https://crates.io/crates/maili-protocol
[maili-provider]: https://crates.io/crates/maili-provider
+[maili-rpc-types-engine]: https://crates.io/crates/maili-rpc-types-engine
diff --git a/book/src/starting.md b/book/src/starting.md
index 4eeba160..ea0e41db 100644
--- a/book/src/starting.md
+++ b/book/src/starting.md
@@ -57,12 +57,14 @@ so `maili-protocol` types can be used from `maili` through `maili::protocol::Ins
- [`maili-protocol`][maili-protocol] (supports `no_std`)
- [`maili-provider`][maili-provider]
+- [`maili-rpc-types-engine`][maili-rpc-types-engine] (supports `no_std`)
## `no_std`
As noted above, the following crates are `no_std` compatible.
- [`maili-protocol`][maili-protocol]
+- [`maili-rpc-types-engine`][maili-rpc-types-engine]
To add `no_std` support to a crate, ensure the [check_no_std][check-no-std]
script is updated to include this crate once `no_std` compatible.
diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml
index bc87dd83..e35754f0 100644
--- a/crates/provider/Cargo.toml
+++ b/crates/provider/Cargo.toml
@@ -15,6 +15,9 @@ exclude.workspace = true
workspace = true
[dependencies]
+# Workspace
+maili-rpc-types-engine = { workspace = true, features = ["serde"] }
+
# OP-Alloy
op-alloy-rpc-types-engine = { workspace = true, features = ["serde"] }
diff --git a/crates/provider/src/ext/engine.rs b/crates/provider/src/ext/engine.rs
index 43de35ba..c9f3ab84 100644
--- a/crates/provider/src/ext/engine.rs
+++ b/crates/provider/src/ext/engine.rs
@@ -6,9 +6,9 @@ use alloy_rpc_types_engine::{
ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId, PayloadStatus,
};
use alloy_transport::{Transport, TransportResult};
+use maili_rpc_types_engine::{ProtocolVersion, SuperchainSignal};
use op_alloy_rpc_types_engine::{
OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpPayloadAttributes,
- ProtocolVersion, SuperchainSignal,
};
/// Extension trait that gives access to Optimism engine API RPC methods.
diff --git a/crates/rpc-types-engine/Cargo.toml b/crates/rpc-types-engine/Cargo.toml
new file mode 100644
index 00000000..47cb7346
--- /dev/null
+++ b/crates/rpc-types-engine/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "maili-rpc-types-engine"
+description = "Optimism RPC types for the `engine` namespace"
+
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+homepage.workspace = true
+authors.workspace = true
+repository.workspace = true
+exclude.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+# Alloy
+alloy-primitives.workspace = true
+
+# serde
+serde = { workspace = true, optional = true }
+
+# misc
+derive_more = { workspace = true, default-features = false, features = ["display", "from"] }
+
+[features]
+default = ["std", "serde"]
+std = ["alloy-primitives/std"]
+serde = [
+ "dep:serde",
+ "alloy-primitives/serde"
+]
\ No newline at end of file
diff --git a/crates/rpc-types-engine/README.md b/crates/rpc-types-engine/README.md
new file mode 100644
index 00000000..2a1cb528
--- /dev/null
+++ b/crates/rpc-types-engine/README.md
@@ -0,0 +1,10 @@
+## `maili-rpc-types-engine`
+
+
+
+
+
+
+
+
+Optimism RPC types for the `engine` namespace.
diff --git a/crates/rpc-types-engine/src/lib.rs b/crates/rpc-types-engine/src/lib.rs
new file mode 100644
index 00000000..e7ccac92
--- /dev/null
+++ b/crates/rpc-types-engine/src/lib.rs
@@ -0,0 +1,15 @@
+#![doc = include_str!("../README.md")]
+#![doc(
+ html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
+ html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/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 = "std")), no_std)]
+
+extern crate alloc;
+
+mod superchain;
+pub use superchain::{
+ ProtocolVersion, ProtocolVersionError, ProtocolVersionFormatV0, SuperchainSignal,
+};
diff --git a/crates/rpc-types-engine/src/superchain.rs b/crates/rpc-types-engine/src/superchain.rs
new file mode 100644
index 00000000..044ec13e
--- /dev/null
+++ b/crates/rpc-types-engine/src/superchain.rs
@@ -0,0 +1,410 @@
+use alloc::{
+ format,
+ string::{String, ToString},
+};
+use core::array::TryFromSliceError;
+
+use alloy_primitives::{B256, B64};
+use derive_more::derive::{Display, From};
+
+/// Superchain Signal information.
+///
+/// The execution engine SHOULD warn the user when the recommended version is newer than the current
+/// version supported by the execution engine.
+///
+/// The execution engine SHOULD take safety precautions if it does not meet the required protocol
+/// version. This may include halting the engine, with consent of the execution engine operator.
+///
+/// See also:
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
+pub struct SuperchainSignal {
+ /// The recommended Supercain Protocol Version.
+ pub recommended: ProtocolVersion,
+ /// The minimum Supercain Protocol Version required.
+ pub required: ProtocolVersion,
+}
+
+/// Formatted Superchain Protocol Version.
+///
+/// The Protocol Version documents the progression of the total set of canonical OP-Stack
+/// specifications. Components of the OP-Stack implement the subset of their respective protocol
+/// component domain, up to a given Protocol Version of the OP-Stack.
+///
+/// The Protocol Version **is NOT a hardfork identifier**, but rather indicates software-support for
+/// a well-defined set of features introduced in past and future hardforks, not the activation of
+/// said hardforks.
+///
+/// The Protocol Version is Semver-compatible. It is encoded as a single 32 bytes long
+/// protocol version. The version must be encoded as 32 bytes of DATA in JSON RPC usage.
+///
+/// See also:
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ProtocolVersion {
+ /// Version-type 0.
+ V0(ProtocolVersionFormatV0),
+}
+
+impl core::fmt::Display for ProtocolVersion {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Self::V0(value) => write!(f, "{}", value),
+ }
+ }
+}
+
+/// An error that can occur when encoding or decoding a ProtocolVersion.
+#[derive(Copy, Clone, Debug, Display, From)]
+pub enum ProtocolVersionError {
+ /// An unsupported version was encountered.
+ #[display("Unsupported version: {_0}")]
+ UnsupportedVersion(u8),
+ /// An invalid length was encountered.
+ #[display("Invalid length: got {}, expected {}", got, expected)]
+ InvalidLength {
+ /// The length that was encountered.
+ got: usize,
+ /// The expected length.
+ expected: usize,
+ },
+ /// Failed to convert slice to array.
+ #[display("Failed to convert slice to array")]
+ #[from(TryFromSliceError)]
+ TryFromSlice,
+}
+
+impl ProtocolVersion {
+ /// Version-type 0 byte encoding:
+ ///
+ /// ```text
+ /// ::=
+ /// ::=
+ /// ::= <31 bytes>
+ /// ```
+ pub fn encode(&self) -> B256 {
+ let mut bytes = [0u8; 32];
+
+ match self {
+ Self::V0(value) => {
+ bytes[0] = 0x00; // this is not necessary, but addded for clarity
+ bytes[1..].copy_from_slice(&value.encode());
+ B256::from_slice(&bytes)
+ }
+ }
+ }
+
+ /// Version-type 0 byte decoding:
+ ///
+ /// ```text
+ /// ::=
+ /// ::=
+ /// ::= <31 bytes>
+ /// ```
+ pub fn decode(value: B256) -> Result {
+ let version_type = value[0];
+ let typed_payload = &value[1..];
+
+ match version_type {
+ 0 => Ok(Self::V0(ProtocolVersionFormatV0::decode(typed_payload)?)),
+ other => Err(ProtocolVersionError::UnsupportedVersion(other)),
+ }
+ }
+
+ /// Returns the inner value of the ProtocolVersion enum
+ pub const fn inner(&self) -> ProtocolVersionFormatV0 {
+ match self {
+ Self::V0(value) => *value,
+ }
+ }
+
+ /// Returns the inner value of the ProtocolVersion enum if it is V0, otherwise None
+ pub const fn as_v0(&self) -> Option {
+ match self {
+ Self::V0(value) => Some(*value),
+ }
+ }
+
+ /// Differentiates forks and custom-builds of standard protocol
+ pub const fn build(&self) -> B64 {
+ match self {
+ Self::V0(value) => value.build,
+ }
+ }
+
+ /// Incompatible API changes
+ pub const fn major(&self) -> u32 {
+ match self {
+ Self::V0(value) => value.major,
+ }
+ }
+
+ /// Identifies additional functionality in backwards compatible manner
+ pub const fn minor(&self) -> u32 {
+ match self {
+ Self::V0(value) => value.minor,
+ }
+ }
+
+ /// Identifies backward-compatible bug-fixes
+ pub const fn patch(&self) -> u32 {
+ match self {
+ Self::V0(value) => value.patch,
+ }
+ }
+
+ /// Identifies unstable versions that may not satisfy the above
+ pub const fn pre_release(&self) -> u32 {
+ match self {
+ Self::V0(value) => value.pre_release,
+ }
+ }
+
+ /// Returns a human-readable string representation of the ProtocolVersion
+ pub fn display(&self) -> String {
+ match self {
+ Self::V0(value) => format!("{}", value),
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for ProtocolVersion {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ self.encode().serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for ProtocolVersion {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = alloy_primitives::B256::deserialize(deserializer)?;
+ Self::decode(value).map_err(serde::de::Error::custom)
+ }
+}
+
+/// The Protocol Version V0 format.
+/// Encoded as 31 bytes with the following structure:
+///
+/// ```text
+///
+/// ::= <7 zeroed bytes>
+/// ::= <8 bytes>
+/// ::=
+/// ::=
+/// ::=
+/// ::=
+/// ```
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct ProtocolVersionFormatV0 {
+ /// Differentiates forks and custom-builds of standard protocol
+ pub build: B64,
+ /// Incompatible API changes
+ pub major: u32,
+ /// Identifies additional functionality in backwards compatible manner
+ pub minor: u32,
+ /// Identifies backward-compatible bug-fixes
+ pub patch: u32,
+ /// Identifies unstable versions that may not satisfy the above
+ pub pre_release: u32,
+}
+
+impl core::fmt::Display for ProtocolVersionFormatV0 {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ let build_tag = if self.build.0.iter().any(|&byte| byte != 0) {
+ if is_human_readable_build_tag(self.build) {
+ let full = format!("+{}", String::from_utf8_lossy(&self.build.0));
+ full.trim_end_matches('\0').to_string()
+ } else {
+ format!("+{}", self.build)
+ }
+ } else {
+ String::new()
+ };
+
+ let pre_release_tag =
+ if self.pre_release != 0 { format!("-{}", self.pre_release) } else { String::new() };
+
+ write!(f, "v{}.{}.{}{}{}", self.major, self.minor, self.patch, pre_release_tag, build_tag)
+ }
+}
+
+impl ProtocolVersionFormatV0 {
+ /// Version-type 0 byte encoding:
+ ///
+ /// ```text
+ ///
+ /// ::= <7 zeroed bytes>
+ /// ::= <8 bytes>
+ /// ::=
+ /// ::=
+ /// ::=
+ /// ::=
+ /// ```
+ pub fn encode(&self) -> [u8; 31] {
+ let mut bytes = [0u8; 31];
+ bytes[0..7].copy_from_slice(&[0u8; 7]);
+ bytes[7..15].copy_from_slice(&self.build.0);
+ bytes[15..19].copy_from_slice(&self.major.to_be_bytes());
+ bytes[19..23].copy_from_slice(&self.minor.to_be_bytes());
+ bytes[23..27].copy_from_slice(&self.patch.to_be_bytes());
+ bytes[27..31].copy_from_slice(&self.pre_release.to_be_bytes());
+ bytes
+ }
+
+ /// Version-type 0 byte encoding:
+ ///
+ /// ```text
+ ///
+ /// ::= <7 zeroed bytes>
+ /// ::= <8 bytes>
+ /// ::=
+ /// ::=
+ /// ::=
+ /// ::=
+ /// ```
+ fn decode(value: &[u8]) -> Result {
+ if value.len() != 31 {
+ return Err(ProtocolVersionError::InvalidLength { got: value.len(), expected: 31 });
+ }
+
+ Ok(Self {
+ build: B64::from_slice(&value[7..15]),
+ major: u32::from_be_bytes(value[15..19].try_into()?),
+ minor: u32::from_be_bytes(value[19..23].try_into()?),
+ patch: u32::from_be_bytes(value[23..27].try_into()?),
+ pre_release: u32::from_be_bytes(value[27..31].try_into()?),
+ })
+ }
+}
+
+/// Returns true if the build tag is human-readable, false otherwise.
+fn is_human_readable_build_tag(build: B64) -> bool {
+ for (i, &c) in build.iter().enumerate() {
+ if c == 0 {
+ // Trailing zeros are allowed
+ if build[i..].iter().any(|&d| d != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ // following semver.org advertised regex, alphanumeric with '-' and '.', except leading '.'.
+ if !(c.is_ascii_alphanumeric() || c == b'-' || (c == b'.' && i > 0)) {
+ return false;
+ }
+ }
+ true
+}
+
+#[cfg(test)]
+mod tests {
+ use alloy_primitives::b256;
+
+ use super::*;
+
+ #[test]
+ fn test_protocol_version_encode_decode() {
+ let test_cases = vec![
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
+ major: 42,
+ minor: 0,
+ patch: 2,
+ pre_release: 0,
+ }),
+ "v42.0.2+0x6162010000000000",
+ b256!("000000000000000061620100000000000000002a000000000000000200000000"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
+ major: 42,
+ minor: 0,
+ patch: 2,
+ pre_release: 1,
+ }),
+ "v42.0.2-1+0x6162010000000000",
+ b256!("000000000000000061620100000000000000002a000000000000000200000001"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]),
+ major: 42,
+ minor: 0,
+ patch: 2,
+ pre_release: 0,
+ }),
+ "v42.0.2+0x0102030405060708",
+ b256!("000000000000000001020304050607080000002a000000000000000200000000"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
+ major: 0,
+ minor: 100,
+ patch: 2,
+ pre_release: 0,
+ }),
+ "v0.100.2",
+ b256!("0000000000000000000000000000000000000000000000640000000200000000"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[b'O', b'P', b'-', b'm', b'o', b'd', 0x00, 0x00]),
+ major: 42,
+ minor: 0,
+ patch: 2,
+ pre_release: 1,
+ }),
+ "v42.0.2-1+OP-mod",
+ b256!("00000000000000004f502d6d6f6400000000002a000000000000000200000001"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(&[b'a', b'b', 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
+ major: 42,
+ minor: 0,
+ patch: 2,
+ pre_release: 0,
+ }),
+ "v42.0.2+0x6162010000000000", // do not render invalid alpha numeric
+ b256!("000000000000000061620100000000000000002a000000000000000200000000"),
+ ),
+ (
+ ProtocolVersion::V0(ProtocolVersionFormatV0 {
+ build: B64::from_slice(b"beta.123"),
+ major: 1,
+ minor: 0,
+ patch: 0,
+ pre_release: 0,
+ }),
+ "v1.0.0+beta.123",
+ b256!("0000000000000000626574612e31323300000001000000000000000000000000"),
+ ),
+ ];
+
+ for (decoded_exp, formatted_exp, encoded_exp) in test_cases {
+ encode_decode_v0(encoded_exp, formatted_exp, decoded_exp);
+ }
+ }
+
+ fn encode_decode_v0(encoded_exp: B256, formatted_exp: &str, decoded_exp: ProtocolVersion) {
+ let decoded = ProtocolVersion::decode(encoded_exp).unwrap();
+ assert_eq!(decoded, decoded_exp);
+
+ let encoded = decoded.encode();
+ assert_eq!(encoded, encoded_exp);
+
+ let formatted = decoded.display();
+ assert_eq!(formatted, formatted_exp);
+ }
+}