Skip to main content

miden_client/rpc/errors/
mod.rs

1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use core::error::Error;
4use core::fmt;
5use core::num::TryFromIntError;
6
7use miden_protocol::account::AccountId;
8use miden_protocol::crypto::merkle::MerkleError;
9use miden_protocol::errors::NoteError;
10use miden_protocol::note::NoteId;
11use miden_protocol::utils::serde::DeserializationError;
12use thiserror::Error;
13
14use super::RpcEndpoint;
15
16pub mod node;
17pub use node::{AddTransactionError, EndpointError};
18
19// RPC ERROR
20// ================================================================================================
21
22#[derive(Debug, Error)]
23pub enum RpcError {
24    #[error("accept header validation failed")]
25    AcceptHeaderError(#[from] AcceptHeaderError),
26    #[error(
27        "unexpected update received for private account {0}; private account state should not be sent by the node"
28    )]
29    AccountUpdateForPrivateAccountReceived(AccountId),
30    #[error("failed to connect to the Miden node")]
31    ConnectionError(#[source] Box<dyn Error + Send + Sync + 'static>),
32    #[error("failed to deserialize response from the Miden node: {0}")]
33    DeserializationError(String),
34    #[error("Miden node response is missing expected field '{0}'")]
35    ExpectedDataMissing(String),
36    #[error("rpc pagination error: {0}")]
37    PaginationError(String),
38    #[error("received an invalid response from the Miden node: {0}")]
39    InvalidResponse(String),
40    #[error("grpc request failed for {endpoint}: {error_kind}{}",
41        endpoint_error.as_ref().map_or(String::new(), |e| format!(" ({e})")))]
42    RequestError {
43        endpoint: RpcEndpoint,
44        error_kind: GrpcError,
45        endpoint_error: Option<EndpointError>,
46        #[source]
47        source: Option<Box<dyn Error + Send + Sync + 'static>>,
48    },
49    #[error("note {0} was not found on the Miden node")]
50    NoteNotFound(NoteId),
51    #[error("failed to seal transaction inputs for submission: {0}")]
52    TransactionInputsSealingFailed(String),
53    #[error("the transaction encryption key served by the node was rejected: {0}")]
54    TransactionEncryptionKeyRejected(String),
55    #[error("invalid Miden node endpoint '{0}'; expected format: https://host:port")]
56    InvalidNodeEndpoint(String),
57}
58
59impl RpcError {
60    /// Returns the typed endpoint error if this is a request error, or `None` otherwise.
61    pub fn endpoint_error(&self) -> Option<&EndpointError> {
62        match self {
63            Self::RequestError { endpoint_error, .. } => endpoint_error.as_ref(),
64            _ => None,
65        }
66    }
67
68    /// Returns whether this is a submission rejected because the transaction inputs were sealed
69    /// against an encryption key the validator does not hold.
70    pub fn is_stale_transaction_encryption_key(&self) -> bool {
71        matches!(
72            self,
73            Self::RequestError {
74                endpoint: RpcEndpoint::SubmitProvenTx | RpcEndpoint::SubmitProvenBatch,
75                error_kind: GrpcError::FailedPrecondition,
76                ..
77            }
78        )
79    }
80}
81
82impl From<DeserializationError> for RpcError {
83    fn from(err: DeserializationError) -> Self {
84        Self::DeserializationError(err.to_string())
85    }
86}
87
88impl From<NoteError> for RpcError {
89    fn from(err: NoteError) -> Self {
90        Self::DeserializationError(err.to_string())
91    }
92}
93
94impl From<RpcConversionError> for RpcError {
95    fn from(err: RpcConversionError) -> Self {
96        Self::DeserializationError(err.to_string())
97    }
98}
99
100// RPC CONVERSION ERROR
101// ================================================================================================
102
103#[derive(Debug, Error)]
104pub enum RpcConversionError {
105    #[error("failed to deserialize")]
106    DeserializationError(#[from] DeserializationError),
107    #[error(
108        "invalid field element: value is outside the valid range (0..modulus, where modulus = 2^64 - 2^32 + 1)"
109    )]
110    NotAValidFelt,
111    #[error("invalid note type in node response")]
112    NoteTypeError(#[from] NoteError),
113    #[error("merkle proof error in node response")]
114    MerkleError(#[from] MerkleError),
115    #[error("invalid field in node response: {0}")]
116    InvalidField(String),
117    #[error("integer conversion failed in node response")]
118    InvalidInt(#[from] TryFromIntError),
119    #[error("field `{field_name}` expected to be present in protobuf representation of {entity}")]
120    MissingFieldInProtobufRepresentation {
121        entity: &'static str,
122        field_name: &'static str,
123    },
124}
125
126// GRPC ERROR KIND
127// ================================================================================================
128
129/// Categorizes gRPC errors based on their status codes and common patterns
130#[derive(Debug, Error)]
131pub enum GrpcError {
132    #[error("resource not found")]
133    NotFound,
134    #[error("invalid request parameters")]
135    InvalidArgument,
136    #[error("permission denied")]
137    PermissionDenied,
138    #[error("resource already exists")]
139    AlreadyExists,
140    #[error("request was rate-limited or the node's resources are exhausted; retry after a delay")]
141    ResourceExhausted,
142    #[error("precondition failed")]
143    FailedPrecondition,
144    #[error("operation was cancelled")]
145    Cancelled,
146    #[error("request to Miden node timed out; the node may be under heavy load")]
147    DeadlineExceeded,
148    #[error("Miden node is unavailable; check that the node is running and reachable")]
149    Unavailable,
150    #[error("Miden node returned an internal error; this is likely a node-side issue")]
151    Internal,
152    #[error("the requested method is not implemented by this version of the Miden node")]
153    Unimplemented,
154    #[error(
155        "request was rejected as unauthenticated; check your credentials and connection settings"
156    )]
157    Unauthenticated,
158    #[error("operation was aborted")]
159    Aborted,
160    #[error("operation was attempted past the valid range")]
161    OutOfRange,
162    #[error("unrecoverable data loss or corruption")]
163    DataLoss,
164    #[error("unknown error: {0}")]
165    Unknown(String),
166}
167
168impl GrpcError {
169    /// Creates a `GrpcError` from a gRPC status code following the official specification
170    /// <https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc>
171    pub fn from_code(code: i32, message: Option<String>) -> Self {
172        match code {
173            1 => Self::Cancelled,
174            2 => Self::Unknown(message.unwrap_or_default()),
175            3 => Self::InvalidArgument,
176            4 => Self::DeadlineExceeded,
177            5 => Self::NotFound,
178            6 => Self::AlreadyExists,
179            7 => Self::PermissionDenied,
180            8 => Self::ResourceExhausted,
181            9 => Self::FailedPrecondition,
182            10 => Self::Aborted,
183            11 => Self::OutOfRange,
184            12 => Self::Unimplemented,
185            13 => Self::Internal,
186            14 => Self::Unavailable,
187            15 => Self::DataLoss,
188            16 => Self::Unauthenticated,
189            _ => Self::Unknown(
190                message.unwrap_or_else(|| format!("Unknown gRPC status code: {code}")),
191            ),
192        }
193    }
194}
195
196// ACCEPT HEADER ERROR
197// ================================================================================================
198
199// TODO: Accept header errors are still parsed from message strings, which is fragile.
200// Ideally the node would return structured error codes for these too. See #1129.
201
202/// Errors that can occur during accept header validation.
203#[derive(Debug, Error)]
204pub enum AcceptHeaderError {
205    #[error("server rejected request - please check your version and network settings ({0})")]
206    NoSupportedMediaRange(AcceptHeaderContext),
207    #[error("server rejected request - parsing error: {0}")]
208    ParsingError(String),
209}
210
211/// Extra context attached to Accept header negotiation failures.
212#[derive(Debug, Clone)]
213pub struct AcceptHeaderContext {
214    pub client_version: String,
215    pub genesis_commitment: String,
216}
217
218impl AcceptHeaderContext {
219    pub fn unknown() -> Self {
220        Self {
221            client_version: "unknown".to_string(),
222            genesis_commitment: "unknown".to_string(),
223        }
224    }
225}
226
227impl fmt::Display for AcceptHeaderContext {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(
230            f,
231            "client version: {}, genesis commitment: {}",
232            self.client_version, self.genesis_commitment
233        )
234    }
235}
236
237impl AcceptHeaderError {
238    /// Try to parse an accept header error from a message string, adding context.
239    pub fn try_from_message_with_context(
240        message: &str,
241        context: AcceptHeaderContext,
242    ) -> Option<Self> {
243        // Check for the main compatibility error message
244        if message.contains(
245            "server does not support any of the specified application/vnd.miden content types",
246        ) {
247            return Some(Self::NoSupportedMediaRange(context));
248        }
249        if message.contains("genesis value failed to parse")
250            || message.contains("version value failed to parse")
251        {
252            return Some(Self::ParsingError(message.to_string()));
253        }
254        None
255    }
256}