Skip to main content

miden_node_store/state/
mod.rs

1//! Abstraction to synchronize state modifications.
2//!
3//! The [State] provides data access and modifications methods, its main purpose is to ensure that
4//! data is atomically written, and that reads are consistent.
5
6use std::collections::{BTreeMap, BTreeSet, HashSet};
7use std::num::NonZeroUsize;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use miden_node_proto::domain::batch::BatchInputs;
12use miden_node_utils::clap::StorageOptions;
13use miden_node_utils::formatting::format_array;
14use miden_node_utils::tracing::miden_instrument;
15use miden_protocol::Word;
16use miden_protocol::account::AccountId;
17use miden_protocol::block::account_tree::AccountWitness;
18use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness};
19use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, Blockchain};
20use miden_protocol::crypto::merkle::mmr::{MmrProof, PartialMmr};
21use miden_protocol::crypto::merkle::smt::{LargeSmt, SmtStorage};
22use miden_protocol::note::{NoteId, NoteScript, Nullifier};
23use miden_protocol::transaction::PartialBlockchain;
24use tokio::sync::{Mutex, RwLock, watch};
25use tracing::{Instrument, Span};
26
27use crate::account_state_forest::{AccountStateForest, AccountStateForestBackend};
28use crate::accounts::AccountTreeWithHistory;
29use crate::blocks::BlockStore;
30use crate::db::{Db, NoteRecord, NullifierInfo};
31use crate::errors::{
32    DatabaseError,
33    GetBatchInputsError,
34    GetBlockHeaderError,
35    GetBlockInputsError,
36    StateInitializationError,
37};
38use crate::proven_tip::ProvenTipWriter;
39use crate::{COMPONENT, DataDirectory, DatabaseOptions};
40
41/// Number of recent committed blocks held in the in-memory cache for replica subscriptions.
42const BLOCK_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
43
44/// Number of recent block proofs held in the in-memory cache for replica subscriptions.
45const PROOF_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
46
47mod loader;
48use loader::{
49    ACCOUNT_STATE_FOREST_STORAGE_DIR,
50    ACCOUNT_TREE_STORAGE_DIR,
51    AccountForestLoader,
52    NULLIFIER_TREE_STORAGE_DIR,
53    TreeStorage,
54    TreeStorageLoader,
55    load_mmr,
56    verify_account_state_forest_consistency,
57    verify_tree_consistency,
58};
59
60mod replica;
61pub use replica::{BlockCache, BlockNotification, ProofCache, ProofNotification};
62
63mod account;
64
65mod apply_block;
66mod apply_proof;
67mod block_lifecycle;
68mod bootstrap;
69mod disk_monitor;
70mod sync_state;
71
72// FINALITY
73// ================================================================================================
74
75/// The finality level for chain tip queries.
76#[derive(Debug, Clone, Copy)]
77pub enum Finality {
78    /// The latest committed (but not necessarily proven) block.
79    Committed,
80    /// The latest block that has been proven in an unbroken sequence from genesis.
81    Proven,
82}
83
84// STRUCTURES
85// ================================================================================================
86
87#[derive(Debug, Default)]
88pub struct TransactionInputs {
89    pub account_commitment: Word,
90    pub nullifiers: Vec<NullifierInfo>,
91    pub found_unauthenticated_notes: HashSet<Word>,
92    pub new_account_id_prefix_is_unique: Option<bool>,
93}
94
95type BlockInputWitnesses = (
96    BlockNumber,
97    BTreeMap<AccountId, AccountWitness>,
98    BTreeMap<Nullifier, NullifierWitness>,
99    PartialMmr,
100);
101
102/// Container for state that needs to be updated atomically.
103struct InnerState<S>
104where
105    S: SmtStorage,
106{
107    nullifier_tree: NullifierTree<LargeSmt<S>>,
108    blockchain: Blockchain,
109    account_tree: AccountTreeWithHistory<S>,
110}
111
112impl<S: SmtStorage> InnerState<S> {
113    /// Returns the latest block number.
114    fn latest_block_num(&self) -> BlockNumber {
115        self.blockchain
116            .chain_tip()
117            .expect("chain should always have at least the genesis block")
118    }
119}
120
121// CHAIN STATE
122// ================================================================================================
123
124/// The rollup state.
125pub struct State {
126    /// Root directory containing the store's on-disk data.
127    data_directory: PathBuf,
128
129    /// The database which stores block headers, nullifiers, notes, and the latest states of
130    /// accounts.
131    db: Arc<Db>,
132
133    /// The block store which stores full block contents for all blocks.
134    block_store: Arc<BlockStore>,
135
136    /// Read-write lock used to prevent writing to a structure while it is being used.
137    ///
138    /// The lock is writer-preferring, meaning the writer won't be starved.
139    inner: RwLock<InnerState<TreeStorage>>,
140
141    /// Forest-related state `(SmtForest, storage_map_roots, vault_roots)` with its own lock.
142    forest: RwLock<AccountStateForest<AccountStateForestBackend>>,
143
144    /// To allow readers to access the tree data while an update in being performed, and prevent
145    /// TOCTOU issues, there must be no concurrent writers. This locks to serialize the writers.
146    writer: Mutex<()>,
147
148    /// The latest proven-in-sequence block number, updated by the proof scheduler or `apply_proof`.
149    proven_tip: ProvenTipWriter,
150
151    /// Watch sender fired after each block is committed. Replicas subscribe via
152    /// `subscribe_committed_tip()` to be woken when new blocks arrive.
153    committed_tip_tx: watch::Sender<BlockNumber>,
154
155    /// FIFO cache of recent committed blocks for replica subscriptions. When a subscriber needs a
156    /// block that has been evicted, it falls back to loading from the block store.
157    pub(crate) block_cache: BlockCache,
158
159    /// FIFO cache of recent block proofs for replica subscriptions. When a subscriber needs a proof
160    /// that has been evicted, it falls back to loading from the block store.
161    pub(crate) proof_cache: ProofCache,
162}
163
164impl State {
165    // CONSTRUCTOR
166    // --------------------------------------------------------------------------------------------
167
168    /// Loads the state from the data directory.
169    ///
170    /// The loaded state owns all store data structures and exposes subscription methods for
171    /// sequencer and replica tasks.
172    #[miden_instrument(
173        target = COMPONENT,
174        skip_all,
175    )]
176    pub async fn load(
177        data_path: &Path,
178        storage_options: StorageOptions,
179    ) -> Result<Self, StateInitializationError> {
180        Self::load_with_database_options(data_path, storage_options, DatabaseOptions::default())
181            .await
182    }
183
184    /// Loads the state from the data directory using explicit database options.
185    ///
186    /// The loaded state owns all store data structures and exposes subscription methods for
187    /// sequencer and replica tasks.
188    #[miden_instrument(
189        target = COMPONENT,
190        skip_all,
191    )]
192    pub async fn load_with_database_options(
193        data_path: &Path,
194        storage_options: StorageOptions,
195        database_options: DatabaseOptions,
196    ) -> Result<Self, StateInitializationError> {
197        let data_directory = DataDirectory::load(data_path.to_path_buf())
198            .map_err(StateInitializationError::DataDirectoryLoadError)?;
199
200        let block_store = Arc::new(
201            BlockStore::load(data_directory.block_store_dir())
202                .map_err(StateInitializationError::BlockStoreLoadError)?,
203        );
204
205        let database_filepath = data_directory.database_path();
206        let mut db = Db::load_with_pool_size(
207            database_filepath.clone(),
208            database_options.connection_pool_size,
209        )
210        .await
211        .map_err(StateInitializationError::DatabaseLoadError)?;
212
213        let blockchain = load_mmr(&mut db).await?;
214        let latest_block_num = blockchain.chain_tip().unwrap_or(BlockNumber::GENESIS);
215
216        #[cfg(feature = "rocksdb")]
217        let (account_storage_config, nullifier_storage_config, forest_storage_config) = (
218            storage_options.account_tree.into(),
219            storage_options.nullifier_tree.into(),
220            storage_options.account_state_forest.into(),
221        );
222        #[cfg(not(feature = "rocksdb"))]
223        let (account_storage_config, nullifier_storage_config, forest_storage_config) = {
224            let _ = &storage_options;
225            ((), (), ())
226        };
227        let account_storage =
228            TreeStorage::create(data_path, &account_storage_config, ACCOUNT_TREE_STORAGE_DIR)?;
229        let account_tree = account_storage.load_account_tree(&mut db).await?;
230
231        let nullifier_storage =
232            TreeStorage::create(data_path, &nullifier_storage_config, NULLIFIER_TREE_STORAGE_DIR)?;
233        let nullifier_tree = nullifier_storage.load_nullifier_tree(&mut db).await?;
234
235        // Verify that tree roots match the expected roots from the database. This catches any
236        // divergence between persistent storage and the database caused by corruption or incomplete
237        // shutdown.
238        verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &mut db).await?;
239
240        let account_tree = AccountTreeWithHistory::new(account_tree, latest_block_num);
241
242        let forest_backend = AccountStateForestBackend::create(
243            data_path,
244            &forest_storage_config,
245            ACCOUNT_STATE_FOREST_STORAGE_DIR,
246        )?;
247        let forest = forest_backend.load_account_state_forest(&mut db, latest_block_num).await?;
248        verify_account_state_forest_consistency(&forest, &mut db).await?;
249
250        let inner = RwLock::new(InnerState { nullifier_tree, blockchain, account_tree });
251
252        let forest = RwLock::new(forest);
253        let writer = Mutex::new(());
254        let db = Arc::new(db);
255
256        // Initialize the proven tip from the block store.
257        let proven_tip_init = block_store
258            .load_proven_tip()
259            .map_err(StateInitializationError::ProvenTipLoadError)?;
260        let (proven_tip, _rx) = ProvenTipWriter::new(proven_tip_init);
261
262        // Committed-tip watch: fires after each successful apply_block.
263        let (committed_tip_tx, _rx) = watch::channel(latest_block_num);
264
265        Ok(Self {
266            data_directory: data_path.to_path_buf(),
267            db,
268            block_store,
269            inner,
270            forest,
271            writer,
272            proven_tip,
273            committed_tip_tx,
274            block_cache: BlockCache::new(BLOCK_CACHE_CAPACITY),
275            proof_cache: ProofCache::new(PROOF_CACHE_CAPACITY),
276        })
277    }
278
279    /// Returns a watch receiver that wakes every time a new block is committed.
280    pub fn subscribe_committed_tip(&self) -> watch::Receiver<BlockNumber> {
281        self.committed_tip_tx.subscribe()
282    }
283
284    /// Loads serialized block proving inputs from the block store.
285    pub async fn load_proving_inputs(
286        &self,
287        block_num: BlockNumber,
288    ) -> std::io::Result<Option<Vec<u8>>> {
289        self.block_store.load_proving_inputs(block_num).await
290    }
291
292    /// Returns a watch receiver that wakes every time the proven-in-sequence tip advances.
293    pub fn subscribe_proven_tip(&self) -> watch::Receiver<BlockNumber> {
294        self.proven_tip.subscribe()
295    }
296
297    // HELPER FUNCTIONS TO AVOID BLOCKING CALLS IN ASYNC CONTEXT
298    // --------------------------------------------------------------------------------------------
299
300    /// Runs a synchronous read-only operation over the inner state on Tokio's blocking path.
301    ///
302    /// The account and nullifier trees may be backed by `RocksDB`, so tree access must not run on
303    /// an async worker thread directly. This helper preserves the current tracing span while
304    /// moving the blocking lock acquisition and closure body into `block_in_place`.
305    fn with_inner_read_blocking<R>(&self, f: impl FnOnce(&InnerState<TreeStorage>) -> R) -> R {
306        let span = Span::current();
307        tokio::task::block_in_place(|| {
308            span.in_scope(|| {
309                let inner = self.inner.blocking_read();
310                f(&inner)
311            })
312        })
313    }
314
315    /// Runs a synchronous mutable operation while holding both in-memory state write locks.
316    ///
317    /// Locks are always acquired in inner-state then forest order. Holding both across the database
318    /// commit window prevents readers from observing canonical tree state from one block and
319    /// account-state forest data from another.
320    fn with_inner_and_forest_write_blocking<R>(
321        &self,
322        f: impl FnOnce(
323            &mut InnerState<TreeStorage>,
324            &mut AccountStateForest<AccountStateForestBackend>,
325        ) -> R,
326    ) -> R {
327        let span = Span::current();
328        tokio::task::block_in_place(|| {
329            span.in_scope(|| {
330                let mut inner = self.inner.blocking_write();
331                let mut forest = self.forest.blocking_write();
332                f(&mut inner, &mut forest)
333            })
334        })
335    }
336
337    /// Runs a synchronous read-only operation over the account state forest on Tokio's blocking
338    /// path.
339    ///
340    /// The forest may be backed by `RocksDB`, so accesses to the underlying `LargeSmtForest` must
341    /// not run directly on an async worker thread.
342    fn with_forest_read_blocking<R>(
343        &self,
344        f: impl FnOnce(&AccountStateForest<AccountStateForestBackend>) -> R,
345    ) -> R {
346        let span = Span::current();
347        tokio::task::block_in_place(|| {
348            span.in_scope(|| {
349                let forest = self.forest.blocking_read();
350                f(&forest)
351            })
352        })
353    }
354
355    // STATE ACCESSORS
356    // --------------------------------------------------------------------------------------------
357
358    /// Queries a [BlockHeader] from the database, and returns it alongside its inclusion proof.
359    ///
360    /// If [None] is given as the value of `block_num`, the data for the latest [BlockHeader] is
361    /// returned.
362    #[miden_instrument(
363        level = "debug",
364        target = COMPONENT,
365        skip_all,
366        err,
367    )]
368    pub async fn get_block_header(
369        &self,
370        block_num: Option<BlockNumber>,
371        include_mmr_proof: bool,
372    ) -> Result<(Option<BlockHeader>, Option<MmrProof>), GetBlockHeaderError> {
373        let block_header = self.db.select_block_header_by_block_num(block_num).await?;
374        if let Some(header) = block_header {
375            let mmr_proof = if include_mmr_proof {
376                let inner = self.inner.read().await;
377                let mmr_proof = inner.blockchain.open(header.block_num())?;
378                Some(mmr_proof)
379            } else {
380                None
381            };
382            Ok((Some(header), mmr_proof))
383        } else {
384            Ok((None, None))
385        }
386    }
387
388    /// Queries a list of notes from the database.
389    ///
390    /// If the provided list of [`NoteId`] given is empty or no note matches the provided
391    /// [`NoteId`] an empty list is returned.
392    pub async fn get_notes_by_id(
393        &self,
394        note_ids: Vec<NoteId>,
395    ) -> Result<Vec<NoteRecord>, DatabaseError> {
396        self.db.select_notes_by_id(note_ids).await
397    }
398
399    /// Fetches the inputs for a transaction batch from the database.
400    ///
401    /// ## Inputs
402    ///
403    /// The function takes as input:
404    /// - The tx reference blocks are the set of blocks referenced by transactions in the batch.
405    /// - The unauthenticated note commitments are the set of commitments of unauthenticated notes
406    ///   consumed by all transactions in the batch. For these notes, we attempt to find inclusion
407    ///   proofs. Not all notes will exist in the DB necessarily, as some notes can be created and
408    ///   consumed within the same batch.
409    ///
410    /// ## Outputs
411    ///
412    /// The function will return:
413    /// - A block inclusion proof for all tx reference blocks and for all blocks which are
414    ///   referenced by a note inclusion proof.
415    /// - Note inclusion proofs for all notes that were found in the DB.
416    /// - The block header that the batch should reference, i.e. the latest known block.
417    pub async fn get_batch_inputs(
418        &self,
419        tx_reference_blocks: BTreeSet<BlockNumber>,
420        unauthenticated_note_commitments: BTreeSet<Word>,
421    ) -> Result<BatchInputs, GetBatchInputsError> {
422        if tx_reference_blocks.is_empty() {
423            return Err(GetBatchInputsError::TransactionBlockReferencesEmpty);
424        }
425
426        // First we grab note inclusion proofs for the known notes. These proofs only prove that the
427        // note was included in a given block. We then also need to prove that each of those blocks
428        // is included in the chain.
429        let note_proofs = self
430            .db
431            .select_note_inclusion_proofs(unauthenticated_note_commitments)
432            .await
433            .map_err(GetBatchInputsError::SelectNoteInclusionProofError)?;
434
435        // The set of blocks that the notes are included in.
436        let note_blocks = note_proofs.values().map(|proof| proof.location().block_num());
437
438        // Collect all blocks we need to query without duplicates, which is:
439        // - all blocks for which we need to prove note inclusion.
440        // - all blocks referenced by transactions in the batch.
441        let mut blocks: BTreeSet<BlockNumber> = tx_reference_blocks;
442        blocks.extend(note_blocks);
443
444        // Scoped block to automatically drop the read lock guard as soon as we're done. We also
445        // avoid accessing the db in the block as this would delay dropping the guard.
446        let (batch_reference_block, partial_mmr) = {
447            let inner_state = self.inner.read().await;
448
449            let latest_block_num = inner_state.latest_block_num();
450
451            let highest_block_num =
452                *blocks.last().expect("we should have checked for empty block references");
453            if highest_block_num > latest_block_num {
454                return Err(GetBatchInputsError::UnknownTransactionBlockReference {
455                    highest_block_num,
456                    latest_block_num,
457                });
458            }
459
460            // Remove the latest block from the to-be-tracked blocks as it will be the reference
461            // block for the batch itself and thus added to the MMR within the batch kernel, so
462            // there is no need to prove its inclusion.
463            blocks.remove(&latest_block_num);
464
465            // SAFETY:
466            // - The latest block num was retrieved from the inner blockchain from which we will
467            //   also retrieve the proofs, so it is guaranteed to exist in that chain.
468            // - We have checked that no block number in the blocks set is greater than latest block
469            //   number *and* latest block num was removed from the set. Therefore only block
470            //   numbers smaller than latest block num remain in the set. Therefore all the block
471            //   numbers are guaranteed to exist in the chain state at latest block num.
472            let partial_mmr = inner_state
473                .blockchain
474                .partial_mmr_from_blocks(&blocks, latest_block_num)
475                .expect("latest block num should exist and all blocks in set should be < than latest block");
476
477            (latest_block_num, partial_mmr)
478        };
479
480        // Fetch the reference block of the batch as part of this query, so we can avoid looking it
481        // up in a separate DB access.
482        let mut headers = self
483            .db
484            .select_block_headers(blocks.into_iter().chain(std::iter::once(batch_reference_block)))
485            .await
486            .map_err(GetBatchInputsError::SelectBlockHeaderError)?;
487
488        // Find and remove the batch reference block as we don't want to add it to the chain MMR.
489        let header_index = headers
490            .iter()
491            .enumerate()
492            .find_map(|(index, header)| {
493                (header.block_num() == batch_reference_block).then_some(index)
494            })
495            .expect("DB should have returned the header of the batch reference block");
496
497        // The order doesn't matter for PartialBlockchain::new, so swap remove is fine.
498        let batch_reference_block_header = headers.swap_remove(header_index);
499
500        // SAFETY: This should not error because:
501        // - we're passing exactly the block headers that we've added to the partial MMR,
502        // - so none of the block headers block numbers should exceed the chain length of the
503        //   partial MMR,
504        // - and we've added blocks to a BTreeSet, so there can be no duplicates.
505        //
506        // We construct headers and partial MMR in concert, so they are consistent. This is why we
507        // can call the unchecked constructor.
508        let partial_block_chain = PartialBlockchain::new_unchecked(partial_mmr, headers)
509            .expect("partial mmr and block headers should be consistent");
510
511        Ok(BatchInputs {
512            batch_reference_block_header,
513            note_proofs,
514            partial_block_chain,
515        })
516    }
517
518    /// Returns data needed by the block producer to construct and prove the next block.
519    pub async fn get_block_inputs(
520        &self,
521        account_ids: Vec<AccountId>,
522        nullifiers: Vec<Nullifier>,
523        unauthenticated_note_commitments: BTreeSet<Word>,
524        reference_blocks: BTreeSet<BlockNumber>,
525    ) -> Result<BlockInputs, GetBlockInputsError> {
526        // Get the note inclusion proofs from the DB. We do this first so we have to acquire the
527        // lock to the state just once. There we need the reference blocks of the note proofs to get
528        // their authentication paths in the chain MMR.
529        let unauthenticated_note_proofs = self
530            .db
531            .select_note_inclusion_proofs(unauthenticated_note_commitments)
532            .await
533            .map_err(GetBlockInputsError::SelectNoteInclusionProofError)?;
534
535        // The set of blocks that the notes are included in.
536        let note_proof_reference_blocks =
537            unauthenticated_note_proofs.values().map(|proof| proof.location().block_num());
538
539        // Collect all blocks we need to prove inclusion for, without duplicates.
540        let mut blocks = reference_blocks;
541        blocks.extend(note_proof_reference_blocks);
542
543        let (latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr) =
544            self.get_block_inputs_witnesses(&mut blocks, &account_ids, &nullifiers)?;
545
546        // Fetch the block headers for all blocks in the partial MMR plus the latest one which will
547        // be used as the previous block header of the block being built.
548        let mut headers = self
549            .db
550            .select_block_headers(blocks.into_iter().chain(std::iter::once(latest_block_number)))
551            .await
552            .map_err(GetBlockInputsError::SelectBlockHeaderError)?;
553
554        // Find and remove the latest block as we must not add it to the chain MMR, since it is not
555        // yet in the chain.
556        let latest_block_header_index = headers
557            .iter()
558            .enumerate()
559            .find_map(|(index, header)| {
560                (header.block_num() == latest_block_number).then_some(index)
561            })
562            .expect("DB should have returned the header of the latest block header");
563
564        // The order doesn't matter for PartialBlockchain::new, so swap remove is fine.
565        let latest_block_header = headers.swap_remove(latest_block_header_index);
566
567        // SAFETY: This should not error because:
568        // - we're passing exactly the block headers that we've added to the partial MMR,
569        // - so none of the block header's block numbers should exceed the chain length of the
570        //   partial MMR,
571        // - and we've added blocks to a BTreeSet, so there can be no duplicates.
572        //
573        // We construct headers and partial MMR in concert, so they are consistent. This is why we
574        // can call the unchecked constructor.
575        let partial_block_chain = PartialBlockchain::new_unchecked(partial_mmr, headers)
576            .expect("partial mmr and block headers should be consistent");
577
578        Ok(BlockInputs::new(
579            latest_block_header,
580            partial_block_chain,
581            account_witnesses,
582            nullifier_witnesses,
583            unauthenticated_note_proofs,
584        ))
585    }
586
587    /// Get account and nullifier witnesses for the requested account IDs and nullifier as well as
588    /// the [`PartialMmr`] for the given blocks. The MMR won't contain the latest block and its
589    /// number is removed from `blocks` and returned separately.
590    ///
591    /// This method acquires the lock to the inner state and does not access the DB so we release
592    /// the lock asap.
593    fn get_block_inputs_witnesses(
594        &self,
595        blocks: &mut BTreeSet<BlockNumber>,
596        account_ids: &[AccountId],
597        nullifiers: &[Nullifier],
598    ) -> Result<BlockInputWitnesses, GetBlockInputsError> {
599        self.with_inner_read_blocking(|inner| {
600            let latest_block_number = inner.latest_block_num();
601
602            // If `blocks` is empty, use the latest block number which will never trigger the error.
603            let highest_block_number = blocks.last().copied().unwrap_or(latest_block_number);
604            if highest_block_number > latest_block_number {
605                return Err(GetBlockInputsError::UnknownBatchBlockReference {
606                    highest_block_number,
607                    latest_block_number,
608                });
609            }
610
611            // The latest block is not yet in the chain MMR, so we can't (and don't need to) prove
612            // its inclusion in the chain.
613            blocks.remove(&latest_block_number);
614
615            // Fetch the partial MMR at the state of the latest block with authentication paths for
616            // the provided set of blocks.
617            //
618            // SAFETY:
619            // - The latest block num was retrieved from the inner blockchain from which we will
620            //   also retrieve the proofs, so it is guaranteed to exist in that chain.
621            // - We have checked that no block number in the blocks set is greater than latest block
622            //   number *and* latest block num was removed from the set. Therefore only block
623            //   numbers smaller than latest block num remain in the set. Therefore all the block
624            //   numbers are guaranteed to exist in the chain state at latest block num.
625            let partial_mmr =
626                inner.blockchain.partial_mmr_from_blocks(blocks, latest_block_number).expect(
627                    "latest block num should exist and all blocks in set should be < than latest block",
628                );
629
630            // Fetch witnesses for all accounts.
631            let account_witnesses = account_ids
632                .iter()
633                .copied()
634                .map(|account_id| (account_id, inner.account_tree.open_latest(account_id)))
635                .collect::<BTreeMap<AccountId, AccountWitness>>();
636
637            // Fetch witnesses for all nullifiers. We don't check whether the nullifiers are spent
638            // or not as this is done as part of proposing the block.
639            let nullifier_witnesses: BTreeMap<Nullifier, NullifierWitness> = nullifiers
640                .iter()
641                .copied()
642                .map(|nullifier| (nullifier, inner.nullifier_tree.open(&nullifier)))
643                .collect();
644
645            Ok((latest_block_number, account_witnesses, nullifier_witnesses, partial_mmr))
646        })
647    }
648
649    /// Returns data needed by the block producer to verify transactions validity.
650    #[miden_instrument(
651        target = COMPONENT,
652        skip_all,
653        fields(
654            account.id=%account_id,
655            nullifiers = %format_array(nullifiers),
656        ),
657    )]
658    pub async fn get_transaction_inputs(
659        &self,
660        account_id: AccountId,
661        nullifiers: &[Nullifier],
662        unauthenticated_note_commitments: Vec<Word>,
663    ) -> Result<TransactionInputs, DatabaseError> {
664        let tree_inputs = self.with_inner_read_blocking(|inner| {
665            let account_commitment = inner.account_tree.get_latest_commitment(account_id);
666
667            let new_account_id_prefix_is_unique = if account_commitment.is_empty() {
668                Some(!inner.account_tree.contains_account_id_prefix_in_latest(account_id.prefix()))
669            } else {
670                None
671            };
672
673            // Non-unique account Id prefixes for new accounts are not allowed.
674            if let Some(false) = new_account_id_prefix_is_unique {
675                return Err(TransactionInputs {
676                    new_account_id_prefix_is_unique,
677                    ..Default::default()
678                });
679            }
680
681            let nullifiers = nullifiers
682                .iter()
683                .map(|nullifier| NullifierInfo {
684                    nullifier: *nullifier,
685                    block_num: inner.nullifier_tree.get_block_num(nullifier).unwrap_or_default(),
686                })
687                .collect();
688
689            Ok((account_commitment, nullifiers, new_account_id_prefix_is_unique))
690        });
691        let (account_commitment, nullifiers, new_account_id_prefix_is_unique) = match tree_inputs {
692            Ok(inputs) => inputs,
693            Err(inputs) => return Ok(inputs),
694        };
695
696        let found_unauthenticated_notes = self
697            .db
698            .select_existing_note_commitments(unauthenticated_note_commitments)
699            .await?;
700
701        Ok(TransactionInputs {
702            account_commitment,
703            nullifiers,
704            found_unauthenticated_notes,
705            new_account_id_prefix_is_unique,
706        })
707    }
708
709    /// Filters `account_ids` down to the subset classified as network accounts.
710    pub async fn filter_network_accounts(
711        &self,
712        account_ids: &[AccountId],
713    ) -> Result<HashSet<AccountId>, DatabaseError> {
714        self.db.select_network_accounts_subset(account_ids.to_vec()).await
715    }
716
717    /// Returns the effective chain tip for the given finality level.
718    ///
719    /// - [`Finality::Committed`]: returns the latest committed block number (from in-memory MMR).
720    /// - [`Finality::Proven`]: returns the latest proven-in-sequence block number (cached via watch
721    ///   channel, updated by the proof scheduler).
722    pub async fn chain_tip(&self, finality: Finality) -> BlockNumber {
723        match finality {
724            Finality::Committed => self
725                .inner
726                .read()
727                .instrument(tracing::info_span!("acquire_inner"))
728                .await
729                .latest_block_num(),
730            Finality::Proven => self.proven_tip.read(),
731        }
732    }
733
734    /// Loads a block from the in-memory replica cache or block store. Return `Ok(None)` if the
735    /// block is not found.
736    pub async fn load_block(
737        &self,
738        block_num: BlockNumber,
739    ) -> Result<Option<Vec<u8>>, DatabaseError> {
740        if block_num > self.chain_tip(Finality::Committed).await {
741            return Ok(None);
742        }
743        if let Some(block) = self.block_cache.get(block_num) {
744            return Ok(Some(block.block_bytes().to_vec()));
745        }
746        self.block_store.load_block(block_num).await.map_err(Into::into)
747    }
748
749    /// Loads a block proof from the in-memory replica cache or block store. Returns `Ok(None)` if
750    /// the proof is not found.
751    pub async fn load_proof(
752        &self,
753        block_num: BlockNumber,
754    ) -> Result<Option<Vec<u8>>, DatabaseError> {
755        if block_num > self.chain_tip(Finality::Proven).await {
756            return Ok(None);
757        }
758        if let Some(proof) = self.proof_cache.get(block_num) {
759            return Ok(Some(proof.proof_bytes().to_vec()));
760        }
761        self.block_store.load_proof(block_num).await.map_err(Into::into)
762    }
763
764    /// Returns the script for a note by its root.
765    pub async fn get_note_script_by_root(
766        &self,
767        root: Word,
768    ) -> Result<Option<NoteScript>, DatabaseError> {
769        self.db.select_note_script_by_root(root).await
770    }
771}