use std::io;
use miden_node_proto::domain::account::NetworkAccountError;
use miden_node_proto::domain::block::InvalidBlockRange;
use miden_node_proto::errors::{ConversionError, GrpcError};
use miden_node_utils::ErrorReport;
use miden_node_utils::limiter::QueryLimitError;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::crypto::merkle::mmr::MmrError;
use miden_protocol::crypto::utils::DeserializationError;
use miden_protocol::errors::{
AccountDeltaError,
AccountError,
AccountTreeError,
AssetError,
AssetVaultError,
NoteError,
NullifierTreeError,
StorageMapError,
};
use miden_protocol::note::{NoteId, Nullifier};
use miden_protocol::transaction::OutputNote;
use thiserror::Error;
use tokio::sync::oneshot::error::RecvError;
use tonic::Status;
use crate::account_state_forest::{AccountStateForestError, WitnessError};
use crate::db::models::conv::DatabaseTypeConversionError;
#[derive(Debug, Error)]
pub enum ProofSchedulerError {
#[error("no proving inputs found for block {0}")]
MissingProvingInputs(BlockNumber),
#[error("failed to deserialize proving inputs for block")]
DeserializationFailed(#[source] DeserializationError),
#[error("invalid remote prover endpoint: {0}")]
InvalidProverEndpoint(String),
}
#[derive(Debug, Error)]
pub enum DatabaseError {
#[error("account error")]
AccountError(#[from] AccountError),
#[error("asset vault error")]
AssetVaultError(#[from] AssetVaultError),
#[error("asset error")]
AssetError(#[from] AssetError),
#[error("closed channel")]
ClosedChannel(#[from] RecvError),
#[error("database error")]
DatabaseError(#[from] miden_node_db::DatabaseError),
#[error("deserialization failed")]
DeserializationError(#[from] DeserializationError),
#[error("I/O error")]
IoError(#[from] io::Error),
#[error("merkle error")]
MerkleError(#[from] MerkleError),
#[error("note error")]
NoteError(#[from] NoteError),
#[error("storage map error")]
StorageMapError(#[from] StorageMapError),
#[error(transparent)]
Diesel(#[from] diesel::result::Error),
#[error(transparent)]
QueryParamLimit(#[from] QueryLimitError),
#[error("account commitment mismatch (expected {expected}, but calculated is {calculated})")]
AccountCommitmentsMismatch { expected: Word, calculated: Word },
#[error("account {0} not found")]
AccountNotFoundInDb(AccountId),
#[error("accounts {0:?} not found")]
AccountsNotFoundInDb(Vec<AccountId>),
#[error("account {0} is not on the chain")]
AccountNotPublic(AccountId),
#[error("invalid block parameters: block_from ({from}) > block_to ({to})")]
InvalidBlockRange { from: BlockNumber, to: BlockNumber },
#[error("data corrupted: {0}")]
DataCorrupted(String),
#[error(transparent)]
SqlValueConversion(#[from] DatabaseTypeConversionError),
#[error("storage root not found for account {account_id}, slot {slot_name}, block {block_num}")]
StorageRootNotFound {
account_id: AccountId,
slot_name: String,
block_num: BlockNumber,
},
}
impl From<DatabaseError> for Status {
fn from(err: DatabaseError) -> Self {
match err {
DatabaseError::AccountNotFoundInDb(_)
| DatabaseError::AccountsNotFoundInDb(_)
| DatabaseError::AccountNotPublic(_) => Status::not_found(err.to_string()),
_ => Status::internal(err.to_string()),
}
}
}
#[derive(Error, Debug)]
pub enum StateInitializationError {
#[error("account tree IO error: {0}")]
AccountTreeIoError(String),
#[error("nullifier tree IO error: {0}")]
NullifierTreeIoError(String),
#[error("database error")]
DatabaseError(#[from] DatabaseError),
#[error("failed to create nullifier tree")]
FailedToCreateNullifierTree(#[from] NullifierTreeError),
#[error("failed to create accounts tree")]
FailedToCreateAccountsTree(#[source] AccountTreeError),
#[error("failed to load data directory")]
DataDirectoryLoadError(#[source] std::io::Error),
#[error("failed to load block store")]
BlockStoreLoadError(#[source] std::io::Error),
#[error("failed to load database")]
DatabaseLoadError(#[source] DatabaseError),
#[error("account state forest error")]
AccountStateForestError(#[from] AccountStateForestError),
#[error(
"{tree_name} SMT root ({tree_root:?}) does not match expected root from block {block_num} \
({block_root:?}). Delete the tree storage directories and restart the node to rebuild \
from the database."
)]
TreeStorageDiverged {
tree_name: &'static str,
block_num: BlockNumber,
tree_root: Word,
block_root: Word,
},
#[error("public account {0} is missing details in database")]
PublicAccountMissingDetails(AccountId),
#[error("failed to convert account to delta: {0}")]
AccountToDeltaConversionFailed(String),
}
#[derive(Error, Debug)]
pub enum InvalidBlockError {
#[error("duplicated nullifiers {0:?}")]
DuplicatedNullifiers(Vec<Nullifier>),
#[error("invalid output note type: {0:?}")]
InvalidOutputNoteType(Box<OutputNote>),
#[error("invalid block tx commitment: expected {expected}, but got {actual}")]
InvalidBlockTxCommitment { expected: Word, actual: Word },
#[error("received invalid account tree root")]
NewBlockInvalidAccountRoot,
#[error("new block number must be 1 greater than the current block number")]
NewBlockInvalidBlockNum {
expected: BlockNumber,
submitted: BlockNumber,
},
#[error("new block chain commitment is not consistent with chain MMR")]
NewBlockInvalidChainCommitment,
#[error("received invalid note root")]
NewBlockInvalidNoteRoot,
#[error("received invalid nullifier root")]
NewBlockInvalidNullifierRoot,
#[error("new block `prev_block_commitment` must match the chain's tip")]
NewBlockInvalidPrevCommitment,
#[error("nullifier in new block is already spent")]
NewBlockNullifierAlreadySpent(#[source] NullifierTreeError),
#[error("duplicate account ID prefix in new block")]
NewBlockDuplicateAccountIdPrefix(#[source] AccountTreeError),
#[error("failed to build note tree: {0}")]
FailedToBuildNoteTree(String),
}
#[derive(Error, Debug)]
pub enum ApplyBlockError {
#[error("database error")]
DatabaseError(#[from] DatabaseError),
#[error("I/O error")]
IoError(#[from] io::Error),
#[error("task join error")]
TokioJoinError(#[from] tokio::task::JoinError),
#[error("invalid block error")]
InvalidBlockError(#[from] InvalidBlockError),
#[error("account state forest error")]
AccountStateForestError(#[from] AccountStateForestError),
#[error("block applying was cancelled because of closed channel on database side")]
ClosedChannel(#[from] RecvError),
#[error("concurrent write detected")]
ConcurrentWrite,
#[error("database doesn't have any block header data")]
DbBlockHeaderEmpty,
#[error("database update failed: {0}")]
DbUpdateTaskFailed(String),
}
impl From<ApplyBlockError> for Status {
fn from(err: ApplyBlockError) -> Self {
match err {
ApplyBlockError::InvalidBlockError(_) => Status::invalid_argument(err.as_report()),
_ => Status::internal(err.as_report()),
}
}
}
#[derive(Error, Debug, GrpcError)]
pub enum GetBlockHeaderError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("error retrieving the merkle proof for the block")]
#[grpc(internal)]
MmrError(#[from] MmrError),
}
#[derive(Error, Debug)]
pub enum GetBlockInputsError {
#[error("failed to select note inclusion proofs")]
SelectNoteInclusionProofError(#[source] DatabaseError),
#[error("failed to select block headers")]
SelectBlockHeaderError(#[source] DatabaseError),
#[error(
"highest block number {highest_block_number} referenced by a batch is newer than the latest block {latest_block_number}"
)]
UnknownBatchBlockReference {
highest_block_number: BlockNumber,
latest_block_number: BlockNumber,
},
}
#[derive(Error, Debug)]
pub enum StateSyncError {
#[error("database error")]
DatabaseError(#[from] DatabaseError),
#[error("block headers table is empty")]
EmptyBlockHeadersTable,
#[error("failed to build MMR delta")]
FailedToBuildMmrDelta(#[from] MmrError),
}
#[derive(Error, Debug, GrpcError)]
pub enum SyncChainMmrError {
#[error("invalid block range")]
InvalidBlockRange(#[source] InvalidBlockRange),
#[error("start block is not known")]
FutureBlock {
chain_tip: BlockNumber,
block_from: BlockNumber,
},
#[error("malformed block number")]
DeserializationFailed(#[source] ConversionError),
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[source] DatabaseError),
}
impl From<diesel::result::Error> for StateSyncError {
fn from(value: diesel::result::Error) -> Self {
Self::DatabaseError(DatabaseError::from(value))
}
}
#[derive(Error, Debug, GrpcError)]
pub enum NoteSyncError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("database error")]
#[grpc(internal)]
UnderlyingDatabaseError(#[from] miden_node_db::DatabaseError),
#[error("block headers table is empty")]
#[grpc(internal)]
EmptyBlockHeadersTable,
#[error("error retrieving the merkle proof for the block")]
#[grpc(internal)]
MmrError(#[from] MmrError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("block_to ({block_to}) is greater than chain tip ({chain_tip})")]
FutureBlock {
chain_tip: BlockNumber,
block_to: BlockNumber,
},
#[error("malformed note tags")]
DeserializationFailed(#[from] ConversionError),
}
impl From<diesel::result::Error> for NoteSyncError {
fn from(value: diesel::result::Error) -> Self {
Self::DatabaseError(DatabaseError::from(value))
}
}
#[derive(Error, Debug)]
pub enum GetCurrentBlockchainDataError {
#[error("failed to retrieve block header")]
ErrorRetrievingBlockHeader(#[source] DatabaseError),
#[error("failed to instantiate MMR peaks")]
InvalidPeaks(MmrError),
}
#[derive(Error, Debug)]
pub enum GetBatchInputsError {
#[error("failed to select note inclusion proofs")]
SelectNoteInclusionProofError(#[source] DatabaseError),
#[error("failed to select block headers")]
SelectBlockHeaderError(#[source] DatabaseError),
#[error("set of blocks referenced by transactions is empty")]
TransactionBlockReferencesEmpty,
#[error(
"highest block number {highest_block_num} referenced by a transaction is newer than the latest block {latest_block_num}"
)]
UnknownTransactionBlockReference {
highest_block_num: BlockNumber,
latest_block_num: BlockNumber,
},
}
#[derive(Debug, Error, GrpcError)]
pub enum SyncNullifiersError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("unsupported prefix length: {0} (only 16-bit prefixes are supported)")]
InvalidPrefixLength(u32),
#[error("malformed nullifier prefix")]
DeserializationFailed(#[from] ConversionError),
}
#[derive(Debug, Error, GrpcError)]
pub enum SyncAccountVaultError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("malformed account ID")]
DeserializationFailed(#[from] ConversionError),
#[error("account {0} is not public")]
AccountNotPublic(AccountId),
}
#[derive(Debug, Error, GrpcError)]
pub enum SyncAccountStorageMapsError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("malformed account ID")]
DeserializationFailed(#[from] ConversionError),
#[error("account {0} not found")]
AccountNotFound(AccountId),
#[error("account {0} is not public")]
AccountNotPublic(AccountId),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetNetworkAccountIdsError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("malformed nullifier prefix")]
DeserializationFailed(#[from] ConversionError),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetBlockByNumberError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("malformed block number")]
DeserializationFailed(#[from] DeserializationError),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetAccountError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("malformed request")]
DeserializationFailed(#[from] ConversionError),
#[error("account {0} not found at block {1}")]
AccountNotFound(AccountId, BlockNumber),
#[error("account {0} is not public")]
AccountNotPublic(AccountId),
#[error("block {0} is unknown")]
UnknownBlock(BlockNumber),
#[error("block {0} has been pruned")]
BlockPruned(BlockNumber),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetNotesByIdError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("malformed note ID")]
DeserializationFailed(#[from] ConversionError),
#[error("note {0} not found")]
NoteNotFound(NoteId),
#[error("note {0} is not public")]
NoteNotPublic(NoteId),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetNoteScriptByRootError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("malformed script root")]
DeserializationFailed(#[from] ConversionError),
#[error("script with given root not found")]
ScriptNotFound,
}
#[derive(Debug, Error, GrpcError)]
pub enum CheckNullifiersError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("malformed nullifier")]
DeserializationFailed(#[from] ConversionError),
}
#[derive(Debug, Error, GrpcError)]
pub enum SyncTransactionsError {
#[error("database error")]
#[grpc(internal)]
DatabaseError(#[from] DatabaseError),
#[error("invalid block range")]
InvalidBlockRange(#[from] InvalidBlockRange),
#[error("malformed account ID")]
DeserializationFailed(#[from] ConversionError),
#[error("account {0} not found")]
AccountNotFound(AccountId),
#[error("failed to retrieve witness")]
WitnessError(#[from] WitnessError),
}
#[derive(Debug, Error, GrpcError)]
pub enum GetWitnessesError {
#[error("malformed request")]
DeserializationFailed(#[from] ConversionError),
#[error("failed to retrieve witness")]
WitnessError(#[from] WitnessError),
}
#[cfg(test)]
mod get_account_error_tests {
use miden_protocol::account::AccountId;
use miden_protocol::block::BlockNumber;
use miden_protocol::testing::account_id::AccountIdBuilder;
use tonic::Status;
use super::GetAccountError;
fn test_account_id() -> AccountId {
AccountIdBuilder::new().build_with_seed([1; 32])
}
#[test]
fn unknown_block_returns_invalid_argument() {
let block = BlockNumber::from(999);
let err = GetAccountError::UnknownBlock(block);
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(!status.metadata().is_empty() || !status.details().is_empty());
}
#[test]
fn block_pruned_returns_invalid_argument() {
let block = BlockNumber::from(1);
let err = GetAccountError::BlockPruned(block);
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn account_not_public_returns_invalid_argument() {
let err = GetAccountError::AccountNotPublic(test_account_id());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn account_not_found_returns_invalid_argument_with_block_context() {
let account_id = test_account_id();
let block = BlockNumber::from(5);
let err = GetAccountError::AccountNotFound(account_id, block);
let msg = err.to_string();
assert!(msg.contains("not found"), "error message should mention 'not found'");
assert!(msg.contains("block"), "error message should include block context");
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn each_variant_has_unique_discriminant() {
let account_id = test_account_id();
let block = BlockNumber::from(1);
let errors = [
GetAccountError::AccountNotFound(account_id, block),
GetAccountError::AccountNotPublic(account_id),
GetAccountError::UnknownBlock(block),
GetAccountError::BlockPruned(block),
];
let codes: Vec<u8> = errors.iter().map(|e| e.api_error().api_code()).collect();
for &code in &codes {
assert_ne!(code, 0, "non-internal variants should not map to Internal (0)");
}
let mut sorted = codes.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), codes.len(), "all error variants should have unique codes");
}
}
mod compile_tests {
use std::marker::PhantomData;
use super::{
AccountDeltaError,
AccountError,
DatabaseError,
DeserializationError,
NetworkAccountError,
NoteError,
RecvError,
StateInitializationError,
};
#[expect(dead_code)]
fn assumed_trait_bounds_upheld() {
fn ensure_is_error<E>(_phony: PhantomData<E>)
where
E: std::error::Error + Send + Sync + 'static,
{
}
ensure_is_error::<AccountError>(PhantomData);
ensure_is_error::<AccountDeltaError>(PhantomData);
ensure_is_error::<RecvError>(PhantomData);
ensure_is_error::<DeserializationError>(PhantomData);
ensure_is_error::<NetworkAccountError>(PhantomData);
ensure_is_error::<NoteError>(PhantomData);
ensure_is_error::<hex::FromHexError>(PhantomData);
ensure_is_error::<deadpool::managed::PoolError<deadpool_diesel::Error>>(PhantomData);
ensure_is_error::<diesel::result::Error>(PhantomData);
ensure_is_error::<deadpool_diesel::Error>(PhantomData);
ensure_is_error::<deadpool::managed::RecycleError<deadpool_diesel::Error>>(PhantomData);
ensure_is_error::<DatabaseError>(PhantomData);
ensure_is_error::<diesel::result::Error>(PhantomData);
ensure_is_error::<StateInitializationError>(PhantomData);
ensure_is_error::<deadpool::managed::PoolError<deadpool_diesel::Error>>(PhantomData);
}
}