miden-node-store 0.17.2

Miden node's state store component
use std::io;

use miden_node_proto::domain::block::InvalidBlockRange;
use miden_node_proto::errors::ConversionError;
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::merkle::smt::{LargeSmtForestError, LineageId};
use miden_protocol::crypto::utils::DeserializationError;
use miden_protocol::errors::{
    AccountDeltaError,
    AccountError,
    AccountTreeError,
    AssetError,
    AssetVaultError,
    NoteError,
    NullifierTreeError,
    StorageMapError,
};
use miden_protocol::note::Nullifier;
use miden_protocol::transaction::OutputNote;
use thiserror::Error;
use tokio::sync::oneshot::error::RecvError;

/// Errors produced while preparing or rebuilding account-state forest updates.
///
/// The underlying [`LargeSmtForestError`] is preserved so callers can distinguish fatal backend
/// failures from invalid update preparation.
#[derive(Debug, Error)]
pub enum AccountStateForestUpdateError {
    /// The patch of a new account attempted to create an existing vault lineage.
    #[error("account {account_id} vault lineage already exists")]
    VaultLineageAlreadyExists { account_id: AccountId },
    /// The patch of a new account attempted to create an existing storage-map lineage.
    #[error("account {account_id} storage map lineage for slot {slot_name} already exists")]
    StorageLineageAlreadyExists {
        account_id: AccountId,
        slot_name: miden_protocol::account::StorageSlotName,
    },
    /// The forest mutation set did not contain a root for a lineage it was expected to update.
    #[error("computed forest mutations are missing lineage {lineage}")]
    MissingComputedRoot { lineage: LineageId },
    /// The forest backend failed while computing, reading, or applying mutations.
    #[error(transparent)]
    Forest(#[from] LargeSmtForestError),
}

// DATABASE ERRORS
// =================================================================================================

#[derive(Debug, Error)]
pub enum DatabaseError {
    #[error("invalid persisted protobuf value")]
    Persistence(#[from] miden_node_persistence::PersistenceError),
    // ERRORS WITH AUTOMATIC CONVERSIONS FROM NESTED ERROR TYPES
    // ---------------------------------------------------------------------------------------------
    #[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)]
    QueryParamLimit(#[from] QueryLimitError),
    #[error(transparent)]
    RangeBeyondTip(#[from] RangeBeyondTip),

    // OTHER ERRORS
    // ---------------------------------------------------------------------------------------------
    #[error("account commitment mismatch (expected {expected}, but calculated is {calculated})")]
    AccountCommitmentsMismatch { expected: Word, calculated: Word },
    #[error(
        "protocol config commitment mismatch (expected {expected}, but calculated is {calculated})"
    )]
    ProtocolConfigCommitmentMismatch { expected: Word, calculated: Word },
    #[error("protocol config {0} is missing")]
    ProtocolConfigNotFound(Word),
    #[error("account {0} not found")]
    AccountNotFoundInDb(AccountId),
    #[error("account {0} already exists")]
    AccountAlreadyExistsInDb(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(
        "transactions for block {block_num} would exceed maximum response size, \
         use a stricter filter to reduce the number of transactions returned"
    )]
    TransactionPageExceedsPayloadLimit { block_num: BlockNumber },
    #[error("data corrupted: {0}")]
    DataCorrupted(String),
    #[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,
    },
}

// INITIALIZATION ERRORS
// =================================================================================================

#[derive(Error, Debug)]
pub enum StateInitializationError {
    #[error("account tree IO error: {0}")]
    AccountTreeIoError(String),
    #[error("nullifier tree IO error: {0}")]
    NullifierTreeIoError(String),
    #[error("account state forest IO error: {0}")]
    AccountStateForestIoError(String),
    #[error("failed to rebuild account state forest")]
    AccountStateForestRebuild(#[source] AccountStateForestUpdateError),
    #[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 proven tip")]
    ProvenTipLoadError(#[source] std::io::Error),
    #[error("failed to load database")]
    DatabaseLoadError(#[source] DatabaseError),
    #[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("loaded chain MMR cannot produce peaks for block {block_num}")]
    ChainMmrLoadError {
        block_num: BlockNumber,
        #[source]
        source: MmrError,
    },
    #[error(
        "chain MMR commitment ({chain_mmr_commitment:?}) does not match expected chain commitment \
         from block {block_num} ({block_header_commitment:?})"
    )]
    ChainMmrStorageDiverged {
        block_num: BlockNumber,
        chain_mmr_commitment: Word,
        block_header_commitment: Word,
    },
    #[error(
        "account state forest root ({forest_root}) does not match SQLite root \
         ({database_root}) for account {account_id}, slot {slot_name:?}. Delete the account \
         state forest storage directory and restart the node to rebuild from the database."
    )]
    AccountStateForestStorageDiverged {
        account_id: AccountId,
        slot_name: Option<String>,
        forest_root: Word,
        database_root: Word,
    },
    #[error("public account {0} is missing details in database")]
    PublicAccountMissingDetails(AccountId),
    #[error("failed to convert account to delta: {0}")]
    AccountToDeltaConversionFailed(String),
    #[error("genesis block missing. The database should be bootstrapped first.")]
    GenesisBlockMissing,
    #[error(
        "genesis protocol config {commitment} is missing. Rebootstrap the database from genesis."
    )]
    GenesisProtocolConfigMissing { commitment: Word },
}

