Skip to main content

miden_tx/errors/
mod.rs

1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::vec::Vec;
4use core::error::Error;
5
6use miden_processor::ExecutionError;
7use miden_processor::serde::DeserializationError;
8use miden_protocol::account::auth::{PublicKeyCommitment, Signature};
9use miden_protocol::account::{AccountId, StorageMapKey, StorageSlotName};
10use miden_protocol::assembly::diagnostics::reporting::PrintDiagnostic;
11use miden_protocol::asset::AssetId;
12use miden_protocol::block::BlockNumber;
13use miden_protocol::crypto::merkle::smt::SmtProofError;
14use miden_protocol::errors::{
15    AccountDeltaError,
16    AssetError,
17    NoteError,
18    OutputNoteError,
19    ProvenTransactionError,
20    TransactionInputError,
21    TransactionOutputError,
22};
23use miden_protocol::note::{NoteId, PartialNoteMetadata};
24use miden_protocol::transaction::{TransactionEventId, TransactionSummary};
25use miden_protocol::{Felt, Word};
26use miden_prover::ProverError;
27use thiserror::Error;
28
29// NOTE EXECUTION ERROR
30// ================================================================================================
31
32#[derive(Debug, Error)]
33pub enum NoteCheckerError {
34    #[error("invalid input note count {0} is out of range)")]
35    InputNoteCountOutOfRange(usize),
36    #[error("transaction preparation failed: {0}")]
37    TransactionPreparation(#[source] TransactionExecutorError),
38    #[error("transaction execution prologue failed: {0}")]
39    PrologueExecution(#[source] TransactionExecutorError),
40}
41
42// TRANSACTION CHECKER ERROR
43// ================================================================================================
44
45#[derive(Debug, Error)]
46pub(crate) enum TransactionCheckerError {
47    #[error("transaction preparation failed: {0}")]
48    TransactionPreparation(#[source] TransactionExecutorError),
49    #[error("transaction execution prologue failed: {0}")]
50    PrologueExecution(#[source] TransactionExecutorError),
51    #[error("transaction execution epilogue failed: {error}")]
52    EpilogueExecution {
53        error: TransactionExecutorError,
54        /// Cycle counts for notes that executed successfully before the epilogue failed.
55        successful_notes_cycle_counts: Vec<usize>,
56    },
57    #[error("transaction note execution failed on note index {failed_note_index}: {error}")]
58    NoteExecution {
59        failed_note_index: usize,
60        error: TransactionExecutorError,
61        /// Cycle counts for notes that executed successfully before the failed note.
62        successful_notes_cycle_counts: Vec<usize>,
63        /// The number of cycles consumed by the failed note before it errored.
64        ///
65        /// This is `Some` when the failure was due to exceeding the cycle limit, and `None`
66        /// for other error types where the cycle count is not meaningful.
67        failed_note_cycle_count: Option<usize>,
68    },
69}
70
71impl From<TransactionCheckerError> for TransactionExecutorError {
72    fn from(error: TransactionCheckerError) -> Self {
73        match error {
74            TransactionCheckerError::TransactionPreparation(error) => error,
75            TransactionCheckerError::PrologueExecution(error) => error,
76            TransactionCheckerError::EpilogueExecution { error, .. } => error,
77            TransactionCheckerError::NoteExecution { error, .. } => error,
78        }
79    }
80}
81
82// TRANSACTION EXECUTOR ERROR
83// ================================================================================================
84
85#[derive(Debug, Error)]
86pub enum TransactionExecutorError {
87    #[error("failed to fetch transaction inputs from the data store")]
88    FetchTransactionInputsFailed(#[source] DataStoreError),
89    #[error("failed to fetch asset witnesses from the data store")]
90    FetchAssetWitnessFailed(#[source] DataStoreError),
91    #[error("foreign account inputs for ID {0} are not anchored on reference block")]
92    ForeignAccountNotAnchoredInReference(AccountId),
93    #[error(
94        "execution options' cycles must be between {min_cycles} and {max_cycles}, but found {actual}"
95    )]
96    InvalidExecutionOptionsCycles {
97        min_cycles: u32,
98        max_cycles: u32,
99        actual: u32,
100    },
101    #[error("failed to create transaction inputs")]
102    InvalidTransactionInputs(#[source] TransactionInputError),
103    // It is boxed to avoid triggering clippy::result_large_err for functions that return this
104    // type.
105    #[error("failed to create transaction host")]
106    TransactionHostCreationFailed(#[source] Box<TransactionKernelError>),
107    #[error("failed to process account update commitment: {0}")]
108    AccountUpdateCommitment(&'static str),
109    #[error(
110        "account patch commitment computed in transaction kernel ({in_kernel_commitment}) does not match account patch computed via the host ({host_commitment})"
111    )]
112    InconsistentAccountPatchCommitment {
113        in_kernel_commitment: Word,
114        host_commitment: Word,
115    },
116    #[error("input account ID {input_id} does not match output account ID {output_id}")]
117    InconsistentAccountId {
118        input_id: AccountId,
119        output_id: AccountId,
120    },
121    #[error("account witness provided for account ID {0} is invalid")]
122    InvalidAccountWitness(AccountId, #[source] SmtProofError),
123    #[error(
124        "input note {0} was created in a block past the transaction reference block number ({1})"
125    )]
126    NoteBlockPastReferenceBlock(NoteId, BlockNumber),
127    #[error("failed to construct transaction outputs")]
128    TransactionOutputConstructionFailed(#[source] TransactionOutputError),
129    // Print the diagnostic directly instead of returning the source error. In the source error
130    // case, the diagnostic is lost if the execution error is not explicitly unwrapped.
131    #[error("failed to execute transaction kernel program:\n{}", PrintDiagnostic::new(.0))]
132    TransactionProgramExecutionFailed(ExecutionError),
133    /// This variant can be matched on to get the summary of a transaction for signing purposes.
134    // It is boxed to avoid triggering clippy::result_large_err for functions that return this type.
135    #[error("transaction is unauthorized with summary {0:?}")]
136    Unauthorized(Box<TransactionSummary>),
137    #[error(
138        "failed to respond to signature requested since no authenticator is assigned to the host"
139    )]
140    MissingAuthenticator,
141    #[error("received an auth request event emitted outside the authentication procedure")]
142    AuthRequestOutsideAuthProcedure,
143    #[error("received privileged event {0} emitted outside the tx kernel context")]
144    PrivilegedEventFromOutsideTransactionKernelContext(TransactionEventId),
145}
146
147#[cfg(any(test, feature = "testing"))]
148impl TransactionExecutorError {
149    pub fn unwrap_unauthorized_err(self) -> Box<TransactionSummary> {
150        match self {
151            TransactionExecutorError::Unauthorized(transaction_summary) => transaction_summary,
152            other => panic!("expected TransactionExecutorError::Unauthorized, got {other}"),
153        }
154    }
155}
156
157// TRANSACTION PROVER ERROR
158// ================================================================================================
159
160#[derive(Debug, Error)]
161pub enum TransactionProverError {
162    #[error("failed to construct transaction outputs")]
163    TransactionOutputConstructionFailed(#[source] TransactionOutputError),
164    #[error("failed to shrink output note")]
165    OutputNoteShrinkFailed(#[source] OutputNoteError),
166    #[error("failed to build proven transaction")]
167    ProvenTransactionBuildFailed(#[source] ProvenTransactionError),
168    // It is boxed to avoid triggering clippy::result_large_err for functions that return this
169    // type.
170    #[error("failed to create transaction host")]
171    TransactionHostCreationFailed(#[source] Box<TransactionKernelError>),
172    // Print the diagnostic directly instead of returning the source error. In the source error
173    // case, the diagnostic is lost if the execution error is not explicitly unwrapped.
174    #[error("failed to execute transaction kernel program:\n{}", PrintDiagnostic::new(.0))]
175    TransactionProgramExecutionFailed(ExecutionError),
176    #[error("failed to generate transaction proof")]
177    TransactionProofGenerationFailed(#[source] ProverError),
178    /// Custom error variant for errors not covered by the other variants.
179    #[error("{error_msg}")]
180    Other {
181        error_msg: Box<str>,
182        // thiserror will return this when calling Error::source on DataStoreError.
183        source: Option<Box<dyn Error + Send + Sync + 'static>>,
184    },
185}
186
187impl TransactionProverError {
188    /// Creates a custom error using the [`TransactionProverError::Other`] variant from an error
189    /// message.
190    pub fn other(message: impl Into<String>) -> Self {
191        let message: String = message.into();
192        Self::Other { error_msg: message.into(), source: None }
193    }
194
195    /// Creates a custom error using the [`TransactionProverError::Other`] variant from an error
196    /// message and a source error.
197    pub fn other_with_source(
198        message: impl Into<String>,
199        source: impl Error + Send + Sync + 'static,
200    ) -> Self {
201        let message: String = message.into();
202        Self::Other {
203            error_msg: message.into(),
204            source: Some(Box::new(source)),
205        }
206    }
207}
208
209// TRANSACTION KERNEL ERROR
210// ================================================================================================
211
212#[derive(Debug, Error)]
213pub enum TransactionKernelError {
214    #[error("failed to add asset to account delta")]
215    AccountDeltaAddAssetFailed(#[source] AccountDeltaError),
216    #[error("failed to remove asset from account delta")]
217    AccountDeltaRemoveAssetFailed(#[source] AccountDeltaError),
218    #[error("failed to add asset to note")]
219    FailedToAddAssetToNote(#[source] NoteError),
220    #[error(
221        "transaction initialized an upgrade to account code {0} but the advice map did not provide the new code"
222    )]
223    AccountCodeUpgradeMissing(Word),
224    #[error(
225        "transaction initialized an upgrade to account code {new_code_commitment} but the advice map provides invalid code"
226    )]
227    AccountCodeUpgradeInvalid {
228        new_code_commitment: Word,
229        source: DeserializationError,
230    },
231    #[error(
232        "transaction initialized an upgrade to account code {expected} but the advice map provides code {actual}"
233    )]
234    AccountCodeUpgradeCommitmentMismatch { expected: Word, actual: Word },
235    #[error("account code upgrade is not allowed for new accounts")]
236    AccountCodeUpgradeNotAllowedForNewAccount,
237    #[error("note storage has commitment {actual} but expected commitment {expected}")]
238    InvalidNoteStorage { expected: Word, actual: Word },
239    #[error(
240        "failed to respond to signature requested since no authenticator is assigned to the host"
241    )]
242    MissingAuthenticator,
243    #[error("received an auth request event emitted outside the authentication procedure")]
244    AuthRequestOutsideAuthProcedure,
245    #[error("received privileged event {0} emitted outside the tx kernel context")]
246    PrivilegedEventFromOutsideTransactionKernelContext(TransactionEventId),
247    #[error("failed to generate signature")]
248    SignatureGenerationFailed(#[source] AuthenticationError),
249    #[error("transaction returned unauthorized event but a commitment did not match: {0}")]
250    TransactionSummaryCommitmentMismatch(#[source] Box<dyn Error + Send + Sync + 'static>),
251    #[error(
252        "transaction summary binds expiration delta {actual} but the transaction's expiration delta is {expected}"
253    )]
254    TransactionSummaryExpirationDeltaMismatch { expected: u16, actual: u16 },
255    #[error("transaction summary binds block {0}, which the transaction does not authenticate")]
256    TransactionSummaryUnknownBlockNumber(BlockNumber),
257    #[error("failed to construct transaction summary")]
258    TransactionSummaryConstructionFailed(#[source] Box<dyn Error + Send + Sync + 'static>),
259    #[error("asset data extracted from the stack by event handler `{handler}` is not well formed")]
260    MalformedAssetInEventHandler {
261        handler: &'static str,
262        source: AssetError,
263    },
264    #[error(
265        "note storage data extracted from the advice map by the event handler is not well formed"
266    )]
267    MalformedNoteStorage(#[source] NoteError),
268    #[error(
269        "note script elements `{script_elements:?}` extracted from the advice map by the event handler are not well formed"
270    )]
271    MalformedNoteScript {
272        script_elements: Vec<Felt>,
273        source: DeserializationError,
274    },
275    #[error(
276        "encoded signature under advice map key {signature_key} has {actual} elements, but a valid encoded signature has between 1 and {max} elements",
277        max = Signature::MAX_NUM_ENCODED_SIGNATURE_FELTS
278    )]
279    InvalidEncodedSignatureLength { signature_key: Word, actual: usize },
280    #[error("recipient data `{0:?}` in the advice provider is not well formed")]
281    MalformedRecipientData(Vec<Felt>),
282    #[error("cannot add asset to note with index {0}, note does not exist in the advice provider")]
283    MissingNote(usize),
284    #[error(
285        "public note with metadata {0:?} and recipient digest {1} is missing details in the advice provider"
286    )]
287    PublicNoteMissingDetails(PartialNoteMetadata, Word),
288    #[error(
289        "commitment of note attachment advice data is {actual} which does not match commitment {provided} provided to add_attachment"
290    )]
291    NoteAttachmentCommitmentMismatch { actual: Word, provided: Word },
292    #[error(
293        "note storage in advice provider contains fewer items ({actual}) than specified ({specified}) by its number of storage items"
294    )]
295    TooFewElementsForNoteStorage { specified: u64, actual: u64 },
296    #[error("account procedure with procedure root {0} is not in the account procedure index map")]
297    UnknownAccountProcedure(Word),
298    #[error("code commitment {0} is not in the account procedure index map")]
299    UnknownCodeCommitment(Word),
300    #[error("account storage slots number is missing in memory at address {0}")]
301    AccountStorageSlotsNumMissing(u32),
302    #[error("account nonce can only be incremented once")]
303    NonceCanOnlyIncrementOnce,
304    #[error("partial storage of a new account is missing the storage map of slot {0}")]
305    NewAccountMissingStorageMap(StorageSlotName),
306    #[error(
307        "failed to get inputs for foreign account {foreign_account_id} from data store at reference block {ref_block}"
308    )]
309    GetForeignAccountInputs {
310        foreign_account_id: AccountId,
311        ref_block: BlockNumber,
312        // thiserror will return this when calling Error::source on TransactionKernelError.
313        source: DataStoreError,
314    },
315    #[error(
316        "failed to get vault asset witness from data store for vault root {vault_root} and asset_id {asset_id}"
317    )]
318    GetVaultAssetWitness {
319        vault_root: Word,
320        asset_id: AssetId,
321        // thiserror will return this when calling Error::source on TransactionKernelError.
322        source: DataStoreError,
323    },
324    #[error(
325        "failed to get storage map witness from data store for map root {map_root} and map_key {map_key}"
326    )]
327    GetStorageMapWitness {
328        map_root: Word,
329        map_key: StorageMapKey,
330        // thiserror will return this when calling Error::source on TransactionKernelError.
331        source: DataStoreError,
332    },
333    /// This variant signals that a signature over the contained commitments is required, but
334    /// missing.
335    #[error("transaction requires a signature")]
336    Unauthorized(Box<TransactionSummary>),
337    /// A generic error returned when the transaction kernel did not behave as expected.
338    #[error("{message}")]
339    Other {
340        message: Box<str>,
341        // thiserror will return this when calling Error::source on TransactionKernelError.
342        source: Option<Box<dyn Error + Send + Sync + 'static>>,
343    },
344}
345
346impl TransactionKernelError {
347    /// Creates a custom error using the [`TransactionKernelError::Other`] variant from an error
348    /// message.
349    pub fn other(message: impl Into<String>) -> Self {
350        let message: String = message.into();
351        Self::Other { message: message.into(), source: None }
352    }
353
354    /// Creates a custom error using the [`TransactionKernelError::Other`] variant from an error
355    /// message and a source error.
356    pub fn other_with_source(
357        message: impl Into<String>,
358        source: impl Error + Send + Sync + 'static,
359    ) -> Self {
360        let message: String = message.into();
361        Self::Other {
362            message: message.into(),
363            source: Some(Box::new(source)),
364        }
365    }
366}
367
368// DATA STORE ERROR
369// ================================================================================================
370
371#[derive(Debug, Error)]
372pub enum DataStoreError {
373    #[error("account with id {0} not found in data store")]
374    AccountNotFound(AccountId),
375    #[error("block with number {0} not found in data store")]
376    BlockNotFound(BlockNumber),
377    /// Custom error variant for implementors of the [`DataStore`](crate::executor::DataStore)
378    /// trait.
379    #[error("{error_msg}")]
380    Other {
381        error_msg: Box<str>,
382        // thiserror will return this when calling Error::source on DataStoreError.
383        source: Option<Box<dyn Error + Send + Sync + 'static>>,
384    },
385}
386
387impl DataStoreError {
388    /// Creates a custom error using the [`DataStoreError::Other`] variant from an error message.
389    pub fn other(message: impl Into<String>) -> Self {
390        let message: String = message.into();
391        Self::Other { error_msg: message.into(), source: None }
392    }
393
394    /// Creates a custom error using the [`DataStoreError::Other`] variant from an error message and
395    /// a source error.
396    pub fn other_with_source(
397        message: impl Into<String>,
398        source: impl Error + Send + Sync + 'static,
399    ) -> Self {
400        let message: String = message.into();
401        Self::Other {
402            error_msg: message.into(),
403            source: Some(Box::new(source)),
404        }
405    }
406}
407
408// AUTHENTICATION ERROR
409// ================================================================================================
410
411#[derive(Debug, Error)]
412pub enum AuthenticationError {
413    #[error("signature rejected: {0}")]
414    RejectedSignature(String),
415    #[error("public key `{0}` is not contained in the authenticator's keys")]
416    UnknownPublicKey(PublicKeyCommitment),
417    /// Custom error variant for implementors of the
418    /// [`TransactionAuthenticator`](crate::auth::TransactionAuthenticator) trait.
419    #[error("{error_msg}")]
420    Other {
421        error_msg: Box<str>,
422        // thiserror will return this when calling Error::source on DataStoreError.
423        source: Option<Box<dyn Error + Send + Sync + 'static>>,
424    },
425}
426
427impl AuthenticationError {
428    /// Creates a custom error using the [`AuthenticationError::Other`] variant from an error
429    /// message.
430    pub fn other(message: impl Into<String>) -> Self {
431        let message: String = message.into();
432        Self::Other { error_msg: message.into(), source: None }
433    }
434
435    /// Creates a custom error using the [`AuthenticationError::Other`] variant from an error
436    /// message and a source error.
437    pub fn other_with_source(
438        message: impl Into<String>,
439        source: impl Error + Send + Sync + 'static,
440    ) -> Self {
441        let message: String = message.into();
442        Self::Other {
443            error_msg: message.into(),
444            source: Some(Box::new(source)),
445        }
446    }
447}
448
449#[cfg(test)]
450mod error_assertions {
451    use super::*;
452
453    /// Asserts at compile time that the passed error has Send + Sync + 'static bounds.
454    fn _assert_error_is_send_sync_static<E: core::error::Error + Send + Sync + 'static>(_: E) {}
455
456    fn _assert_data_store_error_bounds(err: DataStoreError) {
457        _assert_error_is_send_sync_static(err);
458    }
459
460    fn _assert_authentication_error_bounds(err: AuthenticationError) {
461        _assert_error_is_send_sync_static(err);
462    }
463
464    fn _assert_transaction_kernel_error_bounds(err: TransactionKernelError) {
465        _assert_error_is_send_sync_static(err);
466    }
467}