Skip to main content

miden_node_store/
errors.rs

1use std::io;
2
3use miden_node_proto::domain::block::InvalidBlockRange;
4use miden_node_proto::errors::ConversionError;
5use miden_node_utils::limiter::QueryLimitError;
6use miden_protocol::Word;
7use miden_protocol::account::AccountId;
8use miden_protocol::block::BlockNumber;
9use miden_protocol::crypto::merkle::MerkleError;
10use miden_protocol::crypto::merkle::mmr::MmrError;
11use miden_protocol::crypto::merkle::smt::{LargeSmtForestError, LineageId};
12use miden_protocol::crypto::utils::DeserializationError;
13use miden_protocol::errors::{
14    AccountDeltaError,
15    AccountError,
16    AccountTreeError,
17    AssetError,
18    AssetVaultError,
19    NoteError,
20    NullifierTreeError,
21    StorageMapError,
22};
23use miden_protocol::note::Nullifier;
24use miden_protocol::transaction::OutputNote;
25use thiserror::Error;
26use tokio::sync::oneshot::error::RecvError;
27
28use crate::db::models::conv::DatabaseTypeConversionError;
29
30/// Errors produced while preparing or rebuilding account-state forest updates.
31///
32/// The underlying [`LargeSmtForestError`] is preserved so callers can distinguish fatal backend
33/// failures from invalid update preparation.
34#[derive(Debug, Error)]
35pub enum AccountStateForestUpdateError {
36    /// A full-state patch attempted to create a vault lineage that is already present.
37    #[error("account {account_id} vault lineage already exists")]
38    VaultLineageAlreadyExists { account_id: AccountId },
39    /// A full-state patch attempted to create a storage-map lineage that is already present.
40    #[error("account {account_id} storage map lineage for slot {slot_name} already exists")]
41    StorageLineageAlreadyExists {
42        account_id: AccountId,
43        slot_name: miden_protocol::account::StorageSlotName,
44    },
45    /// The forest mutation set did not contain a root for a lineage it was expected to update.
46    #[error("computed forest mutations are missing lineage {lineage}")]
47    MissingComputedRoot { lineage: LineageId },
48    /// The forest backend failed while computing, reading, or applying mutations.
49    #[error(transparent)]
50    Forest(#[from] LargeSmtForestError),
51}
52
53// DATABASE ERRORS
54// =================================================================================================
55
56#[derive(Debug, Error)]
57pub enum DatabaseError {
58    // ERRORS WITH AUTOMATIC CONVERSIONS FROM NESTED ERROR TYPES
59    // ---------------------------------------------------------------------------------------------
60    #[error("account error")]
61    AccountError(#[from] AccountError),
62    #[error("asset vault error")]
63    AssetVaultError(#[from] AssetVaultError),
64    #[error("asset error")]
65    AssetError(#[from] AssetError),
66    #[error("closed channel")]
67    ClosedChannel(#[from] RecvError),
68    #[error("database error")]
69    DatabaseError(#[from] miden_node_db::DatabaseError),
70    #[error("deserialization failed")]
71    DeserializationError(#[from] DeserializationError),
72    #[error("I/O error")]
73    IoError(#[from] io::Error),
74    #[error("merkle error")]
75    MerkleError(#[from] MerkleError),
76    #[error("note error")]
77    NoteError(#[from] NoteError),
78    #[error("storage map error")]
79    StorageMapError(#[from] StorageMapError),
80    #[error(transparent)]
81    Diesel(#[from] diesel::result::Error),
82    #[error(transparent)]
83    QueryParamLimit(#[from] QueryLimitError),
84    #[error(transparent)]
85    RangeBeyondTip(#[from] RangeBeyondTip),
86
87    // OTHER ERRORS
88    // ---------------------------------------------------------------------------------------------
89    #[error("account commitment mismatch (expected {expected}, but calculated is {calculated})")]
90    AccountCommitmentsMismatch { expected: Word, calculated: Word },
91    #[error("account {0} not found")]
92    AccountNotFoundInDb(AccountId),
93    #[error("accounts {0:?} not found")]
94    AccountsNotFoundInDb(Vec<AccountId>),
95    #[error("account {0} is not on the chain")]
96    AccountNotPublic(AccountId),
97    #[error("invalid block parameters: block_from ({from}) > block_to ({to})")]
98    InvalidBlockRange { from: BlockNumber, to: BlockNumber },
99    #[error(
100        "transactions for block {block_num} would exceed maximum response size, \
101         use a stricter filter to reduce the number of transactions returned"
102    )]
103    TransactionPageExceedsPayloadLimit { block_num: BlockNumber },
104    #[error("data corrupted: {0}")]
105    DataCorrupted(String),
106    #[error(transparent)]
107    SqlValueConversion(#[from] DatabaseTypeConversionError),
108    #[error("storage root not found for account {account_id}, slot {slot_name}, block {block_num}")]
109    StorageRootNotFound {
110        account_id: AccountId,
111        slot_name: String,
112        block_num: BlockNumber,
113    },
114}
115
116// INITIALIZATION ERRORS
117// =================================================================================================
118
119#[derive(Error, Debug)]
120pub enum StateInitializationError {
121    #[error("account tree IO error: {0}")]
122    AccountTreeIoError(String),
123    #[error("nullifier tree IO error: {0}")]
124    NullifierTreeIoError(String),
125    #[error("account state forest IO error: {0}")]
126    AccountStateForestIoError(String),
127    #[error("failed to rebuild account state forest")]
128    AccountStateForestRebuild(#[source] AccountStateForestUpdateError),
129    #[error("database error")]
130    DatabaseError(#[from] DatabaseError),
131    #[error("failed to create nullifier tree")]
132    FailedToCreateNullifierTree(#[from] NullifierTreeError),
133    #[error("failed to create accounts tree")]
134    FailedToCreateAccountsTree(#[source] AccountTreeError),
135    #[error("failed to load data directory")]
136    DataDirectoryLoadError(#[source] std::io::Error),
137    #[error("failed to load block store")]
138    BlockStoreLoadError(#[source] std::io::Error),
139    #[error("failed to load proven tip")]
140    ProvenTipLoadError(#[source] std::io::Error),
141    #[error("failed to load database")]
142    DatabaseLoadError(#[source] DatabaseError),
143    #[error(
144        "{tree_name} SMT root ({tree_root:?}) does not match expected root from block {block_num} \
145         ({block_root:?}). Delete the tree storage directories and restart the node to rebuild \
146         from the database."
147    )]
148    TreeStorageDiverged {
149        tree_name: &'static str,
150        block_num: BlockNumber,
151        tree_root: Word,
152        block_root: Word,
153    },
154    #[error("loaded chain MMR cannot produce peaks for block {block_num}")]
155    ChainMmrLoadError {
156        block_num: BlockNumber,
157        #[source]
158        source: MmrError,
159    },
160    #[error(
161        "chain MMR commitment ({chain_mmr_commitment:?}) does not match expected chain commitment \
162         from block {block_num} ({block_header_commitment:?})"
163    )]
164    ChainMmrStorageDiverged {
165        block_num: BlockNumber,
166        chain_mmr_commitment: Word,
167        block_header_commitment: Word,
168    },
169    #[error(
170        "account state forest root ({forest_root}) does not match SQLite root \
171         ({database_root}) for account {account_id}, slot {slot_name:?}. Delete the account \
172         state forest storage directory and restart the node to rebuild from the database."
173    )]
174    AccountStateForestStorageDiverged {
175        account_id: AccountId,
176        slot_name: Option<String>,
177        forest_root: Word,
178        database_root: Word,
179    },
180    #[error("public account {0} is missing details in database")]
181    PublicAccountMissingDetails(AccountId),
182    #[error("failed to convert account to delta: {0}")]
183    AccountToDeltaConversionFailed(String),
184    #[error("genesis block missing. The database should be bootstrapped first.")]
185    GenesisBlockMissing,
186}
187
188// ENDPOINT ERRORS
189// =================================================================================================
190#[derive(Error, Debug)]
191pub enum InvalidBlockError {
192    #[error("duplicated nullifiers {0:?}")]
193    DuplicatedNullifiers(Vec<Nullifier>),
194    #[error("invalid output note type: {0:?}")]
195    InvalidOutputNoteType(Box<OutputNote>),
196    #[error("invalid block tx commitment: expected {expected}, but got {actual}")]
197    InvalidBlockTxCommitment { expected: Word, actual: Word },
198    #[error("received invalid account tree root")]
199    NewBlockInvalidAccountRoot,
200    #[error("new block number must be 1 greater than the current block number")]
201    NewBlockInvalidBlockNum {
202        expected: BlockNumber,
203        submitted: BlockNumber,
204    },
205    #[error("new block chain commitment is not consistent with chain MMR")]
206    NewBlockInvalidChainCommitment,
207    #[error("received invalid note root")]
208    NewBlockInvalidNoteRoot,
209    #[error("received invalid nullifier root")]
210    NewBlockInvalidNullifierRoot,
211    #[error("new block `prev_block_commitment` must match the chain's tip")]
212    NewBlockInvalidPrevCommitment,
213    #[error("nullifier in new block is already spent")]
214    NewBlockNullifierAlreadySpent(#[source] NullifierTreeError),
215    #[error("duplicate account ID prefix in new block")]
216    NewBlockDuplicateAccountIdPrefix(#[source] AccountTreeError),
217    #[error("failed to build note tree: {0}")]
218    FailedToBuildNoteTree(String),
219}
220
221#[derive(Error, Debug)]
222pub enum ApplyBlockError {
223    // ERRORS WITH AUTOMATIC CONVERSIONS FROM NESTED ERROR TYPES
224    // ---------------------------------------------------------------------------------------------
225    #[error("database error")]
226    DatabaseError(#[from] DatabaseError),
227    #[error("I/O error")]
228    IoError(#[from] io::Error),
229    #[error("task join error")]
230    TokioJoinError(#[from] tokio::task::JoinError),
231    #[error("invalid block error")]
232    InvalidBlockError(#[from] InvalidBlockError),
233
234    // OTHER ERRORS
235    // ---------------------------------------------------------------------------------------------
236    #[error("block applying was cancelled because the writer task dropped the result channel")]
237    ClosedChannel(#[from] RecvError),
238    #[error("account state forest update preparation failed")]
239    AccountStateForestPreparation(#[source] AccountStateForestUpdateError),
240    #[error("database doesn't have any block header data")]
241    DbBlockHeaderEmpty,
242    #[error("database update failed: {0}")]
243    DbUpdateTaskFailed(String),
244    #[error("failed to send block to the writer task: {0}")]
245    WriterTaskSendFailed(String),
246}
247
248#[derive(Error, Debug)]
249pub enum ApplyBlockWithProvingInputsError {
250    #[error("failed to save block proving inputs")]
251    SaveProvingInputs(#[source] io::Error),
252    #[error("failed to apply block")]
253    ApplyBlock(#[source] ApplyBlockError),
254}
255
256/// A requested block range extends beyond the chain tip of the state view serving the request.
257#[derive(Error, Debug)]
258#[error("block_to ({block_to}) is greater than chain tip ({chain_tip})")]
259pub struct RangeBeyondTip {
260    pub chain_tip: BlockNumber,
261    pub block_to: BlockNumber,
262}
263
264#[derive(Error, Debug)]
265pub enum GetBlockHeaderError {
266    #[error("database error")]
267    DatabaseError(#[from] DatabaseError),
268    #[error("error retrieving the merkle proof for the block")]
269    MmrError(#[from] MmrError),
270}
271
272#[derive(Error, Debug)]
273pub enum StateSyncError {
274    #[error("database error")]
275    DatabaseError(#[from] DatabaseError),
276    #[error("block headers table is empty")]
277    EmptyBlockHeadersTable,
278    #[error("failed to build MMR delta")]
279    FailedToBuildMmrDelta(#[from] MmrError),
280    #[error(transparent)]
281    RangeBeyondTip(#[from] RangeBeyondTip),
282}
283
284impl From<diesel::result::Error> for StateSyncError {
285    fn from(value: diesel::result::Error) -> Self {
286        Self::DatabaseError(DatabaseError::from(value))
287    }
288}
289
290#[derive(Error, Debug)]
291pub enum NoteSyncError {
292    #[error("database error")]
293    DatabaseError(#[from] DatabaseError),
294    #[error("database error")]
295    UnderlyingDatabaseError(#[from] miden_node_db::DatabaseError),
296    #[error("block headers table is empty")]
297    EmptyBlockHeadersTable,
298    #[error("error retrieving the merkle proof for the block")]
299    MmrError(#[from] MmrError),
300    #[error("invalid block range")]
301    InvalidBlockRange(#[from] InvalidBlockRange),
302    #[error(transparent)]
303    RangeBeyondTip(#[from] RangeBeyondTip),
304    #[error("malformed note tags")]
305    DeserializationFailed(#[from] ConversionError),
306}
307
308impl From<diesel::result::Error> for NoteSyncError {
309    fn from(value: diesel::result::Error) -> Self {
310        Self::DatabaseError(DatabaseError::from(value))
311    }
312}
313
314#[derive(Error, Debug)]
315pub enum GetNoteInclusionProofsError {
316    #[error("failed to select note inclusion proofs")]
317    SelectNoteInclusionProofError(#[source] DatabaseError),
318    #[error("reference block {reference_block} is newer than the latest block {latest_block_num}")]
319    ReferenceBlockAfterTip {
320        reference_block: BlockNumber,
321        latest_block_num: BlockNumber,
322    },
323}
324
325#[derive(Error, Debug)]
326pub enum GetBlockInclusionProofsError {
327    #[error("failed to select block headers")]
328    SelectBlockHeaderError(#[source] DatabaseError),
329    #[error("reference block {reference_block} is newer than the latest block {latest_block_num}")]
330    ReferenceBlockAfterTip {
331        reference_block: BlockNumber,
332        latest_block_num: BlockNumber,
333    },
334    #[error("block {block_num} is newer than the reference block {reference_block}")]
335    BlockAfterReferenceBlock {
336        block_num: BlockNumber,
337        reference_block: BlockNumber,
338    },
339}
340
341// GET ACCOUNT ERRORS
342// ================================================================================================
343
344#[derive(Debug, Error)]
345pub enum GetAccountError {
346    #[error("database error")]
347    DatabaseError(#[from] DatabaseError),
348    #[error("malformed request")]
349    DeserializationFailed(#[from] ConversionError),
350    #[error("account {0} not found at block {1}")]
351    AccountNotFound(AccountId, BlockNumber),
352    #[error("account {0} is not public")]
353    AccountNotPublic(AccountId),
354    #[error("block {0} is unknown")]
355    UnknownBlock(BlockNumber),
356    #[error("block {0} has been pruned")]
357    BlockPruned(BlockNumber),
358}
359
360// Do not scope for `cfg(test)` - if it the traitbounds don't suffice the issue will already appear
361// in the compilation of the library or binary, which would prevent getting to compiling the
362// following code.
363mod compile_tests {
364    use std::marker::PhantomData;
365
366    use super::{
367        AccountDeltaError,
368        AccountError,
369        DatabaseError,
370        DeserializationError,
371        NoteError,
372        RecvError,
373        StateInitializationError,
374    };
375
376    /// Ensure all enum variants remain compat with the desired trait bounds. Otherwise one gets
377    /// very unwieldy errors.
378    #[expect(dead_code)]
379    fn assumed_trait_bounds_upheld() {
380        fn ensure_is_error<E>(_phony: PhantomData<E>)
381        where
382            E: std::error::Error + Send + Sync + 'static,
383        {
384        }
385
386        ensure_is_error::<AccountError>(PhantomData);
387        ensure_is_error::<AccountDeltaError>(PhantomData);
388        ensure_is_error::<RecvError>(PhantomData);
389        ensure_is_error::<DeserializationError>(PhantomData);
390        ensure_is_error::<NoteError>(PhantomData);
391        ensure_is_error::<hex::FromHexError>(PhantomData);
392        ensure_is_error::<deadpool::managed::PoolError<deadpool_diesel::Error>>(PhantomData);
393        ensure_is_error::<diesel::result::Error>(PhantomData);
394        ensure_is_error::<deadpool_diesel::Error>(PhantomData);
395        ensure_is_error::<deadpool::managed::RecycleError<deadpool_diesel::Error>>(PhantomData);
396
397        ensure_is_error::<DatabaseError>(PhantomData);
398        ensure_is_error::<diesel::result::Error>(PhantomData);
399        ensure_is_error::<StateInitializationError>(PhantomData);
400        ensure_is_error::<deadpool::managed::PoolError<deadpool_diesel::Error>>(PhantomData);
401    }
402}