starknet_devnet_core/
error.rs

1use blockifier::execution::stack_trace::{
2    ErrorStack, ErrorStackHeader, ErrorStackSegment, PreambleType, gen_tx_execution_error_trace,
3};
4use blockifier::fee::fee_checks::FeeCheckError;
5use blockifier::transaction::errors::{
6    TransactionExecutionError, TransactionFeeError, TransactionPreValidationError,
7};
8use starknet_api::core::Nonce;
9use starknet_rs_core::types::Felt;
10use starknet_types;
11use starknet_types::contract_address::ContractAddress;
12use starknet_types::contract_storage_key::ContractStorageKey;
13use starknet_types::rpc::block::BlockId;
14use thiserror::Error;
15
16#[derive(Error, Debug)]
17pub enum Error {
18    #[error(transparent)]
19    StarknetApiError(#[from] starknet_api::StarknetApiError),
20    #[error(transparent)]
21    StateError(#[from] StateError),
22    #[error(transparent)]
23    BlockifierStateError(#[from] blockifier::state::errors::StateError),
24    #[error("{0:?}")]
25    ContractExecutionError(ContractExecutionError),
26    #[error("Execution error in simulating transaction no. {failure_index}: {execution_error:?}")]
27    ContractExecutionErrorInSimulation {
28        failure_index: usize,
29        execution_error: ContractExecutionError,
30    },
31    #[error("Types error: {0}")]
32    TypesError(#[from] starknet_types::error::Error),
33    #[error("I/O error: {0}")]
34    IoError(#[from] std::io::Error),
35    #[error("Error when reading file {path}")]
36    ReadFileError { source: std::io::Error, path: String },
37    #[error("The file does not exist")]
38    FileNotFound,
39    #[error("Contract not found")]
40    ContractNotFound,
41    #[error(transparent)]
42    SignError(#[from] starknet_rs_signers::local_wallet::SignError),
43    #[error("No block found")]
44    NoBlock,
45    #[error("No state at block {block_id:?}; consider running with --state-archive-capacity full")]
46    NoStateAtBlock { block_id: BlockId },
47    #[error("Format error")]
48    FormatError,
49    #[error("No transaction found")]
50    NoTransaction,
51    #[error("Invalid transaction index in a block")]
52    InvalidTransactionIndexInBlock,
53    #[error("Unsupported transaction type")]
54    UnsupportedTransactionType,
55    #[error("{msg}")]
56    UnsupportedAction { msg: String },
57    #[error("Unexpected internal error: {msg}")]
58    UnexpectedInternalError { msg: String },
59    #[error("Failed to load ContractClass: {0}")]
60    ContractClassLoadError(String),
61    #[error("Deserialization error: {origin}")]
62    DeserializationError { origin: String },
63    #[error("Serialization error: {origin}")]
64    SerializationError { origin: String },
65    #[error("Serialization not supported: {obj_name}")]
66    SerializationNotSupported { obj_name: String },
67    #[error(transparent)]
68    TransactionValidationError(#[from] TransactionValidationError),
69    #[error(transparent)]
70    TransactionFeeError(blockifier::transaction::errors::TransactionFeeError),
71    #[error(transparent)]
72    MessagingError(#[from] MessagingError),
73    #[error("Transaction has no trace")]
74    NoTransactionTrace,
75    #[error("the compiled class hash did not match the one supplied in the transaction")]
76    CompiledClassHashMismatch,
77    #[error("{msg}")]
78    ClassAlreadyDeclared { msg: String },
79    #[error("Requested entrypoint does not exist in the contract")]
80    EntrypointNotFound,
81    #[error("Contract class size is too large")]
82    ContractClassSizeIsTooLarge,
83}
84
85impl From<starknet_types_core::felt::FromStrError> for Error {
86    fn from(value: starknet_types_core::felt::FromStrError) -> Self {
87        Self::UnexpectedInternalError { msg: value.to_string() }
88    }
89}
90
91#[derive(Debug, Error)]
92pub enum StateError {
93    #[error("No class hash {0:x} found")]
94    NoneClassHash(Felt),
95    #[error("No compiled class hash found for class_hash {0:x}")]
96    NoneCompiledHash(Felt),
97    #[error("No casm class found for hash {0:x}")]
98    NoneCasmClass(Felt),
99    #[error("No contract state assigned for contact address: {0:x}")]
100    NoneContractState(ContractAddress),
101    #[error("No storage value assigned for: {0}")]
102    NoneStorage(ContractStorageKey),
103}
104
105#[derive(Debug, Error)]
106pub enum TransactionValidationError {
107    #[error("The transaction's resources don't cover validation or the minimal transaction fee.")]
108    InsufficientResourcesForValidate,
109    #[error("Account transaction nonce is invalid.")]
110    InvalidTransactionNonce {
111        address: ContractAddress,
112        account_nonce: Nonce,
113        incoming_tx_nonce: Nonce,
114    },
115    #[error("Account balance is not enough to cover the transaction cost.")]
116    InsufficientAccountBalance,
117    #[error("Account validation failed: {reason}")]
118    ValidationFailure { reason: String },
119}
120
121impl From<TransactionExecutionError> for Error {
122    fn from(value: TransactionExecutionError) -> Self {
123        match value {
124            TransactionExecutionError::TransactionPreValidationError(err) => match *err {
125                TransactionPreValidationError::InvalidNonce {
126                    address,
127                    account_nonce,
128                    incoming_tx_nonce,
129                } => Self::TransactionValidationError(
130                    TransactionValidationError::InvalidTransactionNonce {
131                        address: address.into(),
132                        account_nonce,
133                        incoming_tx_nonce,
134                    },
135                ),
136                TransactionPreValidationError::StateError(err) => {
137                    Self::ContractExecutionError(err.to_string().into())
138                }
139                TransactionPreValidationError::TransactionFeeError(tx_fee_err) => {
140                    Self::from(*tx_fee_err)
141                }
142            },
143            TransactionExecutionError::FeeCheckError(err) => err.into(),
144            TransactionExecutionError::TransactionFeeError(err) => (*err).into(),
145            TransactionExecutionError::ValidateTransactionError { .. } => {
146                TransactionValidationError::ValidationFailure { reason: value.to_string() }.into()
147            }
148            err @ TransactionExecutionError::DeclareTransactionError { .. } => {
149                Error::ClassAlreadyDeclared { msg: err.to_string() }
150            }
151            TransactionExecutionError::PanicInValidate { panic_reason } => {
152                TransactionValidationError::ValidationFailure { reason: panic_reason.to_string() }
153                    .into()
154            }
155            other => Self::ContractExecutionError(other.into()),
156        }
157    }
158}
159
160impl From<FeeCheckError> for Error {
161    fn from(value: FeeCheckError) -> Self {
162        match value {
163            FeeCheckError::MaxGasAmountExceeded { .. } | FeeCheckError::MaxFeeExceeded { .. } => {
164                TransactionValidationError::InsufficientResourcesForValidate.into()
165            }
166            FeeCheckError::InsufficientFeeTokenBalance { .. } => {
167                TransactionValidationError::InsufficientAccountBalance.into()
168            }
169        }
170    }
171}
172
173impl From<TransactionFeeError> for Error {
174    fn from(value: TransactionFeeError) -> Self {
175        #[warn(clippy::wildcard_enum_match_arm)]
176        match value {
177            TransactionFeeError::FeeTransferError { .. }
178            | TransactionFeeError::MaxFeeTooLow { .. }
179            | TransactionFeeError::InsufficientResourceBounds { .. } => {
180                TransactionValidationError::InsufficientResourcesForValidate.into()
181            }
182            TransactionFeeError::MaxFeeExceedsBalance { .. }
183            | TransactionFeeError::ResourcesBoundsExceedBalance { .. }
184            | TransactionFeeError::GasBoundsExceedBalance { .. } => {
185                TransactionValidationError::InsufficientAccountBalance.into()
186            }
187            err @ (TransactionFeeError::CairoResourcesNotContainedInFeeCosts
188            | TransactionFeeError::ExecuteFeeTransferError(_)
189            | TransactionFeeError::InsufficientFee { .. }
190            | TransactionFeeError::MissingL1GasBounds
191            | TransactionFeeError::StateError(_)) => Error::TransactionFeeError(err),
192        }
193    }
194}
195
196#[derive(Debug, Error)]
197pub enum MessagingError {
198    #[error(
199        "Message is not configured, ensure you've used `postman/load_l1_messaging_contract` \
200         endpoint first."
201    )]
202    NotConfigured,
203    #[error("An error has occurred during messages conversion: {0}.")]
204    ConversionError(String),
205    #[error("Alloy error: {0}.")]
206    AlloyError(String),
207    #[error("Message to L1 with hash {0} is not present (never received OR already consumed).")]
208    MessageToL1NotPresent(String),
209    #[error("L1 not compatible: {0}")]
210    IncompatibleL1(String),
211}
212
213pub type DevnetResult<T, E = Error> = Result<T, E>;
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct InnerContractExecutionError {
217    pub contract_address: starknet_api::core::ContractAddress,
218    pub class_hash: Felt,
219    pub selector: Felt,
220    #[serde(skip)]
221    pub return_data: Retdata,
222    pub error: Box<ContractExecutionError>,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(untagged)]
227pub enum ContractExecutionError {
228    /// Nested contract call stack trace frame.
229    Nested(InnerContractExecutionError),
230    /// Terminal error message.
231    Message(String),
232}
233
234impl From<String> for ContractExecutionError {
235    fn from(value: String) -> Self {
236        ContractExecutionError::Message(value)
237    }
238}
239
240use blockifier::execution::call_info::{CallInfo, Retdata};
241use serde::{Deserialize, Serialize};
242
243impl From<TransactionExecutionError> for ContractExecutionError {
244    fn from(value: TransactionExecutionError) -> Self {
245        let error_stack = gen_tx_execution_error_trace(&value);
246        error_stack.into()
247    }
248}
249
250fn preamble_type_to_error_msg(preamble_type: &PreambleType) -> &'static str {
251    match preamble_type {
252        PreambleType::CallContract => "Error in external contract",
253        PreambleType::LibraryCall => "Error in library call",
254        PreambleType::Constructor => "Error in constructor",
255    }
256}
257
258fn header_to_error_msg(header: &ErrorStackHeader) -> &'static str {
259    match header {
260        ErrorStackHeader::Constructor => "Constructor error",
261        ErrorStackHeader::Execution => "Execution error",
262        ErrorStackHeader::Validation => "Validation error",
263        ErrorStackHeader::None => "Unknown error",
264    }
265}
266
267/// [[[error inner] error outer] root]
268impl From<ErrorStack> for ContractExecutionError {
269    fn from(error_stack: ErrorStack) -> Self {
270        let error_string = error_stack.to_string();
271        fn format_error(stringified_error: &str, error_cause: &str) -> String {
272            if stringified_error.is_empty() {
273                error_cause.to_string()
274            } else {
275                format!("{} {}", stringified_error, error_cause)
276            }
277        }
278
279        let mut recursive_error_option = Option::<ContractExecutionError>::None;
280        for frame in error_stack.stack.iter().rev() {
281            let stack_err = match frame {
282                ErrorStackSegment::Cairo1RevertSummary(revert_summary) => {
283                    let mut recursive_error = ContractExecutionError::Message(format_error(
284                        &error_string,
285                        &serde_json::to_string(&revert_summary.last_retdata.0).unwrap_or_default(),
286                    ));
287
288                    for trace in revert_summary.stack.iter().rev() {
289                        recursive_error =
290                            ContractExecutionError::Nested(InnerContractExecutionError {
291                                contract_address: trace.contract_address,
292                                class_hash: trace.class_hash.unwrap_or_default().0,
293                                selector: trace.selector.0,
294                                return_data: revert_summary.last_retdata.clone(),
295                                error: Box::new(recursive_error),
296                            });
297                    }
298
299                    recursive_error
300                }
301
302                // VMException frame is omitted, unless it's the last frame of the error stack. It
303                // doesn't produce any meaningful message to the developer.
304                ErrorStackSegment::Vm(vm) => recursive_error_option.take().unwrap_or(
305                    ContractExecutionError::Message(format_error(&error_string, &String::from(vm))),
306                ),
307                ErrorStackSegment::StringFrame(msg) => {
308                    ContractExecutionError::Message(format_error("", msg.as_str()))
309                }
310                ErrorStackSegment::EntryPoint(entry_point_error_frame) => {
311                    let error = recursive_error_option.take().unwrap_or_else(|| {
312                        ContractExecutionError::Message(format_error(
313                            &error_string,
314                            preamble_type_to_error_msg(&entry_point_error_frame.preamble_type),
315                        ))
316                    });
317
318                    ContractExecutionError::Nested(InnerContractExecutionError {
319                        contract_address: entry_point_error_frame.storage_address,
320                        class_hash: entry_point_error_frame.class_hash.0,
321                        selector: entry_point_error_frame.selector.unwrap_or_default().0,
322                        return_data: Retdata(Vec::new()),
323                        error: Box::new(error),
324                    })
325                }
326            };
327
328            recursive_error_option = Some(stack_err);
329        }
330
331        if let Some(recursive_error) = recursive_error_option {
332            recursive_error
333        } else {
334            let error_msg = header_to_error_msg(&error_stack.header);
335            ContractExecutionError::Message(format_error(&error_string, error_msg))
336        }
337    }
338}
339
340impl From<&CallInfo> for ContractExecutionError {
341    fn from(call_info: &CallInfo) -> Self {
342        /// Traces recursively and returns elements starting from the deepest element
343        /// and then moves outward to the enclosing elements
344        fn collect_failed_calls(root_call: &CallInfo) -> Vec<&CallInfo> {
345            let mut calls = vec![];
346
347            for inner_call in root_call.inner_calls.iter() {
348                calls.extend(collect_failed_calls(inner_call));
349            }
350
351            if root_call.execution.failed {
352                calls.push(root_call);
353            }
354
355            calls
356        }
357
358        let failed_calls = collect_failed_calls(call_info);
359
360        // collects retdata of each CallInfo, starting from the outermost element of failed_calls
361        // collection and combines them in 1-dimensional array
362        // It serves as the reason for the failed call stack trace
363        let mut recursive_error = ContractExecutionError::Message(
364            serde_json::to_string(
365                &failed_calls
366                    .iter()
367                    .rev()
368                    .flat_map(|f| f.execution.retdata.clone().0)
369                    .collect::<Vec<Felt>>(),
370            )
371            .unwrap_or_default(),
372        );
373
374        for failed in failed_calls {
375            let current = ContractExecutionError::Nested(InnerContractExecutionError {
376                contract_address: failed.call.storage_address,
377                class_hash: failed.call.class_hash.unwrap_or_default().0,
378                selector: failed.call.entry_point_selector.0,
379                return_data: failed.execution.retdata.clone(),
380                error: Box::new(recursive_error),
381            });
382            recursive_error = current;
383        }
384
385        recursive_error
386    }
387}