// ENDPOINT ERRORS
// =================================================================================================
#[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 {
    // ERRORS WITH AUTOMATIC CONVERSIONS FROM NESTED ERROR TYPES
    // ---------------------------------------------------------------------------------------------
    #[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),

    // OTHER ERRORS
    // ---------------------------------------------------------------------------------------------
    #[error("block applying was cancelled because the writer task dropped the result channel")]
    ClosedChannel(#[from] RecvError),
    #[error("account state forest update preparation failed")]
    AccountStateForestPreparation(#[source] AccountStateForestUpdateError),
    #[error("database doesn't have any block header data")]
    DbBlockHeaderEmpty,
    #[error("database update failed: {0}")]
    DbUpdateTaskFailed(String),
    #[error("failed to send block to the writer task: {0}")]
    WriterTaskSendFailed(String),
}

#[derive(Error, Debug)]
pub enum ApplyBlockWithProvingInputsError {
    #[error("failed to save block proving inputs")]
    SaveProvingInputs(#[source] io::Error),
    #[error("failed to apply block")]
    ApplyBlock(#[source] ApplyBlockError),
}

/// A requested block range extends beyond the chain tip of the state view serving the request.
#[derive(Error, Debug)]
#[error("block_to ({block_to}) is greater than chain tip ({chain_tip})")]
pub struct RangeBeyondTip {
    pub chain_tip: BlockNumber,
    pub block_to: BlockNumber,
}

#[derive(Error, Debug)]
pub enum GetBlockHeaderError {
    #[error("database error")]
    DatabaseError(#[from] DatabaseError),
    #[error("error retrieving the merkle proof for the block")]
    MmrError(#[from] MmrError),
}

#[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),
    #[error(transparent)]
    RangeBeyondTip(#[from] RangeBeyondTip),
}

#[derive(Error, Debug)]
pub enum NoteSyncError {
    #[error("database error")]
    DatabaseError(#[from] DatabaseError),
    #[error("database error")]
    UnderlyingDatabaseError(#[from] miden_node_db::DatabaseError),
    #[error("block headers table is empty")]
    EmptyBlockHeadersTable,
    #[error("error retrieving the merkle proof for the block")]
    MmrError(#[from] MmrError),
    #[error("invalid block range")]
    InvalidBlockRange(#[from] InvalidBlockRange),
    #[error(transparent)]
    RangeBeyondTip(#[from] RangeBeyondTip),
    #[error("malformed note tags")]
    DeserializationFailed(#[from] ConversionError),
}

#[derive(Error, Debug)]
pub enum GetNoteInclusionProofsError {
    #[error("failed to select note inclusion proofs")]
    SelectNoteInclusionProofError(#[source] DatabaseError),
    #[error("reference block {reference_block} is newer than the latest block {latest_block_num}")]
    ReferenceBlockAfterTip {
        reference_block: BlockNumber,
        latest_block_num: BlockNumber,
    },
}

#[derive(Error, Debug)]
pub enum GetBlockInclusionProofsError {
    #[error("failed to select block headers")]
    SelectBlockHeaderError(#[source] DatabaseError),
    #[error("reference block {reference_block} is newer than the latest block {latest_block_num}")]
    ReferenceBlockAfterTip {
        reference_block: BlockNumber,
        latest_block_num: BlockNumber,
    },
    #[error("block {block_num} is newer than the reference block {reference_block}")]
    BlockAfterReferenceBlock {
        block_num: BlockNumber,
        reference_block: BlockNumber,
    },
}

// GET ACCOUNT ERRORS
// ================================================================================================

#[derive(Debug, Error)]
pub enum GetAccountError {
    #[error("database error")]
    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),
}

// Do not scope for `cfg(test)` - if it the traitbounds don't suffice the issue will already appear
// in the compilation of the library or binary, which would prevent getting to compiling the
// following code.
mod compile_tests {
    use std::marker::PhantomData;

    use super::{
        AccountDeltaError,
        AccountError,
        DatabaseError,
        DeserializationError,
        NoteError,
        RecvError,
        StateInitializationError,
    };

    /// Ensure all enum variants remain compat with the desired trait bounds. Otherwise one gets
    /// very unwieldy errors.
    #[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::<NoteError>(PhantomData);
        ensure_is_error::<hex::FromHexError>(PhantomData);

        ensure_is_error::<DatabaseError>(PhantomData);
        ensure_is_error::<StateInitializationError>(PhantomData);
    }
}