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