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