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