maili_common/
superchain.rs

1//! Common engine types
2
3use alloc::{
4    format,
5    string::{String, ToString},
6};
7use core::array::TryFromSliceError;
8
9use alloy_primitives::{B256, B64};
10use derive_more::derive::{Display, From};
11
12/// Superchain Signal information.
13///
14/// The execution engine SHOULD warn the user when the recommended version is newer than the current
15/// version supported by the execution engine.
16///
17/// The execution engine SHOULD take safety precautions if it does not meet the required protocol
18/// version. This may include halting the engine, with consent of the execution engine operator.
19///
20/// See also: <https://specs.optimism.io/protocol/exec-engine.html#engine_signalsuperchainv1>
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
24pub struct SuperchainSignal {
25    /// The recommended Supercain Protocol Version.
26    pub recommended: ProtocolVersion,
27    /// The minimum Supercain Protocol Version required.
28    pub required: ProtocolVersion,
29}
30
31/// Formatted Superchain Protocol Version.
32///
33/// The Protocol Version documents the progression of the total set of canonical OP-Stack
34/// specifications. Components of the OP-Stack implement the subset of their respective protocol
35/// component domain, up to a given Protocol Version of the OP-Stack.
36///
37/// The Protocol Version **is NOT a hardfork identifier**, but rather indicates software-support for
38/// a well-defined set of features introduced in past and future hardforks, not the activation of
39/// said hardforks.
40///
41/// The Protocol Version is Semver-compatible. It is encoded as a single 32 bytes long
42/// protocol version. The version must be encoded as 32 bytes of DATA in JSON RPC usage.
43///
44/// See also: <https://specs.optimism.io/protocol/superchain-upgrades.html#protocol-version>
45#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum ProtocolVersion {
48    /// Version-type 0.
49    V0(ProtocolVersionFormatV0),
50}
51
52impl core::fmt::Display for ProtocolVersion {
53    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
54        match self {
55            Self::V0(value) => write!(f, "{}", value),
56        }
57    }
58}
59
60/// An error that can occur when encoding or decoding a ProtocolVersion.
61#[derive(Copy, Clone, Debug, Display, From)]
62pub enum ProtocolVersionError {
63    /// An unsupported version was encountered.
64    #[display("Unsupported version: {_0}")]
65    UnsupportedVersion(u8),
66    /// An invalid length was encountered.
67    #[display("Invalid length: got {}, expected {}", got, expected)]
68    InvalidLength {
69        /// The length that was encountered.
70        got: usize,
71        /// The expected length.
72        expected: usize,
73    },
74    /// Failed to convert slice to array.
75    #[display("Failed to convert slice to array")]
76    #[from(TryFromSliceError)]
77    TryFromSlice,
78}
79
80impl ProtocolVersion {
81    /// Version-type 0 byte encoding:
82    ///
83    /// ```text
84    /// <protocol version> ::= <version-type><typed-payload>
85    /// <version-type> ::= <uint8>
86    /// <typed-payload> ::= <31 bytes>
87    /// ```
88    pub fn encode(&self) -> B256 {
89        let mut bytes = [0u8; 32];
90
91        match self {
92            Self::V0(value) => {
93                bytes[0] = 0x00; // this is not necessary, but addded for clarity
94                bytes[1..].copy_from_slice(&value.encode());
95                B256::from_slice(&bytes)
96            }
97        }
98    }
99
100    /// Version-type 0 byte decoding:
101    ///
102    /// ```text
103    /// <protocol version> ::= <version-type><typed-payload>
104    /// <version-type> ::= <uint8>
105    /// <typed-payload> ::= <31 bytes>
106    /// ```
107    pub fn decode(value: B256) -> Result<Self, ProtocolVersionError> {
108        let version_type = value[0];
109        let typed_payload = &value[1..];
110
111        match version_type {
112            0 => Ok(Self::V0(ProtocolVersionFormatV0::decode(typed_payload)?)),
113            other => Err(ProtocolVersionError::UnsupportedVersion(other)),
114        }
115    }
116
117    /// Returns the inner value of the ProtocolVersion enum
118    pub const fn inner(&self) -> ProtocolVersionFormatV0 {
119        match self {
120            Self::V0(value) => *value,
121        }
122    }
123
124    /// Returns the inner value of the ProtocolVersion enum if it is V0, otherwise None
125    pub const fn as_v0(&self) -> Option<ProtocolVersionFormatV0> {
126        match self {
127            Self::V0(value) => Some(*value),
128        }
129    }
130
131    /// Differentiates forks and custom-builds of standard protocol
132    pub const fn build(&self) -> B64 {
133        match self {
134            Self::V0(value) => value.build,
135        }
136    }
137
138    /// Incompatible API changes
139    pub const fn major(&self) -> u32 {
140        match self {
141            Self::V0(value) => value.major,
142        }
143    }
144
145    /// Identifies additional functionality in backwards compatible manner
146    pub const fn minor(&self) -> u32 {
147        match self {
148            Self::V0(value) => value.minor,
149        }
150    }
151
152    /// Identifies backward-compatible bug-fixes
153    pub const fn patch(&self) -> u32 {
154        match self {
155            Self::V0(value) => value.patch,
156        }
157    }
158
159    /// Identifies unstable versions that may not satisfy the above
160    pub const fn pre_release(&self) -> u32 {
161        match self {
162            Self::V0(value) => value.pre_release,
163        }
164    }
165
166    /// Returns a human-readable string representation of the ProtocolVersion
167    pub fn display(&self) -> String {
168        match self {
169            Self::V0(value) => format!("{}", value),
170        }
171    }
172}
173
174#[cfg(feature = "serde")]
175impl serde::Serialize for ProtocolVersion {
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: serde::Serializer,
179    {
180        self.encode().serialize(serializer)
181    }
182}
183
184#[cfg(feature = "serde")]
185impl<'de> serde::Deserialize<'de> for ProtocolVersion {
186    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187    where
188        D: serde::Deserializer<'de>,
189    {
190        let value = alloy_primitives::B256::deserialize(deserializer)?;
191        Self::decode(value).map_err(serde::de::Error::custom)
192    }
193}
194
195/// The Protocol Version V0 format.
196/// Encoded as 31 bytes with the following structure:
197///
198/// ```text
199/// <reserved><build><major><minor><patch><pre-release>
200/// <reserved> ::= <7 zeroed bytes>
201/// <build> ::= <8 bytes>
202/// <major> ::= <big-endian uint32>
203/// <minor> ::= <big-endian uint32>
204/// <patch> ::= <big-endian uint32>
205/// <pre-release> ::= <big-endian uint32>
206/// ```
207#[derive(Copy, Clone, Debug, PartialEq, Eq)]
208pub struct ProtocolVersionFormatV0 {
209    /// Differentiates forks and custom-builds of standard protocol
210    pub build: B64,
211    /// Incompatible API changes
212    pub major: u32,
213    /// Identifies additional functionality in backwards compatible manner
214    pub minor: u32,
215    /// Identifies backward-compatible bug-fixes
216    pub patch: u32,
217    /// Identifies unstable versions that may not satisfy the above
218    pub pre_release: u32,
219}
220
221impl core::fmt::Display for ProtocolVersionFormatV0 {
222    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
223        let build_tag = if self.build.0.iter().any(|&byte| byte != 0) {
224            if is_human_readable_build_tag(self.build) {
225                let full = format!("+{}", String::from_utf8_lossy(&self.build.0));
226                full.trim_end_matches('\0').to_string()
227            } else {
228                format!("+{}", self.build)
229            }
230        } else {
231            String::new()
232        };
233
234        let pre_release_tag =
235            if self.pre_release != 0 { format!("-{}", self.pre_release) } else { String::new() };
236
237        write!(f, "v{}.{}.{}{}{}", self.major, self.minor, self.patch, pre_release_tag, build_tag)
238    }
239}
240
241impl ProtocolVersionFormatV0 {
242    /// Version-type 0 byte encoding:
243    ///
244    /// ```text
245    /// <reserved><build><major><minor><patch><pre-release>
246    /// <reserved> ::= <7 zeroed bytes>
247    /// <build> ::= <8 bytes>
248    /// <major> ::= <big-endian uint32>
249    /// <minor> ::= <big-endian uint32>
250    /// <patch> ::= <big-endian uint32>
251    /// <pre-release> ::= <big-endian uint32>
252    /// ```
253    pub fn encode(&self) -> [u8; 31] {
254        let mut bytes = [0u8; 31];
255        bytes[0..7].copy_from_slice(&[0u8; 7]);
256        bytes[7..15].copy_from_slice(&self.build.0);
257        bytes[15..19].copy_from_slice(&self.major.to_be_bytes());
258        bytes[19..23].copy_from_slice(&self.minor.to_be_bytes());
259        bytes[23..27].copy_from_slice(&self.patch.to_be_bytes());
260        bytes[27..31].copy_from_slice(&self.pre_release.to_be_bytes());
261        bytes
262    }
263
264    /// Version-type 0 byte encoding:
265    ///
266    /// ```text
267    /// <reserved><build><major><minor><patch><pre-release>
268    /// <reserved> ::= <7 zeroed bytes>
269    /// <build> ::= <8 bytes>
270    /// <major> ::= <big-endian uint32>
271    /// <minor> ::= <big-endian uint32>
272    /// <patch> ::= <big-endian uint32>
273    /// <pre-release> ::= <big-endian uint32>
274    /// ```
275    fn decode(value: &[u8]) -> Result<Self, ProtocolVersionError> {
276        if value.len() != 31 {
277            return Err(ProtocolVersionError::InvalidLength { got: value.len(), expected: 31 });
278        }
279
280        Ok(Self {
281            build: B64::from_slice(&value[7..15]),
282            major: u32::from_be_bytes(value[15..19].try_into()?),
283            minor: u32::from_be_bytes(value[19..23].try_into()?),
284            patch: u32::from_be_bytes(value[23..27].try_into()?),
285            pre_release: u32::from_be_bytes(value[27..31].try_into()?),
286        })
287    }
288}
289
290/// Returns true if the build tag is human-readable, false otherwise.
291fn is_human_readable_build_tag(build: B64) -> bool {
292    for (i, &c) in build.iter().enumerate() {
293        if c == 0 {
294            // Trailing zeros are allowed
295            if build[i..].iter().any(|&d| d != 0) {
296                return false;
297            }
298            return true;
299        }
300
301        // following semver.org advertised regex, alphanumeric with '-' and '.', except leading '.'.
302        if !(c.is_ascii_alphanumeric() || c == b'-' || (c == b'.' && i > 0)) {
303            return false;
304        }
305    }
306    true
307}
308
309#[cfg(test)]
310mod tests {
311    use alloy_primitives::b256;
312
313    use super::*;
314
315    #[test]
316    fn test_protocol_version_encode_decode() {
317        let test_cases = vec![
318            (
319                ProtocolVersion::V0(ProtocolVersionFormatV0 {
320                    build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
321                    major: 42,
322                    minor: 0,
323                    patch: 2,
324                    pre_release: 0,
325                }),
326                "v42.0.2+0x6162010000000000",
327                b256!("000000000000000061620100000000000000002a000000000000000200000000"),
328            ),
329            (
330                ProtocolVersion::V0(ProtocolVersionFormatV0 {
331                    build: B64::from_slice(&[0x61, 0x62, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
332                    major: 42,
333                    minor: 0,
334                    patch: 2,
335                    pre_release: 1,
336                }),
337                "v42.0.2-1+0x6162010000000000",
338                b256!("000000000000000061620100000000000000002a000000000000000200000001"),
339            ),
340            (
341                ProtocolVersion::V0(ProtocolVersionFormatV0 {
342                    build: B64::from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]),
343                    major: 42,
344                    minor: 0,
345                    patch: 2,
346                    pre_release: 0,
347                }),
348                "v42.0.2+0x0102030405060708",
349                b256!("000000000000000001020304050607080000002a000000000000000200000000"),
350            ),
351            (
352                ProtocolVersion::V0(ProtocolVersionFormatV0 {
353                    build: B64::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
354                    major: 0,
355                    minor: 100,
356                    patch: 2,
357                    pre_release: 0,
358                }),
359                "v0.100.2",
360                b256!("0000000000000000000000000000000000000000000000640000000200000000"),
361            ),
362            (
363                ProtocolVersion::V0(ProtocolVersionFormatV0 {
364                    build: B64::from_slice(&[b'O', b'P', b'-', b'm', b'o', b'd', 0x00, 0x00]),
365                    major: 42,
366                    minor: 0,
367                    patch: 2,
368                    pre_release: 1,
369                }),
370                "v42.0.2-1+OP-mod",
371                b256!("00000000000000004f502d6d6f6400000000002a000000000000000200000001"),
372            ),
373            (
374                ProtocolVersion::V0(ProtocolVersionFormatV0 {
375                    build: B64::from_slice(&[b'a', b'b', 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]),
376                    major: 42,
377                    minor: 0,
378                    patch: 2,
379                    pre_release: 0,
380                }),
381                "v42.0.2+0x6162010000000000", // do not render invalid alpha numeric
382                b256!("000000000000000061620100000000000000002a000000000000000200000000"),
383            ),
384            (
385                ProtocolVersion::V0(ProtocolVersionFormatV0 {
386                    build: B64::from_slice(b"beta.123"),
387                    major: 1,
388                    minor: 0,
389                    patch: 0,
390                    pre_release: 0,
391                }),
392                "v1.0.0+beta.123",
393                b256!("0000000000000000626574612e31323300000001000000000000000000000000"),
394            ),
395        ];
396
397        for (decoded_exp, formatted_exp, encoded_exp) in test_cases {
398            encode_decode_v0(encoded_exp, formatted_exp, decoded_exp);
399        }
400    }
401
402    fn encode_decode_v0(encoded_exp: B256, formatted_exp: &str, decoded_exp: ProtocolVersion) {
403        let decoded = ProtocolVersion::decode(encoded_exp).unwrap();
404        assert_eq!(decoded, decoded_exp);
405
406        let encoded = decoded.encode();
407        assert_eq!(encoded, encoded_exp);
408
409        let formatted = decoded.display();
410        assert_eq!(formatted, formatted_exp);
411    }
412}