Skip to main content

miden_node_store/db/
mod.rs

1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::mem::size_of;
3use std::num::NonZeroUsize;
4use std::ops::{Deref, DerefMut};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use anyhow::Context;
9use diesel::{Connection, SqliteConnection};
10use miden_node_proto::domain::account::AccountInfo;
11use miden_node_tracing::{info, miden_instrument, warn};
12use miden_node_utils::limiter::{
13    MAX_RESPONSE_PAYLOAD_BYTES,
14    QueryParamLimiter,
15    QueryParamNoteCommitmentLimit,
16};
17use miden_protocol::Word;
18use miden_protocol::account::{AccountHeader, AccountId, AccountStorageHeader, StorageMapKey};
19use miden_protocol::asset::{Asset, AssetId};
20use miden_protocol::block::{
21    BlockHeader,
22    BlockNoteIndex,
23    BlockNumber,
24    BlockSignatures,
25    SignedBlock,
26};
27use miden_protocol::crypto::merkle::SparseMerklePath;
28use miden_protocol::note::{
29    NoteAttachments,
30    NoteDetails,
31    NoteId,
32    NoteInclusionProof,
33    NoteMetadata,
34    NoteScript,
35    Nullifier,
36};
37use miden_protocol::protocol_config::ProtocolConfig;
38use miden_protocol::transaction::TransactionHeader;
39use miden_protocol::utils::serde::Deserializable;
40
41use crate::db::migrations::{migrate_database, verify_latest_schema};
42use crate::db::models::conv::SqlTypeConvert;
43use crate::db::models::queries;
44pub use crate::db::models::queries::{
45    AccountCommitmentsPage,
46    NullifiersPage,
47    PublicAccountIdsPage,
48    PublicAccountStateRootsPage,
49};
50use crate::db::models::queries::{
51    BlockHeaderCommitment,
52    PrecomputedPublicAccountStates,
53    StorageMapValuesPage,
54};
55use crate::errors::{DatabaseError, NoteSyncError};
56use crate::genesis::GenesisBlock;
57use crate::state::{ScopedBlockNum, ScopedBlockRange};
58use crate::{COMPONENT, LOG_TARGET};
59
60const STORAGE_MAP_VALUE_PER_ROW_BYTES: usize =
61    2 * size_of::<Word>() + size_of::<u32>() + size_of::<u8>();
62
63fn default_storage_map_entries_limit() -> usize {
64    MAX_RESPONSE_PAYLOAD_BYTES / STORAGE_MAP_VALUE_PER_ROW_BYTES
65}
66
67mod migrations;
68#[cfg(test)]
69pub(crate) use migrations::bootstrap_database;
70
71#[cfg(test)]
72mod tests;
73
74pub(crate) mod models;
75
76/// [diesel](https://diesel.rs) generated schema
77///
78/// The ignored `diesel_schema_is_in_sync_with_migrations` test verifies that this file matches the
79/// schema produced by the current migrations.
80pub(crate) mod schema;
81
82pub type Result<T, E = DatabaseError> = std::result::Result<T, E>;
83
84/// Database options used by the store state.
85#[derive(Copy, Clone, Debug, PartialEq, Eq)]
86pub struct DatabaseOptions {
87    /// Maximum number of SQLite connections in the connection pool.
88    pub connection_pool_size: NonZeroUsize,
89}
90
91impl Default for DatabaseOptions {
92    fn default() -> Self {
93        Self {
94            connection_pool_size: miden_node_db::default_connection_pool_size(),
95        }
96    }
97}
98
99/// The Store's database.
100///
101/// Extends the underlying [`miden_node_db::Db`] type with functionality specific to the Store.
102pub struct Db {
103    db: miden_node_db::Db,
104}
105
106fn insert_genesis(conn: &mut SqliteConnection, genesis: GenesisBlock) -> Result<()> {
107    let (genesis_block, protocol_config) = genesis.into_parts();
108    conn.transaction(move |conn| {
109        models::queries::insert_protocol_config(conn, &protocol_config, BlockNumber::GENESIS)?;
110        models::queries::apply_block(
111            conn,
112            &genesis_block,
113            &[],
114            &PrecomputedPublicAccountStates::new(),
115        )
116    })?;
117    Ok(())
118}
119
120impl Deref for Db {
121    type Target = miden_node_db::Db;
122
123    fn deref(&self) -> &Self::Target {
124        &self.db
125    }
126}
127
128impl DerefMut for Db {
129    fn deref_mut(&mut self) -> &mut Self::Target {
130        &mut self.db
131    }
132}
133
134/// Describes the value of an asset for an account ID at `block_num` specifically.
135///
136/// If `asset` is `None`, the asset was removed.
137#[derive(Debug, Clone)]
138pub struct AccountVaultValue {
139    pub block_num: BlockNumber,
140    pub vault_key: AssetId,
141    /// None if the asset was removed
142    pub asset: Option<Asset>,
143}
144
145impl AccountVaultValue {
146    pub fn from_raw_row(row: (i64, Vec<u8>, Option<Vec<u8>>)) -> Result<Self, DatabaseError> {
147        let (block_num, vault_key, asset) = row;
148        let vault_key = Word::read_from_bytes(&vault_key)?;
149        Ok(Self {
150            block_num: BlockNumber::from_raw_sql(block_num)?,
151            vault_key: AssetId::try_from(vault_key)?,
152            asset: asset.map(|b| Asset::read_from_bytes(&b)).transpose()?,
153        })
154    }
155}
156
157#[derive(Debug, PartialEq)]
158pub struct NullifierInfo {
159    pub nullifier: Nullifier,
160    pub block_num: BlockNumber,
161}
162
163impl PartialEq<(Nullifier, BlockNumber)> for NullifierInfo {
164    fn eq(&self, (nullifier, block_num): &(Nullifier, BlockNumber)) -> bool {
165        &self.nullifier == nullifier && &self.block_num == block_num
166    }
167}
168
169#[derive(Debug, PartialEq)]
170pub struct TransactionRecord {
171    pub block_num: BlockNumber,
172    pub header: TransactionHeader,
173    /// Inclusion proofs for committed output notes. Notes in `header.output_notes()` without a
174    /// corresponding proof here were erased (created and consumed within the same batch).
175    pub output_note_proofs: Vec<NoteSyncRecord>,
176    /// Maps each consumed input note's nullifier to its note ID, for public notes the node could
177    /// resolve. This is to enable the recover of notes by their id.
178    pub consumed_note_refs: Vec<(Nullifier, NoteId)>,
179}
180
181#[derive(Debug, Clone, PartialEq)]
182pub struct NoteRecord {
183    pub block_num: BlockNumber,
184    pub note_index: BlockNoteIndex,
185    pub note_id: Word,
186    pub metadata: NoteMetadata,
187    pub details: Option<NoteDetails>,
188    pub attachments: NoteAttachments,
189    pub inclusion_path: SparseMerklePath,
190}
191
192#[derive(Debug, PartialEq)]
193pub struct NoteSyncUpdate {
194    pub notes: Vec<NoteSyncRecord>,
195    pub block_header: BlockHeader,
196}
197
198#[derive(Debug, Clone, PartialEq)]
199pub struct NoteSyncRecord {
200    pub block_num: BlockNumber,
201    pub note_index: BlockNoteIndex,
202    pub note_id: NoteId,
203    pub metadata: NoteMetadata,
204    pub attachments: NoteAttachments,
205    pub inclusion_path: SparseMerklePath,
206}
207
208impl From<NoteRecord> for NoteSyncRecord {
209    fn from(note: NoteRecord) -> Self {
210        Self {
211            block_num: note.block_num,
212            note_index: note.note_index,
213            note_id: NoteId::from_raw(note.note_id),
214            metadata: note.metadata,
215            attachments: note.attachments,
216            inclusion_path: note.inclusion_path,
217        }
218    }
219}
220
221impl Db {
222    /// Creates a new database and inserts the genesis block.
223    #[miden_instrument(
224        target = COMPONENT,
225        name = "store.database.bootstrap",
226        fields(path = database_filepath),
227        err,
228    )]
229    pub fn bootstrap(database_filepath: PathBuf, genesis: GenesisBlock) -> anyhow::Result<()> {
230        migrations::bootstrap_database(&database_filepath)
231            .context("failed to bootstrap database schema")?;
232
233        let mut conn: SqliteConnection = diesel::sqlite::SqliteConnection::establish(
234            database_filepath.to_str().context("database filepath is invalid")?,
235        )
236        .context("failed to open a database connection")?;
237
238        miden_node_db::configure_connection_on_creation(&mut conn)?;
239
240        // Insert genesis block data.
241        insert_genesis(&mut conn, genesis).context("failed to insert genesis block")?;
242        Ok(())
243    }
244
245    /// Open a connection to the DB after verifying that it is at the latest schema version.
246    #[miden_instrument(
247        target = COMPONENT,
248    )]
249    pub async fn load(database_filepath: PathBuf) -> Result<Self, DatabaseError> {
250        Self::load_with_pool_size(database_filepath, miden_node_db::default_connection_pool_size())
251            .await
252    }
253
254    /// Open a connection to the DB with a specific pool size after verifying that it is at the
255    /// latest schema version.
256    #[miden_instrument(
257        target = COMPONENT,
258    )]
259    pub async fn load_with_pool_size(
260        database_filepath: PathBuf,
261        connection_pool_size: NonZeroUsize,
262    ) -> Result<Self, DatabaseError> {
263        verify_latest_schema(&database_filepath)?;
264
265        let db = miden_node_db::Db::new_with_pool_size(&database_filepath, connection_pool_size)?;
266        info!(
267            target: LOG_TARGET,
268            "Connected to the database",
269            path = database_filepath,
270            db.sqlite.connection_pool_size = connection_pool_size.get()
271        );
272
273        Ok(Self { db })
274    }
275
276    /// Selects a protocol configuration by its commitment.
277    #[miden_instrument(
278        level = "debug",
279        target = COMPONENT,
280        err,
281    )]
282    pub async fn select_protocol_config_by_commitment(
283        &self,
284        commitment: Word,
285    ) -> Result<Option<ProtocolConfig>> {
286        self.transact("protocol config by commitment", move |conn| {
287            queries::select_protocol_config_by_commitment(conn, commitment)
288        })
289        .await
290    }
291
292    /// Selects the configuration commitment active at the specified block.
293    pub async fn select_protocol_config_commitment_at(
294        &self,
295        block_number: ScopedBlockNum,
296    ) -> Result<Option<Word>> {
297        self.transact("protocol config commitment at block", move |conn| {
298            queries::select_protocol_config_commitment_at(conn, *block_number)
299        })
300        .await
301    }
302
303    /// Applies all pending migrations to an existing DB.
304    #[miden_instrument(
305        target = COMPONENT,
306    )]
307    pub fn migrate(database_filepath: impl AsRef<Path>) -> Result<(), DatabaseError> {
308        migrate_database(database_filepath.as_ref())?;
309        Ok(())
310    }
311
312    /// Returns a page of nullifiers for tree rebuilding.
313    #[miden_instrument(
314        level = "debug",
315        target = COMPONENT,
316        err,
317    )]
318    pub async fn select_nullifiers_paged(
319        &self,
320        page_size: std::num::NonZeroUsize,
321        after_nullifier: Option<Nullifier>,
322    ) -> Result<NullifiersPage> {
323        self.transact("read nullifiers paged", move |conn| {
324            queries::select_nullifiers_paged(conn, page_size, after_nullifier)
325        })
326        .await
327    }
328
329    /// Loads the nullifiers that match the prefixes from the DB.
330    #[miden_instrument(
331        level = "debug",
332        target = COMPONENT,
333        fields(
334            prefix_len,
335            prefix.count = nullifier_prefixes.len(),
336        ),
337        err,
338    )]
339    pub async fn select_nullifiers_by_prefix(
340        &self,
341        prefix_len: u32,
342        nullifier_prefixes: Vec<u32>,
343        block_range: ScopedBlockRange,
344    ) -> Result<(Vec<NullifierInfo>, BlockNumber)> {
345        let block_range = block_range.into_inner();
346        assert_eq!(prefix_len, 16, "Only 16-bit prefixes are supported");
347
348        self.transact("nullifieres by prefix", move |conn| {
349            let nullifier_prefixes =
350                nullifier_prefixes.into_iter().map(|prefix| prefix as u16).collect::<Vec<_>>();
351            queries::select_nullifiers_by_prefix(
352                conn,
353                prefix_len as u8,
354                &nullifier_prefixes[..],
355                block_range,
356            )
357        })
358        .await
359    }
360
361    /// Search for a [`BlockHeader`] from the database by its `block_num`.
362    ///
363    /// When `block_number` is [None], the latest block header is returned.
364    #[miden_instrument(
365        level = "debug",
366        target = COMPONENT,
367        err,
368    )]
369    pub async fn select_block_header_by_block_num(
370        &self,
371        maybe_block_number: Option<ScopedBlockNum>,
372    ) -> Result<Option<BlockHeader>> {
373        self.transact("block headers by block number", move |conn| {
374            let val = queries::select_block_header_by_block_num(
375                conn,
376                maybe_block_number.map(|block_number| *block_number),
377            )?;
378            Ok(val)
379        })
380        .await
381    }
382
383    /// Selects the genesis block header for state initialization.
384    pub(crate) async fn select_genesis_block_header(&self) -> Result<Option<BlockHeader>> {
385        self.transact("genesis block header", |conn| {
386            queries::select_block_header_by_block_num(conn, Some(BlockNumber::GENESIS))
387        })
388        .await
389    }
390
391    /// Search for a [`BlockHeader`] and its [`BlockSignatures`] from the database by its
392    /// `block_num`.
393    #[miden_instrument(
394        level = "debug",
395        target = COMPONENT,
396        err,
397    )]
398    pub async fn select_block_header_and_signatures_by_block_num(
399        &self,
400        block_number: ScopedBlockNum,
401    ) -> Result<Option<(BlockHeader, BlockSignatures)>> {
402        self.transact("block headers and signatures by block number", move |conn| {
403            let val =
404                queries::select_block_header_and_signatures_by_block_num(conn, *block_number)?;
405            Ok(val)
406        })
407        .await
408    }
409
410    /// Loads multiple block headers from the DB.
411    #[miden_instrument(
412        level = "debug",
413        target = COMPONENT,
414        err,
415    )]
416    pub async fn select_block_headers(
417        &self,
418        blocks: impl Iterator<Item = ScopedBlockNum> + Send + 'static,
419    ) -> Result<Vec<BlockHeader>> {
420        self.transact("block headers from given block numbers", move |conn| {
421            let raw = queries::select_block_headers(conn, blocks.map(|block| *block))?;
422            Ok(raw)
423        })
424        .await
425    }
426
427    /// Loads all the block headers from the DB.
428    #[miden_instrument(
429        level = "debug",
430        target = COMPONENT,
431        err,
432    )]
433    pub async fn select_all_block_header_commitments(&self) -> Result<Vec<BlockHeaderCommitment>> {
434        self.transact("all block headers", |conn| {
435            let raw = queries::select_all_block_header_commitments(conn)?;
436            Ok(raw)
437        })
438        .await
439    }
440
441    /// Returns a page of account commitments for tree rebuilding.
442    #[miden_instrument(
443        level = "debug",
444        target = COMPONENT,
445        err,
446    )]
447    pub async fn select_account_commitments_paged(
448        &self,
449        page_size: std::num::NonZeroUsize,
450        after_account_id: Option<AccountId>,
451    ) -> Result<AccountCommitmentsPage> {
452        self.transact("read account commitments paged", move |conn| {
453            queries::select_account_commitments_paged(conn, page_size, after_account_id)
454        })
455        .await
456    }
457
458    /// Returns a page of public account IDs for forest rebuilding.
459    #[miden_instrument(
460        level = "debug",
461        target = COMPONENT,
462        err,
463    )]
464    pub async fn select_public_account_ids_paged(
465        &self,
466        page_size: std::num::NonZeroUsize,
467        after_account_id: Option<AccountId>,
468    ) -> Result<PublicAccountIdsPage> {
469        self.transact("read public account IDs paged", move |conn| {
470            queries::select_public_account_ids_paged(conn, page_size, after_account_id)
471        })
472        .await
473    }
474
475    /// Returns a page of public account state roots for forest consistency verification.
476    #[miden_instrument(
477        level = "debug",
478        target = COMPONENT,
479        err,
480    )]
481    pub async fn select_public_account_state_roots_paged(
482        &self,
483        page_size: std::num::NonZeroUsize,
484        after_account_id: Option<AccountId>,
485    ) -> Result<PublicAccountStateRootsPage> {
486        self.transact("read public account state roots paged", move |conn| {
487            queries::select_public_account_state_roots_paged(conn, page_size, after_account_id)
488        })
489        .await
490    }
491
492    /// Loads public account details from the DB.
493    #[miden_instrument(
494        level = "debug",
495        target = COMPONENT,
496        err,
497    )]
498    pub async fn select_account(&self, id: AccountId) -> Result<AccountInfo> {
499        self.transact("Get account details", move |conn| queries::select_account(conn, id))
500            .await
501    }
502
503    /// Returns the subset of the provided account IDs that classify as network accounts.
504    #[miden_instrument(
505        level = "debug",
506        target = COMPONENT,
507        err,
508    )]
509    pub async fn select_network_accounts_subset(
510        &self,
511        account_ids: Vec<AccountId>,
512    ) -> Result<HashSet<AccountId>> {
513        self.transact("Filter network accounts subset", move |conn| {
514            queries::select_network_accounts_subset(conn, &account_ids)
515        })
516        .await
517    }
518
519    /// Queries the account code by its commitment hash.
520    ///
521    /// Returns `None` if no code exists with that commitment.
522    #[miden_instrument(
523        target = COMPONENT,
524    )]
525    pub async fn select_account_code_by_commitment(
526        &self,
527        code_commitment: Word,
528    ) -> Result<Option<miden_protocol::account::AccountCode>> {
529        self.transact("Get account code by commitment", move |conn| {
530            queries::select_account_code_by_commitment(conn, code_commitment)?
531                .map(|bytes| miden_protocol::account::AccountCode::read_from_bytes(&bytes))
532                .transpose()
533                .map_err(DatabaseError::from)
534        })
535        .await
536    }
537
538    /// Queries the account header and storage header for a specific account at a block.
539    ///
540    /// Returns both in a single query to avoid querying the database twice.
541    /// Returns `None` if the account doesn't exist at that block.
542    #[miden_instrument(
543        target = COMPONENT,
544    )]
545    pub async fn select_account_header_with_storage_header_at_block(
546        &self,
547        account_id: AccountId,
548        block_num: ScopedBlockNum,
549    ) -> Result<Option<(AccountHeader, AccountStorageHeader)>> {
550        self.transact("Get account header with storage header at block", move |conn| {
551            queries::select_account_header_with_storage_header_at_block(
552                conn, account_id, *block_num,
553            )
554        })
555        .await
556    }
557
558    #[miden_instrument(
559        level = "debug",
560        target = COMPONENT,
561        err,
562    )]
563    pub async fn get_note_sync_multi(
564        &self,
565        block_range: ScopedBlockRange,
566        note_tags: Arc<[u32]>,
567    ) -> Result<Vec<NoteSyncUpdate>, NoteSyncError> {
568        let block_range = block_range.into_inner();
569        self.transact("notes sync task", move |conn| {
570            queries::get_note_sync_multi(conn, &note_tags, block_range, MAX_RESPONSE_PAYLOAD_BYTES)
571        })
572        .await
573    }
574
575    /// Loads all the [`miden_protocol::note::Note`]s matching a certain [`NoteId`] from the
576    /// database.
577    #[miden_instrument(
578        level = "debug",
579        target = COMPONENT,
580        err,
581    )]
582    pub async fn select_notes_by_id(&self, note_ids: Vec<NoteId>) -> Result<Vec<NoteRecord>> {
583        self.transact("note by id", move |conn| {
584            queries::select_notes_by_id(conn, note_ids.as_slice())
585        })
586        .await
587    }
588
589    /// Returns all note commitments from the DB that match the provided ones and were committed at
590    /// or before `up_to_block`.
591    #[miden_instrument(
592        level = "debug",
593        target = COMPONENT,
594        err,
595    )]
596    pub async fn select_existing_note_commitments(
597        &self,
598        note_commitments: Vec<Word>,
599        up_to_block: ScopedBlockNum,
600    ) -> Result<HashSet<Word>> {
601        self.transact("note by commitment", move |conn| {
602            queries::select_existing_note_commitments(
603                conn,
604                note_commitments.as_slice(),
605                *up_to_block,
606            )
607        })
608        .await
609    }
610
611    /// Loads inclusion proofs for notes matching the given note commitments that were committed at
612    /// or before `up_to_block`.
613    #[miden_instrument(
614        level = "debug",
615        target = COMPONENT,
616        err,
617    )]
618    pub async fn select_note_inclusion_proofs(
619        &self,
620        note_commitments: BTreeSet<Word>,
621        up_to_block: ScopedBlockNum,
622    ) -> Result<BTreeMap<NoteId, NoteInclusionProof>> {
623        self.transact("block note inclusion proofs by commitment", move |conn| {
624            models::queries::select_note_inclusion_proofs(conn, &note_commitments, *up_to_block)
625        })
626        .await
627    }
628
629    /// Inserts the data of a new block into the DB.
630    ///
631    /// The transaction is committed when this method returns. Synchronization with the in-memory
632    /// trees is handled by the block writer task; see [`super::state::State::apply_block`].
633    ///
634    /// Account history is pruned in the same transaction against `prune_tip`: the effective tip
635    /// for retention, which lags the actual tip while old snapshot generations are still pinned
636    /// by readers (SQLite reads have no point-in-time protection, unlike the `RocksDB`-backed
637    /// trees).
638    ///
639    /// Consumed note IDs omitted from transaction headers are resolved from
640    /// `unresolved_note_nullifiers` on a best-effort basis. The returned mapping is used only for
641    /// lifecycle events and never affects block application. `unresolved_note_nullifiers` is empty
642    /// when neither INFO nor DEBUG lifecycle events are enabled.
643    // TODO: This span is logged in a root span, we should connect it to the parent one.
644    #[miden_instrument(
645        target = COMPONENT,
646        err,
647    )]
648    pub(crate) async fn apply_block(
649        &self,
650        signed_block: SignedBlock,
651        activated_protocol_config: Option<ProtocolConfig>,
652        notes: Vec<(NoteRecord, Option<Nullifier>)>,
653        precomputed_public_states: PrecomputedPublicAccountStates,
654        unresolved_note_nullifiers: Vec<Nullifier>,
655        prune_tip: BlockNumber,
656    ) -> Result<BTreeMap<Nullifier, NoteId>> {
657        self.transact("apply block", move |conn| {
658            if let Some(protocol_config) = activated_protocol_config.as_ref() {
659                queries::insert_protocol_config(
660                    conn,
661                    protocol_config,
662                    signed_block.header().block_num(),
663                )?;
664            }
665            models::queries::apply_block(conn, &signed_block, &notes, &precomputed_public_states)?;
666            models::queries::prune_history(conn, prune_tip)?;
667
668            let mut resolved_note_ids = BTreeMap::new();
669            for chunk in unresolved_note_nullifiers.chunks(QueryParamNoteCommitmentLimit::LIMIT) {
670                match queries::select_note_ids_by_nullifier(conn, chunk) {
671                    Ok(note_ids) => resolved_note_ids.extend(note_ids),
672                    Err(err) => {
673                        warn!(
674                            &err,
675                            target: COMPONENT,
676                            "Failed to resolve consumed note IDs for lifecycle events",
677                            note.nullifier.count = chunk.len()
678                        );
679                        break;
680                    },
681                }
682            }
683
684            Ok(resolved_note_ids)
685        })
686        .await
687    }
688
689    /// Selects storage map values for syncing storage maps for a specific account ID.
690    ///
691    /// The returned values are the latest known values up to `block_range.end()`, and no values
692    /// earlier than `block_range.start()` are returned.
693    pub(crate) async fn select_storage_map_sync_values(
694        &self,
695        account_id: AccountId,
696        block_range: ScopedBlockRange,
697        entries_limit: Option<usize>,
698    ) -> Result<StorageMapValuesPage> {
699        let block_range = block_range.into_inner();
700        let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
701
702        self.transact("select storage map sync values", move |conn| {
703            models::queries::select_account_storage_map_values_paged(
704                conn,
705                account_id,
706                block_range,
707                entries_limit,
708            )
709        })
710        .await
711    }
712
713    /// Reconstructs storage map details from the database for a specific slot at a block.
714    ///
715    /// Used as fallback when `AccountStateForest` cache misses (historical or evicted queries).
716    /// Rebuilds all entries by querying the DB and filtering to the specific slot.
717    ///
718    /// Returns:
719    ///     - `::LimitExceeded` when too many entries are present
720    ///     - `::AllEntries` if the size is less than or equal given `entries_limit`, if any
721    #[miden_instrument(
722        target = COMPONENT,
723    )]
724    pub(crate) async fn reconstruct_storage_map_from_db(
725        &self,
726        account_id: AccountId,
727        slot_name: miden_protocol::account::StorageSlotName,
728        block_num: ScopedBlockNum,
729        entries_limit: Option<usize>,
730    ) -> Result<miden_node_proto::domain::account::AccountStorageMapDetails> {
731        use miden_node_proto::domain::account::{AccountStorageMapDetails, StorageMapEntries};
732        use miden_protocol::EMPTY_WORD;
733
734        // TODO this remains expensive with a large history until we implement pruning for DB
735        // columns
736        let mut values = Vec::new();
737        let mut block_range_start = BlockNumber::GENESIS;
738        let entries_limit = entries_limit.unwrap_or_else(default_storage_map_entries_limit);
739
740        let mut page = self
741            .select_storage_map_sync_values(
742                account_id,
743                block_num.range_from(block_range_start),
744                Some(entries_limit),
745            )
746            .await?;
747
748        values.extend(page.values);
749        let mut last_block_included = page.last_block_included;
750
751        // If the first page returned no values, the block at block_range_start has more entries
752        // than the limit allows (e.g. genesis accounts with large storage maps).
753        if values.is_empty() && last_block_included == block_range_start {
754            return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
755        }
756
757        loop {
758            if page.last_block_included == *block_num
759                || page.last_block_included < block_range_start
760            {
761                break;
762            }
763
764            block_range_start = page.last_block_included.child();
765            page = self
766                .select_storage_map_sync_values(
767                    account_id,
768                    block_num.range_from(block_range_start),
769                    Some(entries_limit),
770                )
771                .await?;
772
773            if page.last_block_included <= last_block_included {
774                return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
775            }
776
777            last_block_included = page.last_block_included;
778            values.extend(page.values);
779        }
780
781        if page.last_block_included != *block_num {
782            return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
783        }
784
785        // Filter to the specific slot and collect latest values per key
786        let mut latest_values = BTreeMap::<StorageMapKey, Word>::new();
787        for value in values {
788            if value.slot_name == slot_name {
789                let raw_key = value.key;
790                latest_values.insert(raw_key, value.value);
791            }
792        }
793
794        // Remove EMPTY_WORD entries (deletions)
795        latest_values.retain(|_, v| *v != EMPTY_WORD);
796
797        if latest_values.len() > AccountStorageMapDetails::MAX_RETURN_ENTRIES {
798            return Ok(AccountStorageMapDetails::limit_exceeded(slot_name));
799        }
800
801        let entries = latest_values.into_iter().collect::<Vec<_>>();
802        Ok(AccountStorageMapDetails {
803            slot_name,
804            entries: StorageMapEntries::AllEntries(entries),
805        })
806    }
807
808    /// Reconstructs the account vault from the database for a specific account at a block.
809    ///
810    /// Used as fallback when the `AccountStateForest` vault-key cache misses (historical or evicted
811    /// queries). Returns the latest asset for each vault key at or before `block_num`.
812    #[miden_instrument(
813        target = COMPONENT,
814    )]
815    pub async fn select_account_vault_at_block(
816        &self,
817        account_id: AccountId,
818        block_num: ScopedBlockNum,
819    ) -> Result<Vec<Asset>, DatabaseError> {
820        self.transact("select account vault at block", move |conn| {
821            queries::select_account_vault_at_block(conn, account_id, *block_num)
822        })
823        .await
824    }
825
826    pub async fn get_account_vault_sync(
827        &self,
828        account_id: AccountId,
829        block_range: ScopedBlockRange,
830    ) -> Result<(BlockNumber, Vec<AccountVaultValue>)> {
831        let block_range = block_range.into_inner();
832        self.transact("account vault sync", move |conn| {
833            queries::select_account_vault_assets(conn, account_id, block_range)
834        })
835        .await
836    }
837
838    /// Returns the script for a note by its root.
839    pub async fn select_note_script_by_root(&self, root: Word) -> Result<Option<NoteScript>> {
840        self.transact("note script by root", move |conn| {
841            queries::select_note_script_by_root(conn, root)
842        })
843        .await
844    }
845
846    /// Returns the complete transaction records for the specified accounts within the specified
847    /// block range, including state commitments and note IDs.
848    ///
849    /// Note: This method is size-limited (~5MB) and may not return all matching transactions
850    /// if the limit is exceeded. Transactions from partial blocks are excluded to maintain
851    /// consistency.
852    pub async fn select_transactions_records(
853        &self,
854        account_ids: Vec<AccountId>,
855        block_range: ScopedBlockRange,
856    ) -> Result<(BlockNumber, Vec<TransactionRecord>)> {
857        let block_range = block_range.into_inner();
858        self.transact("full transactions records", move |conn| {
859            queries::select_transactions_records(conn, &account_ids, block_range)
860        })
861        .await
862    }
863}