iota_json_rpc/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use fastcrypto::error::FastCryptoError;
8use hyper::header::InvalidHeaderValue;
9use iota_json_rpc_api::{
10    TRANSACTION_EXECUTION_CLIENT_ERROR_CODE, TRANSIENT_ERROR_CODE, error_object_from_rpc,
11};
12use iota_names::error::IotaNamesError;
13use iota_types::{
14    error::{IotaError, IotaObjectResponseError, UserInputError},
15    quorum_driver_types::QuorumDriverError,
16};
17use itertools::Itertools;
18use jsonrpsee::{
19    core::{ClientError as RpcError, RegisterMethodError},
20    types::{
21        ErrorObject, ErrorObjectOwned,
22        error::{CALL_EXECUTION_FAILED_CODE, ErrorCode, INTERNAL_ERROR_CODE},
23    },
24};
25use thiserror::Error;
26use tokio::task::JoinError;
27
28use crate::authority_state::StateReadError;
29
30pub type RpcInterimResult<T = ()> = Result<T, Error>;
31
32#[derive(Debug, Error)]
33#[non_exhaustive]
34pub enum Error {
35    #[error(transparent)]
36    Iota(IotaError),
37
38    #[error(transparent)]
39    Internal(#[from] anyhow::Error),
40
41    #[error("Deserialization error: {0}")]
42    Bcs(#[from] bcs::Error),
43    #[error("Unexpected error: {0}")]
44    Unexpected(String),
45
46    #[error(transparent)]
47    RPCServer(#[from] RpcError),
48    #[error(transparent)]
49    RPCRegisterMethod(#[from] RegisterMethodError),
50
51    #[error(transparent)]
52    InvalidHeaderValue(#[from] InvalidHeaderValue),
53
54    #[error(transparent)]
55    UserInput(#[from] UserInputError),
56
57    #[error(transparent)]
58    Encoding(#[from] eyre::Report),
59
60    #[error(transparent)]
61    TokioJoin(#[from] JoinError),
62
63    #[error(transparent)]
64    QuorumDriver(#[from] QuorumDriverError),
65
66    #[error(transparent)]
67    FastCrypto(#[from] FastCryptoError),
68
69    #[error(transparent)]
70    IotaObjectResponse(#[from] IotaObjectResponseError),
71
72    #[error(transparent)]
73    IotaRpcInput(#[from] IotaRpcInputError),
74
75    // TODO(wlmyng): convert StateReadError::Internal message to generic internal error message.
76    #[error(transparent)]
77    StateRead(#[from] StateReadError),
78
79    #[error("Unsupported Feature: {0}")]
80    UnsupportedFeature(String),
81
82    #[error(transparent)]
83    IotaNames(#[from] IotaNamesError),
84}
85
86impl From<IotaError> for Error {
87    fn from(e: IotaError) -> Self {
88        match e {
89            IotaError::UserInput { error } => Self::UserInput(error),
90            IotaError::IotaObjectResponse { error } => Self::IotaObjectResponse(error),
91            IotaError::UnsupportedFeature { error } => Self::UnsupportedFeature(error),
92            IotaError::IndexStoreNotAvailable => Self::UnsupportedFeature(
93                "Required indexes are not available on this node".to_string(),
94            ),
95            other => Self::Iota(other),
96        }
97    }
98}
99
100impl From<Error> for RpcError {
101    /// `InvalidParams`/`INVALID_PARAMS_CODE` for client errors.
102    fn from(e: Error) -> RpcError {
103        match e {
104            Error::UserInput(_) | Error::UnsupportedFeature(_) => RpcError::Call(
105                ErrorObject::owned::<()>(ErrorCode::InvalidRequest.code(), e.to_string(), None),
106            ),
107            Error::IotaObjectResponse(err) => match err {
108                IotaObjectResponseError::NotExists { .. }
109                | IotaObjectResponseError::DynamicFieldNotFound { .. }
110                | IotaObjectResponseError::Deleted { .. }
111                | IotaObjectResponseError::Display { .. } => {
112                    RpcError::Call(ErrorObject::owned::<()>(
113                        ErrorCode::InvalidParams.code(),
114                        err.to_string(),
115                        None,
116                    ))
117                }
118                _ => RpcError::Call(ErrorObject::owned::<()>(
119                    CALL_EXECUTION_FAILED_CODE,
120                    err.to_string(),
121                    None,
122                )),
123            },
124            Error::IotaRpcInput(err) => RpcError::Call(ErrorObject::owned::<()>(
125                ErrorCode::InvalidParams.code(),
126                err.to_string(),
127                None,
128            )),
129            Error::Iota(iota_error) => match iota_error {
130                IotaError::TransactionNotFound { .. }
131                | IotaError::TransactionsNotFound { .. }
132                | IotaError::TransactionEventsNotFound { .. } => {
133                    RpcError::Call(ErrorObject::owned::<()>(
134                        ErrorCode::InvalidParams.code(),
135                        iota_error.to_string(),
136                        None,
137                    ))
138                }
139                _ => RpcError::Call(ErrorObject::owned::<()>(
140                    CALL_EXECUTION_FAILED_CODE,
141                    iota_error.to_string(),
142                    None,
143                )),
144            },
145            Error::StateRead(err) => match err {
146                StateReadError::Client(_) => RpcError::Call(ErrorObject::owned::<()>(
147                    ErrorCode::InvalidParams.code(),
148                    err.to_string(),
149                    None,
150                )),
151                _ => {
152                    let error_object = ErrorObject::owned::<()>(
153                        jsonrpsee::types::error::INTERNAL_ERROR_CODE,
154                        err.to_string(),
155                        None,
156                    );
157                    RpcError::Call(error_object)
158                }
159            },
160            Error::QuorumDriver(err) => {
161                match err {
162                    QuorumDriverError::InvalidUserSignature(err) => {
163                        let inner_error_str = match err {
164                            // TODO(wlmyng): update IotaError display trait to render UserInputError
165                            // with display
166                            IotaError::UserInput { error } => error.to_string(),
167                            _ => err.to_string(),
168                        };
169
170                        let error_message = format!("Invalid user signature: {inner_error_str}");
171
172                        let error_object = ErrorObject::owned::<()>(
173                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
174                            error_message,
175                            None,
176                        );
177                        RpcError::Call(error_object)
178                    }
179                    QuorumDriverError::TxAlreadyFinalizedWithDifferentUserSignatures => {
180                        let error_object = ErrorObject::owned::<()>(
181                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
182                            "The transaction is already finalized but with different user signatures",
183                            None,
184                        );
185                        RpcError::Call(error_object)
186                    }
187                    QuorumDriverError::TimeoutBeforeFinality
188                    | QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts { .. } => {
189                        let error_object =
190                            ErrorObject::owned::<()>(TRANSIENT_ERROR_CODE, err.to_string(), None);
191                        RpcError::Call(error_object)
192                    }
193                    QuorumDriverError::ObjectsDoubleUsed {
194                        conflicting_txes,
195                        retried_tx,
196                        retried_tx_success,
197                    } => {
198                        let error_message = format!(
199                            "Failed to sign transaction by a quorum of validators because of locked objects. Retried a conflicting transaction {:?}, success: {:?}",
200                            retried_tx, retried_tx_success
201                        );
202
203                        let new_map = conflicting_txes
204                            .into_iter()
205                            .map(|(digest, (pairs, _))| {
206                                (
207                                    digest,
208                                    pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
209                                )
210                            })
211                            .collect::<BTreeMap<_, Vec<_>>>();
212
213                        let error_object = ErrorObject::owned(
214                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
215                            error_message,
216                            Some(new_map),
217                        );
218                        RpcError::Call(error_object)
219                    }
220                    QuorumDriverError::NonRecoverableTransactionError { errors } => {
221                        let new_errors: Vec<String> = errors
222                            .into_iter()
223                            // sort by total stake, descending, so users see the most prominent one
224                            // first
225                            .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
226                            .filter_map(|(err, _, _)| {
227                                match &err {
228                                    // Special handling of UserInputError:
229                                    // ObjectNotFound and DependentPackageNotFound are considered
230                                    // retryable errors but they have different treatment
231                                    // in AuthorityAggregator.
232                                    // The optimal fix would be to examine if the total stake
233                                    // of ObjectNotFound/DependentPackageNotFound exceeds the
234                                    // quorum threshold, but it takes a Committee here.
235                                    // So, we take an easier route and consider them non-retryable
236                                    // at all. Combining this with the sorting above, clients will
237                                    // see the dominant error first.
238                                    IotaError::UserInput { error } => Some(error.to_string()),
239                                    _ => {
240                                        if err.is_retryable().0 {
241                                            None
242                                        } else {
243                                            Some(err.to_string())
244                                        }
245                                    }
246                                }
247                            })
248                            .collect();
249
250                        assert!(
251                            !new_errors.is_empty(),
252                            "NonRecoverableTransactionError should have at least one non-retryable error"
253                        );
254
255                        let error_list = new_errors.join(", ");
256                        let error_msg = format!(
257                            "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
258                            error_list
259                        );
260
261                        let error_object = ErrorObject::owned::<()>(
262                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
263                            error_msg,
264                            None,
265                        );
266                        RpcError::Call(error_object)
267                    }
268                    QuorumDriverError::QuorumDriverInternal(_) => {
269                        let error_object = ErrorObject::owned::<()>(
270                            INTERNAL_ERROR_CODE,
271                            "Internal error occurred while executing transaction.",
272                            None,
273                        );
274                        RpcError::Call(error_object)
275                    }
276                    QuorumDriverError::SystemOverload { .. }
277                    | QuorumDriverError::SystemOverloadRetryAfter { .. } => {
278                        let error_object =
279                            ErrorObject::owned::<()>(TRANSIENT_ERROR_CODE, err.to_string(), None);
280                        RpcError::Call(error_object)
281                    }
282                }
283            }
284            _ => RpcError::Call(ErrorObject::owned::<()>(
285                CALL_EXECUTION_FAILED_CODE,
286                e.to_string(),
287                None,
288            )),
289        }
290    }
291}
292
293impl From<Error> for ErrorObjectOwned {
294    fn from(value: Error) -> Self {
295        error_object_from_rpc(value.into())
296    }
297}
298
299#[derive(Debug, Error)]
300pub enum IotaRpcInputError {
301    #[error("Input contains duplicates")]
302    ContainsDuplicates,
303
304    #[error("Input exceeds limit of {0}")]
305    SizeLimitExceeded(String),
306
307    #[error("{0}")]
308    GenericNotFound(String),
309
310    #[error("{0}")]
311    GenericInvalid(String),
312
313    #[error(
314        "request_type` must set to `None` or `WaitForLocalExecution` if effects is required in the response"
315    )]
316    InvalidExecuteTransactionRequestType,
317
318    #[error("Unsupported protocol version requested. Min supported: {0}, max supported: {1}")]
319    ProtocolVersionUnsupported(u64, u64),
320
321    #[error("{0}")]
322    CannotParseIotaStructTag(String),
323
324    #[error(transparent)]
325    Base64(#[from] eyre::Report),
326
327    #[error("Deserialization error: {0}")]
328    Bcs(#[from] bcs::Error),
329
330    #[error(transparent)]
331    FastCrypto(#[from] FastCryptoError),
332
333    #[error(transparent)]
334    Anyhow(#[from] anyhow::Error),
335
336    #[error(transparent)]
337    UserInput(#[from] UserInputError),
338}
339
340impl From<IotaRpcInputError> for RpcError {
341    fn from(e: IotaRpcInputError) -> Self {
342        RpcError::Call(ErrorObject::owned::<()>(
343            ErrorCode::InvalidParams.code(),
344            e.to_string(),
345            None,
346        ))
347    }
348}
349
350impl From<IotaRpcInputError> for ErrorObjectOwned {
351    fn from(value: IotaRpcInputError) -> Self {
352        error_object_from_rpc(value.into())
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use expect_test::expect;
359    use iota_types::{
360        base_types::{AuthorityName, ObjectID, ObjectRef, SequenceNumber},
361        committee::StakeUnit,
362        crypto::{AuthorityPublicKey, AuthorityPublicKeyBytes},
363        digests::{ObjectDigest, TransactionDigest},
364    };
365
366    use super::*;
367
368    fn test_object_ref() -> ObjectRef {
369        (
370            ObjectID::ZERO,
371            SequenceNumber::from_u64(0),
372            ObjectDigest::new([0; 32]),
373        )
374    }
375
376    mod match_quorum_driver_error_tests {
377        use super::*;
378
379        #[test]
380        fn test_invalid_user_signature() {
381            let quorum_driver_error =
382                QuorumDriverError::InvalidUserSignature(IotaError::InvalidSignature {
383                    error: "Test inner invalid signature".to_string(),
384                });
385
386            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
387
388            let error_object = error_object_from_rpc(rpc_error);
389            let expected_code = expect!["-32002"];
390            expected_code.assert_eq(&error_object.code().to_string());
391            let expected_message = expect![
392                "Invalid user signature: Signature is not valid: Test inner invalid signature"
393            ];
394            expected_message.assert_eq(error_object.message());
395        }
396
397        #[test]
398        fn test_timeout_before_finality() {
399            let quorum_driver_error = QuorumDriverError::TimeoutBeforeFinality;
400
401            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
402
403            let error_object = error_object_from_rpc(rpc_error);
404            let expected_code = expect!["-32050"];
405            expected_code.assert_eq(&error_object.code().to_string());
406            let expected_message = expect!["Transaction timed out before reaching finality"];
407            expected_message.assert_eq(error_object.message());
408        }
409
410        #[test]
411        fn test_failed_with_transient_error_after_maximum_attempts() {
412            let quorum_driver_error =
413                QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts {
414                    total_attempts: 10,
415                };
416
417            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
418
419            let error_object = error_object_from_rpc(rpc_error);
420            let expected_code = expect!["-32050"];
421            expected_code.assert_eq(&error_object.code().to_string());
422            let expected_message = expect![
423                "Transaction failed to reach finality with transient error after 10 attempts."
424            ];
425            expected_message.assert_eq(error_object.message());
426        }
427
428        #[test]
429        fn test_objects_double_used() {
430            use iota_types::crypto::VerifyingKey;
431            let mut conflicting_txes: BTreeMap<
432                TransactionDigest,
433                (Vec<(AuthorityName, ObjectRef)>, StakeUnit),
434            > = BTreeMap::new();
435            let tx_digest = TransactionDigest::default();
436            let object_ref = test_object_ref();
437            let stake_unit: StakeUnit = 10;
438            let authority_name = AuthorityPublicKeyBytes([0; AuthorityPublicKey::LENGTH]);
439            conflicting_txes.insert(tx_digest, (vec![(authority_name, object_ref)], stake_unit));
440
441            let quorum_driver_error = QuorumDriverError::ObjectsDoubleUsed {
442                conflicting_txes,
443                retried_tx: Some(TransactionDigest::default()),
444                retried_tx_success: Some(true),
445            };
446
447            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
448
449            let error_object = error_object_from_rpc(rpc_error);
450            let expected_code = expect!["-32002"];
451            expected_code.assert_eq(&error_object.code().to_string());
452            let expected_message = expect![
453                "Failed to sign transaction by a quorum of validators because of locked objects. Retried a conflicting transaction Some(TransactionDigest(11111111111111111111111111111111)), success: Some(true)"
454            ];
455            expected_message.assert_eq(error_object.message());
456            let expected_data = expect![[
457                r#"{"11111111111111111111111111111111":[["0x0000000000000000000000000000000000000000000000000000000000000000",0,"11111111111111111111111111111111"]]}"#
458            ]];
459            let actual_data = error_object.data().unwrap().to_string();
460            expected_data.assert_eq(&actual_data);
461        }
462
463        #[test]
464        fn test_non_recoverable_transaction_error() {
465            let quorum_driver_error = QuorumDriverError::NonRecoverableTransactionError {
466                errors: vec![
467                    (
468                        IotaError::UserInput {
469                            error: UserInputError::GasBalanceTooLow {
470                                gas_balance: 10,
471                                needed_gas_amount: 100,
472                            },
473                        },
474                        0,
475                        vec![],
476                    ),
477                    (
478                        IotaError::UserInput {
479                            error: UserInputError::ObjectVersionUnavailableForConsumption {
480                                provided_obj_ref: test_object_ref(),
481                                current_version: 10.into(),
482                            },
483                        },
484                        0,
485                        vec![],
486                    ),
487                ],
488            };
489
490            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
491
492            let error_object = error_object_from_rpc(rpc_error);
493            let expected_code = expect!["-32002"];
494            expected_code.assert_eq(&error_object.code().to_string());
495            let expected_message = expect![
496                "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: Balance of gas object 10 is lower than the needed amount: 100, Object (0x0000000000000000000000000000000000000000000000000000000000000000, SequenceNumber(0), o#11111111111111111111111111111111) is not available for consumption, its current version: SequenceNumber(10)."
497            ];
498            expected_message.assert_eq(error_object.message());
499        }
500
501        #[test]
502        fn test_non_recoverable_transaction_error_with_transient_errors() {
503            let quorum_driver_error = QuorumDriverError::NonRecoverableTransactionError {
504                errors: vec![
505                    (
506                        IotaError::UserInput {
507                            error: UserInputError::ObjectNotFound {
508                                object_id: test_object_ref().0,
509                                version: None,
510                            },
511                        },
512                        0,
513                        vec![],
514                    ),
515                    (
516                        IotaError::Rpc("Hello".to_string(), "Testing".to_string()),
517                        0,
518                        vec![],
519                    ),
520                ],
521            };
522
523            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
524
525            let error_object = error_object_from_rpc(rpc_error);
526            let expected_code = expect!["-32002"];
527            expected_code.assert_eq(&error_object.code().to_string());
528            let expected_message = expect![
529                "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: Could not find the referenced object 0x0000000000000000000000000000000000000000000000000000000000000000 at version None."
530            ];
531            expected_message.assert_eq(error_object.message());
532        }
533
534        #[test]
535        fn test_quorum_driver_internal_error() {
536            let quorum_driver_error =
537                QuorumDriverError::QuorumDriverInternal(IotaError::UnexpectedMessage);
538
539            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
540
541            let error_object = error_object_from_rpc(rpc_error);
542            let expected_code = expect!["-32603"];
543            expected_code.assert_eq(&error_object.code().to_string());
544            let expected_message = expect!["Internal error occurred while executing transaction."];
545            expected_message.assert_eq(error_object.message());
546        }
547
548        #[test]
549        fn test_system_overload() {
550            let quorum_driver_error = QuorumDriverError::SystemOverload {
551                overloaded_stake: 10,
552                errors: vec![(IotaError::UnexpectedMessage, 0, vec![])],
553            };
554
555            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
556
557            let error_object = error_object_from_rpc(rpc_error);
558            let expected_code = expect!["-32050"];
559            expected_code.assert_eq(&error_object.code().to_string());
560            let expected_message = expect![
561                "Transaction is not processed because 10 of validators by stake are overloaded with certificates pending execution."
562            ];
563            expected_message.assert_eq(error_object.message());
564        }
565    }
566}