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