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