reth_optimism_txpool/supervisor/
errors.rs

1//! Error types for the `kona-interop` crate.
2// Source: https://github.com/op-rs/kona
3// Copyright © 2023 kona contributors Copyright © 2024 Optimism
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6// associated documentation files (the “Software”), to deal in the Software without restriction,
7// including without limitation the rights to use, copy, modify, merge, publish, distribute,
8// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in all copies or
12// substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15// NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19use core::error;
20use op_alloy_consensus::interop::SafetyLevel;
21
22/// Derived from op-supervisor
23// todo: rm once resolved <https://github.com/ethereum-optimism/optimism/issues/14603>
24const UNKNOWN_CHAIN_MSG: &str = "unknown chain: ";
25/// Derived from [op-supervisor](https://github.com/ethereum-optimism/optimism/blob/4ba2eb00eafc3d7de2c8ceb6fd83913a8c0a2c0d/op-supervisor/supervisor/backend/backend.go#L479)
26// todo: rm once resolved <https://github.com/ethereum-optimism/optimism/issues/14603>
27const MINIMUM_SAFETY_MSG: &str = "does not meet the minimum safety";
28
29/// Invalid inbox entry
30#[derive(thiserror::Error, Debug)]
31pub enum InvalidInboxEntry {
32    /// Message does not meet minimum safety level
33    #[error("message does not meet min safety level, got: {got}, expected: {expected}")]
34    MinimumSafety {
35        /// Actual level of the message
36        got: SafetyLevel,
37        /// Minimum acceptable level that was passed to supervisor
38        expected: SafetyLevel,
39    },
40    /// Invalid chain
41    #[error("unsupported chain id: {0}")]
42    UnknownChain(u64),
43}
44
45impl InvalidInboxEntry {
46    /// Returns the [`SafetyLevel`] of message, if this is a [`MinimumSafety`](Self::MinimumSafety)
47    /// error.
48    pub fn msg_safety_level(&self) -> Option<SafetyLevel> {
49        match self {
50            Self::MinimumSafety { got, .. } => Some(*got),
51            Self::UnknownChain(_) => None,
52        }
53    }
54
55    /// Returns `true` if message is [`SafetyLevel::Invalid`]
56    pub fn is_msg_invalid(&self) -> bool {
57        matches!(self, Self::MinimumSafety { got: SafetyLevel::Invalid, .. })
58    }
59
60    /// Returns `true` if message is [`SafetyLevel::Unsafe`].
61    pub fn is_msg_unsafe(&self) -> bool {
62        matches!(self, Self::MinimumSafety { got: SafetyLevel::Unsafe, .. })
63    }
64
65    /// Returns `true` if message is [`SafetyLevel::CrossUnsafe`].
66    pub fn is_msg_cross_unsafe(&self) -> bool {
67        matches!(self, Self::MinimumSafety { got: SafetyLevel::CrossUnsafe, .. })
68    }
69
70    /// Returns `true` if message is [`SafetyLevel::LocalSafe`].
71    pub fn is_msg_local_safe(&self) -> bool {
72        matches!(self, Self::MinimumSafety { got: SafetyLevel::LocalSafe, .. })
73    }
74
75    /// Returns `true` if message is [`SafetyLevel::Safe`].
76    pub fn is_msg_safe(&self) -> bool {
77        matches!(self, Self::MinimumSafety { got: SafetyLevel::Safe, .. })
78    }
79
80    /// Returns `true` if message is at least [`SafetyLevel::CrossUnsafe`].
81    pub fn is_msg_at_least_cross_unsafe(&self) -> bool {
82        self.is_msg_cross_unsafe() || self.is_msg_local_safe() || self.is_msg_safe()
83    }
84
85    /// Parses error message. Returns `None`, if message is not recognized.
86    // todo: match on error code instead of message string once resolved <https://github.com/ethereum-optimism/optimism/issues/14603>
87    pub fn parse_err_msg(err_msg: &str) -> Option<Self> {
88        // Check if it's invalid message call, message example:
89        // `failed to check message: failed to check log: unknown chain: 14417`
90        if err_msg.contains(UNKNOWN_CHAIN_MSG) {
91            if let Ok(chain_id) =
92                err_msg.split(' ').next_back().expect("message contains chain id").parse::<u64>()
93            {
94                return Some(Self::UnknownChain(chain_id))
95            }
96        // Check if it's `does not meet the minimum safety` error, message example:
97        // `message {0x4200000000000000000000000000000000000023 4 1 1728507701 901}
98        // (safety level: unsafe) does not meet the minimum safety cross-unsafe"`
99        } else if err_msg.contains(MINIMUM_SAFETY_MSG) {
100            let message_safety = if err_msg.contains("safety level: safe") {
101                SafetyLevel::Safe
102            } else if err_msg.contains("safety level: local-safe") {
103                SafetyLevel::LocalSafe
104            } else if err_msg.contains("safety level: cross-unsafe") {
105                SafetyLevel::CrossUnsafe
106            } else if err_msg.contains("safety level: unsafe") {
107                SafetyLevel::Unsafe
108            } else if err_msg.contains("safety level: invalid") {
109                SafetyLevel::Invalid
110            } else {
111                // Unexpected level name
112                return None
113            };
114            let expected_safety = if err_msg.contains("safety finalized") {
115                SafetyLevel::Finalized
116            } else if err_msg.contains("safety safe") {
117                SafetyLevel::Safe
118            } else if err_msg.contains("safety local-safe") {
119                SafetyLevel::LocalSafe
120            } else if err_msg.contains("safety cross-unsafe") {
121                SafetyLevel::CrossUnsafe
122            } else if err_msg.contains("safety unsafe") {
123                SafetyLevel::Unsafe
124            } else {
125                // Unexpected level name
126                return None
127            };
128
129            return Some(Self::MinimumSafety { expected: expected_safety, got: message_safety })
130        }
131
132        None
133    }
134}
135
136/// Failures occurring during validation of inbox entries.
137#[derive(thiserror::Error, Debug)]
138pub enum InteropTxValidatorError {
139    /// Error validating interop event.
140    #[error(transparent)]
141    InvalidInboxEntry(#[from] InvalidInboxEntry),
142
143    /// RPC client failure.
144    #[error("supervisor rpc client failure: {0}")]
145    RpcClientError(Box<dyn error::Error + Send + Sync>),
146
147    /// Message validation against the Supervisor took longer than allowed.
148    #[error("message validation timed out, timeout: {0} secs")]
149    ValidationTimeout(u64),
150
151    /// Catch-all variant for other supervisor server errors.
152    #[error("unexpected error from supervisor: {0}")]
153    SupervisorServerError(Box<dyn error::Error + Send + Sync>),
154}
155
156impl InteropTxValidatorError {
157    /// Returns a new instance of [`RpcClientError`](Self::RpcClientError) variant.
158    pub fn client<E>(err: alloy_json_rpc::RpcError<E>) -> Self
159    where
160        E: error::Error + Send + Sync + 'static,
161    {
162        let err_msg = err.to_string();
163
164        // Trying to parse the InvalidInboxEntry
165        if let Some(invalid_entry) = InvalidInboxEntry::parse_err_msg(&err_msg) {
166            return Self::InvalidInboxEntry(invalid_entry);
167        }
168
169        Self::RpcClientError(Box::new(err))
170    }
171
172    /// Returns a new instance of [`RpcClientError`](Self::RpcClientError) variant.
173    pub fn server_unexpected(err: impl error::Error + Send + Sync + 'static) -> Self {
174        Self::SupervisorServerError(Box::new(err))
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use alloy_json_rpc::{ErrorPayload, RpcError};
182
183    const MIN_SAFETY_CROSS_UNSAFE_ERROR: &str = "message {0x4200000000000000000000000000000000000023 4 1 1728507701 901} (safety level: unsafe) does not meet the minimum safety cross-unsafe";
184    const MIN_SAFETY_UNSAFE_ERROR: &str = "message {0x4200000000000000000000000000000000000023 1091637521 4369 0 901} (safety level: invalid) does not meet the minimum safety unsafe";
185    const MIN_SAFETY_FINALIZED_ERROR: &str = "message {0x4200000000000000000000000000000000000023 1091600001 215 1170 901} (safety level: safe) does not meet the minimum safety finalized";
186    const INVALID_CHAIN: &str =
187        "failed to check message: failed to check log: unknown chain: 14417";
188    const RANDOM_ERROR: &str = "gibberish error";
189
190    #[test]
191    fn test_op_supervisor_error_parsing() {
192        assert!(matches!(
193            InvalidInboxEntry::parse_err_msg(MIN_SAFETY_CROSS_UNSAFE_ERROR).unwrap(),
194            InvalidInboxEntry::MinimumSafety {
195                expected: SafetyLevel::CrossUnsafe,
196                got: SafetyLevel::Unsafe
197            }
198        ));
199
200        assert!(matches!(
201            InvalidInboxEntry::parse_err_msg(MIN_SAFETY_UNSAFE_ERROR).unwrap(),
202            InvalidInboxEntry::MinimumSafety {
203                expected: SafetyLevel::Unsafe,
204                got: SafetyLevel::Invalid
205            }
206        ));
207
208        assert!(matches!(
209            InvalidInboxEntry::parse_err_msg(MIN_SAFETY_FINALIZED_ERROR).unwrap(),
210            InvalidInboxEntry::MinimumSafety {
211                expected: SafetyLevel::Finalized,
212                got: SafetyLevel::Safe,
213            }
214        ));
215
216        assert!(matches!(
217            InvalidInboxEntry::parse_err_msg(INVALID_CHAIN).unwrap(),
218            InvalidInboxEntry::UnknownChain(14417)
219        ));
220
221        assert!(InvalidInboxEntry::parse_err_msg(RANDOM_ERROR).is_none());
222    }
223
224    #[test]
225    fn test_client_error_parsing() {
226        let err =
227            ErrorPayload { code: 0, message: MIN_SAFETY_CROSS_UNSAFE_ERROR.into(), data: None };
228        let rpc_err = RpcError::<InvalidInboxEntry>::ErrorResp(err);
229        let error = InteropTxValidatorError::client(rpc_err);
230
231        assert!(matches!(
232            error,
233            InteropTxValidatorError::InvalidInboxEntry(InvalidInboxEntry::MinimumSafety {
234                expected: SafetyLevel::CrossUnsafe,
235                got: SafetyLevel::Unsafe
236            })
237        ));
238
239        // Testing with Unknown message
240        let err = ErrorPayload { code: 0, message: "unknown error".into(), data: None };
241        let rpc_err = RpcError::<InvalidInboxEntry>::ErrorResp(err);
242        let error = InteropTxValidatorError::client(rpc_err);
243        assert!(matches!(error, InteropTxValidatorError::RpcClientError(_)));
244    }
245}