Skip to main content

miden_node_store/state/view/
mod.rs

1//! Request-scoped, consistent read view of the store.
2//!
3//! All store reads go through [`StateView`]: it pins one state snapshot for its whole lifetime,
4//! and every block-scoped database query it exposes is bounded by that snapshot's height (via the
5//! [`scoped`] proof types). This makes it impossible to implement a read whose tree and database
6//! halves observe different chain tips — mid-apply, the database may already contain rows for a
7//! block the snapshot cannot prove yet. The only deliberately unscoped reads are the
8//! content-addressed note and protocol-configuration lookups and the network-account
9//! classification.
10//!
11//! The submodules hold the read endpoints, all `impl StateView`; the snapshot internals
12//! ([`StateSnapshot`]) are only visible within this module tree, so no other part of the store
13//! can reach the trees directly.
14
15use std::ops::RangeInclusive;
16use std::sync::Arc;
17
18use miden_node_tracing::Span;
19use miden_protocol::block::{BlockNumber, Blockchain};
20
21use crate::account_state_forest::{AccountStateForest, AccountStateForestBackendReader};
22use crate::db::Db;
23use crate::errors::RangeBeyondTip;
24use crate::state::State;
25
26mod scoped;
27pub use scoped::{ScopedBlockNum, ScopedBlockRange};
28
29mod snapshot;
30pub(in crate::state) use snapshot::{
31    PublishedGenerations,
32    SNAPSHOTS_LIVE_WARN_THRESHOLD,
33    SnapshotGuard,
34    StateSnapshot,
35};
36
37mod account;
38mod block;
39mod inclusion_proofs;
40mod note;
41mod protocol_config;
42mod state_witnesses;
43pub use state_witnesses::StateWitnesses;
44mod sync;
45
46mod transaction_inputs;
47pub use transaction_inputs::TransactionInputs;
48
49// STATE VIEW
50// ================================================================================================
51
52/// A consistent read view of the store, pinned at its snapshot's block height.
53///
54/// Obtained from [`State::view`]; create one per request and drop it when the request completes.
55/// Holding a view pins a snapshot generation (and thereby the `RocksDB` snapshots backing the
56/// trees), so it must not be stored in long-lived structs; leaked or slow readers are reported by
57/// the store's snapshot-lifetime warnings.
58///
59/// Reads that are technically not block-scoped (for example, immutable content-addressed data)
60/// also live here so that every read path flows through one type.
61pub struct StateView {
62    snapshot: Arc<StateSnapshot>,
63    db: Arc<Db>,
64}
65
66impl State {
67    /// Returns a read view pinned at the current chain tip (wait-free, no lock required).
68    ///
69    /// The view is frozen: it is unaffected if the writer publishes a new snapshot while it is
70    /// held.
71    ///
72    /// Use this only for one single-expression read (`state.view().get_account(..)`),
73    /// where the temporary view drops at the end of the statement. Two `view()` calls observe
74    /// potentially *different* snapshots — a block can commit between them — so reads that must
75    /// be mutually consistent (e.g. a query and the tip it was served at) must share one view via
76    /// [`Self::with_view`]. Binding a view to a variable is also discouraged: it keeps the
77    /// snapshot generation pinned until the end of the scope.
78    pub fn view(&self) -> StateView {
79        StateView {
80            snapshot: self.latest_snapshot.load_full(),
81            db: Arc::clone(&self.db),
82        }
83    }
84
85    /// Runs a read operation over a view pinned at the current chain tip, dropping the view — and
86    /// releasing its snapshot generation — as soon as the operation completes.
87    ///
88    /// This is the required form whenever multiple reads must observe the *same* snapshot: the
89    /// closure's view is one consistent generation, whereas consecutive [`Self::view`] calls may
90    /// straddle a commit. The typical case is pairing a query with [`StateView::tip`] so a
91    /// response reports exactly the height it was served at.
92    ///
93    /// The closure receives a shared reference, so the view cannot escape the call, and the
94    /// snapshot is not pinned through unrelated work that follows (e.g. response encoding). A
95    /// *single* read doesn't need it — `state.view().get_account(..)` drops its temporary view at
96    /// the end of the statement.
97    ///
98    /// Work in the closure should be kept to low-complexity compute over the view, ideally with no
99    /// I/O and no other `.await` points. Anything slower holds the pinned snapshot, and therefore
100    /// its underlying `RocksDB` snapshot, for as long as it runs. The snapshot's lifetime is logged
101    /// as a warning if held too long, but that is a backstop, not a substitute for keeping closures
102    /// short.
103    pub async fn with_view<R>(&self, f: impl AsyncFnOnce(&StateView) -> R) -> R {
104        let view = self.view();
105        f(&view).await
106    }
107}
108
109impl StateView {
110    /// Returns this view's tip as a scoped block number for tip-bounded database queries.
111    pub fn tip(&self) -> ScopedBlockNum {
112        ScopedBlockNum::new(self.snapshot.latest_block_num())
113    }
114
115    /// Returns the pinned snapshot's blockchain MMR.
116    ///
117    /// The MMR is the only part of the snapshot that is purely in-memory and therefore safe to
118    /// access directly on an async worker thread. The account and nullifier trees may be backed
119    /// by `RocksDB` and are deliberately not reachable here — they must be accessed through
120    /// [`Self::with_inner_read_blocking`].
121    fn blockchain(&self) -> &Blockchain {
122        &self.snapshot.blockchain
123    }
124
125    /// Validates that `block_num` does not exceed this view's chain tip, returning the scoped block
126    /// number required by block-bounded database queries.
127    fn scope_block(&self, block_num: BlockNumber) -> Option<ScopedBlockNum> {
128        (block_num <= *self.tip()).then(|| ScopedBlockNum::new(block_num))
129    }
130
131    /// Validates that `range` does not extend beyond this view's chain tip, returning the scoped
132    /// range required by range-bounded database queries.
133    ///
134    /// Every range-scoped read on this type calls this before touching the database, so callers
135    /// never need to pre-validate ranges themselves.
136    fn scope_range(
137        &self,
138        range: RangeInclusive<BlockNumber>,
139    ) -> Result<ScopedBlockRange, RangeBeyondTip> {
140        let tip = *self.tip();
141        if *range.end() > tip {
142            return Err(RangeBeyondTip { chain_tip: tip, block_to: *range.end() });
143        }
144        Ok(ScopedBlockRange::new(range))
145    }
146
147    /// Runs a synchronous read-only operation over the pinned state snapshot on Tokio's blocking
148    /// path.
149    ///
150    /// The account and nullifier trees may be backed by `RocksDB`, so tree access must not run on
151    /// an async worker thread directly. This helper preserves the current tracing span while
152    /// moving the closure body into `block_in_place`.
153    fn with_inner_read_blocking<R>(&self, f: impl FnOnce(&StateSnapshot) -> R) -> R {
154        let span = Span::current();
155        tokio::task::block_in_place(|| span.in_scope(|| f(&self.snapshot)))
156    }
157
158    /// Runs a synchronous read-only operation over the account state forest snapshot on Tokio's
159    /// blocking path.
160    ///
161    /// See [`Self::with_inner_read_blocking`] for why this uses `block_in_place`.
162    fn with_forest_read_blocking<R>(
163        &self,
164        f: impl FnOnce(&AccountStateForest<AccountStateForestBackendReader>) -> R,
165    ) -> R {
166        self.with_inner_read_blocking(|snapshot| f(&snapshot.forest))
167    }
168}