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` + +CI +maili-rpc-types-engine crate +MIT License +Apache License +Book + + +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); + } +}