ckb_script/
error.rs

1use crate::types::{ScriptGroup, ScriptGroupType};
2use ckb_error::{Error, ErrorKind, InternalErrorKind, prelude::*};
3use ckb_types::core::{Cycle, ScriptHashType};
4use ckb_types::packed::{Byte32, Script};
5use ckb_vm::Error as VMInternalError;
6use std::{error::Error as StdError, fmt};
7
8/// Script execution error.
9#[derive(Error, Debug, PartialEq, Eq, Clone)]
10pub enum ScriptError {
11    /// The field code_hash in script can't be resolved
12    #[error("ScriptNotFound: code_hash: {0}")]
13    ScriptNotFound(Byte32),
14
15    /// The script consumes too much cycles
16    #[error("ExceededMaximumCycles: expect cycles <= {0}")]
17    ExceededMaximumCycles(Cycle),
18
19    /// Internal error cycles overflow
20    #[error("CyclesOverflow: lhs {0} rhs {1}")]
21    CyclesOverflow(Cycle, Cycle),
22
23    /// `script.type_hash` hits multiple cells with different data
24    #[error("MultipleMatches")]
25    MultipleMatches,
26
27    /// Non-zero exit code returns by script
28    #[error(
29        "ValidationFailure: see error code {1} on page https://nervosnetwork.github.io/ckb-script-error-codes/{0}.html#{1}"
30    )]
31    ValidationFailure(String, i8),
32
33    /// Known bugs are detected in transaction script outputs
34    #[error("Known bugs encountered in output {1}: {0}")]
35    EncounteredKnownBugs(String, usize),
36
37    /// InvalidScriptHashType
38    #[error("InvalidScriptHashType: {0}")]
39    InvalidScriptHashType(String),
40
41    /// InvalidVmVersion
42    #[error("Invalid VM Version: {0}")]
43    InvalidVmVersion(u8),
44
45    /// Errors thrown by ckb-vm
46    #[error("VM Internal Error: {0:?}")]
47    VMInternalError(VMInternalError),
48
49    /// Interrupts, such as a Ctrl-C signal
50    #[error("VM Interrupts")]
51    Interrupts,
52
53    /// Other errors raised in script execution process
54    #[error("Other Error: {0}")]
55    Other(String),
56}
57
58/// Locate the script using the first input index if possible, otherwise the first output index.
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum TransactionScriptErrorSource {
61    Inputs(usize, ScriptGroupType),
62    Outputs(usize, ScriptGroupType),
63    Unknown,
64}
65
66impl TransactionScriptErrorSource {
67    fn from_script_group(script_group: &ScriptGroup) -> Self {
68        if let Some(n) = script_group.input_indices.first() {
69            TransactionScriptErrorSource::Inputs(*n, script_group.group_type)
70        } else if let Some(n) = script_group.output_indices.first() {
71            TransactionScriptErrorSource::Outputs(*n, script_group.group_type)
72        } else {
73            TransactionScriptErrorSource::Unknown
74        }
75    }
76}
77
78impl fmt::Display for TransactionScriptErrorSource {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        match self {
81            TransactionScriptErrorSource::Inputs(n, field) => write!(f, "Inputs[{n}].{field}"),
82            TransactionScriptErrorSource::Outputs(n, field) => {
83                write!(f, "Outputs[{n}].{field}")
84            }
85            TransactionScriptErrorSource::Unknown => write!(f, "Unknown"),
86        }
87    }
88}
89
90/// Script execution error with the error source information.
91#[derive(Debug, PartialEq, Eq, Clone)]
92pub struct TransactionScriptError {
93    source: TransactionScriptErrorSource,
94    cause: ScriptError,
95}
96
97impl TransactionScriptError {
98    /// Originating script for the generated error
99    pub fn originating_script(&self) -> &TransactionScriptErrorSource {
100        &self.source
101    }
102
103    /// Actual error generated when verifying script
104    pub fn script_error(&self) -> &ScriptError {
105        &self.cause
106    }
107}
108
109/// It is a deliberate choice here to implement StdError directly, instead of
110/// implementing thiserror::Error on TransactionScriptError. This way, calling
111/// root_cause() on ckb_error::Error would return TransactionScriptError structure,
112/// providing us enough information to inspect on all kinds of errors generated when
113/// verifying a script.
114///
115/// This also means calling source() or cause() from std::error::Error on
116/// TransactionScriptError would return None values. One is expected to cast
117/// a value of `std::error::Error` type(possibly returned from root_cause) into
118/// concrete TransactionScriptError type, then use the defined methods to fetch
119/// originating script, as well as the actual script error. See the unit test defined
120/// at the end of this file for an example.
121impl StdError for TransactionScriptError {}
122
123impl fmt::Display for TransactionScriptError {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        write!(
126            f,
127            "TransactionScriptError {{ source: {}, cause: {} }}",
128            self.source, self.cause
129        )
130    }
131}
132
133impl ScriptError {
134    /// Creates a script error originated the script and its exit code.
135    pub fn validation_failure(script: &Script, exit_code: i8) -> ScriptError {
136        let url_path = match ScriptHashType::try_from(script.hash_type()).expect("checked data") {
137            ScriptHashType::Data | ScriptHashType::Data1 | ScriptHashType::Data2 => {
138                format!("by-data-hash/{:x}", script.code_hash())
139            }
140            ScriptHashType::Type => {
141                format!("by-type-hash/{:x}", script.code_hash())
142            }
143        };
144
145        ScriptError::ValidationFailure(url_path, exit_code)
146    }
147
148    ///  Creates a script error originated from the script group.
149    pub fn source(self, script_group: &ScriptGroup) -> TransactionScriptError {
150        TransactionScriptError {
151            source: TransactionScriptErrorSource::from_script_group(script_group),
152            cause: self,
153        }
154    }
155
156    /// Creates a script error originated from the lock script of the input cell at the specific index.
157    pub fn input_lock_script(self, index: usize) -> TransactionScriptError {
158        TransactionScriptError {
159            source: TransactionScriptErrorSource::Inputs(index, ScriptGroupType::Lock),
160            cause: self,
161        }
162    }
163
164    /// Creates a script error originated from the type script of the input cell at the specific index.
165    pub fn input_type_script(self, index: usize) -> TransactionScriptError {
166        TransactionScriptError {
167            source: TransactionScriptErrorSource::Inputs(index, ScriptGroupType::Type),
168            cause: self,
169        }
170    }
171
172    /// Creates a script error originated from the type script of the output cell at the specific index.
173    pub fn output_type_script(self, index: usize) -> TransactionScriptError {
174        TransactionScriptError {
175            source: TransactionScriptErrorSource::Outputs(index, ScriptGroupType::Type),
176            cause: self,
177        }
178    }
179
180    /// Creates a script error with unknown source, usually a internal error
181    pub fn unknown_source(self) -> TransactionScriptError {
182        TransactionScriptError {
183            source: TransactionScriptErrorSource::Unknown,
184            cause: self,
185        }
186    }
187}
188
189impl From<TransactionScriptError> for Error {
190    fn from(error: TransactionScriptError) -> Self {
191        match error.cause {
192            ScriptError::Interrupts => ErrorKind::Internal
193                .because(InternalErrorKind::Interrupts.other(ScriptError::Interrupts.to_string())),
194            _ => ErrorKind::Script.because(error),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use ckb_types::core::error::ARGV_TOO_LONG_TEXT;
203
204    #[test]
205    fn test_downcast_error_to_vm_error() {
206        let vm_error = VMInternalError::ElfParseError("Foo bar baz".to_string());
207        let script_error = ScriptError::VMInternalError(vm_error.clone());
208        let error: Error = script_error.output_type_script(177).into();
209
210        let recovered_transaction_error: TransactionScriptError = error
211            .root_cause()
212            .downcast_ref()
213            .cloned()
214            .expect("downcasting transaction error");
215        assert_eq!(
216            recovered_transaction_error.originating_script(),
217            &TransactionScriptErrorSource::Outputs(177, ScriptGroupType::Type),
218        );
219
220        if let ScriptError::VMInternalError(recovered_vm_error) =
221            recovered_transaction_error.script_error()
222        {
223            assert_eq!(recovered_vm_error, &vm_error);
224        } else {
225            panic!(
226                "Invalid script type: {}",
227                recovered_transaction_error.script_error()
228            );
229        }
230    }
231
232    #[test]
233    fn test_vm_internal_error_preserves_text() {
234        let vm_error = VMInternalError::Unexpected(ARGV_TOO_LONG_TEXT.to_string());
235        let script_error = ScriptError::VMInternalError(vm_error);
236        let error: Error = script_error.output_type_script(177).into();
237
238        assert!(format!("{}", error).contains(ARGV_TOO_LONG_TEXT));
239    }
240}