Skip to main content

miden_node_store/state/view/account/
mod.rs

1use std::collections::HashSet;
2
3use miden_node_proto::domain::account::{
4    AccountDetailRequest,
5    AccountDetails,
6    AccountStorageDetails,
7    AccountStorageMapDetails,
8    AccountStorageRequest,
9    AccountVaultDetails,
10    GetAccountRequest,
11    GetAccountResponse,
12    SlotData,
13    StorageMapEntries,
14    StorageMapRequest,
15};
16use miden_node_tracing::miden_instrument;
17use miden_protocol::account::{AccountId, AccountStorageHeader, StorageSlotName, StorageSlotType};
18use miden_protocol::block::BlockNumber;
19use miden_protocol::block::account_tree::AccountWitness;
20
21use super::{ScopedBlockNum, StateView};
22use crate::COMPONENT;
23use crate::account_state_forest::AccountStorageMapResult;
24use crate::errors::{DatabaseError, GetAccountError};
25
26impl StateView {
27    /// Returns an account witness and optionally account details at a specific block.
28    ///
29    /// The witness is a Merkle proof of inclusion in the account tree, proving the account's
30    /// state commitment. If `details` is requested, the method also returns the account's code,
31    /// vault assets, and storage data. Account details are only available for public accounts.
32    ///
33    /// If `block_num` is provided, returns the state at that historical block; otherwise, returns
34    /// the latest state. Note that historical states are only available for recent blocks close
35    /// to the chain tip.
36    #[miden_instrument(
37        target = COMPONENT,
38    )]
39    pub async fn get_account(
40        &self,
41        get_account_request: GetAccountRequest,
42    ) -> Result<GetAccountResponse, GetAccountError> {
43        let GetAccountRequest { block_num, account_id, details } = get_account_request;
44
45        if details.is_some() && !account_id.is_public() {
46            return Err(GetAccountError::AccountNotPublic(account_id));
47        }
48
49        let (scoped_block, witness) = self.get_account_witness(block_num, account_id).await?;
50
51        let details = if let Some(request) = details {
52            Some(
53                self.fetch_public_account_details(account_id, scoped_block, &witness, request)
54                    .await?,
55            )
56        } else {
57            None
58        };
59
60        Ok(GetAccountResponse {
61            block_num: *scoped_block,
62            witness,
63            details,
64        })
65    }
66
67    /// Returns an account witness (Merkle proof of inclusion in the account tree) together with
68    /// the resolved block as a scoped block number.
69    ///
70    /// If `block_num` is provided, returns the witness at that historical block; otherwise,
71    /// returns the witness at the latest block. The tree resolution doubles as the tip
72    /// validation, so the returned block is ready for block-bounded database queries.
73    #[miden_instrument(
74        target = COMPONENT,
75    )]
76    async fn get_account_witness(
77        &self,
78        block_num: Option<BlockNumber>,
79        account_id: AccountId,
80    ) -> Result<(ScopedBlockNum, AccountWitness), GetAccountError> {
81        // Historical query: scope the requested block up front — a block beyond this view's tip is
82        // unknown, so a missing tree entry below can only mean it was pruned.
83        if let Some(requested_block) = block_num {
84            let scoped_block = self
85                .scope_block(requested_block)
86                .ok_or(GetAccountError::UnknownBlock(requested_block))?;
87            let witness = self
88                .with_inner_read_blocking(|inner_state| {
89                    inner_state.account_tree.open_at(account_id, *scoped_block)
90                })
91                .ok_or(GetAccountError::BlockPruned(*scoped_block))?;
92            Ok((scoped_block, witness))
93        } else {
94            // Latest query: the tree's latest state is the view's tip.
95            let witness = self.with_inner_read_blocking(|inner_state| {
96                inner_state.account_tree.open_latest(account_id)
97            });
98            Ok((self.tip(), witness))
99        }
100    }
101
102    /// Returns storage map details from the forest for a specific account and storage slot.
103    ///
104    /// The forest can only be used if all hashed keys in the storage map are known in the
105    /// reverse-key LRU cache. If any hashed key is unknown, the method returns `Ok(None)` to signal
106    /// that the caller should fall back to reconstructing the storage map details from the
107    /// database.
108    #[miden_instrument(
109        target = COMPONENT,
110    )]
111    fn get_storage_map_details_from_forest(
112        &self,
113        account_id: AccountId,
114        slot_name: &StorageSlotName,
115        block_num: ScopedBlockNum,
116    ) -> Result<Option<AccountStorageMapDetails>, DatabaseError> {
117        self.with_forest_read_blocking(|forest| {
118            match forest
119                .get_storage_map_details_for_all_entries(account_id, slot_name.clone(), *block_num)
120                .map_err(DatabaseError::MerkleError)?
121            {
122                AccountStorageMapResult::NotFound => Err(DatabaseError::StorageRootNotFound {
123                    account_id,
124                    slot_name: slot_name.to_string(),
125                    block_num: *block_num,
126                }),
127                AccountStorageMapResult::Details(details) => Ok(Some(details)),
128                AccountStorageMapResult::CannotReconstructKeysFromCache => Ok(None),
129            }
130        })
131    }
132
133    /// Returns vault details by reconstructing the vault from the database.
134    async fn reconstruct_vault_details_from_db(
135        &self,
136        account_id: AccountId,
137        block_num: ScopedBlockNum,
138    ) -> Result<AccountVaultDetails, DatabaseError> {
139        let assets = self.db.select_vault_at_block(account_id, block_num).await?;
140
141        if assets.len() > AccountVaultDetails::MAX_RETURN_ENTRIES {
142            return Ok(AccountVaultDetails::LimitExceeded);
143        }
144
145        let keys = assets.iter().map(miden_protocol::asset::Asset::id);
146
147        // The reverse-key caches are shared between the writer and all snapshots, so caching via
148        // the current snapshot's forest is visible everywhere.
149        self.with_forest_read_blocking(|forest| {
150            forest
151                .vault_key_cache
152                .put_many(keys.into_iter().map(|raw_key| (raw_key.hash(), raw_key)));
153        });
154
155        Ok(AccountVaultDetails::from_assets(assets))
156    }
157
158    /// Returns storage map details by reconstructing the storage map from the database.
159    async fn reconstruct_storage_map_details_from_db(
160        &self,
161        account_id: AccountId,
162        slot_name: StorageSlotName,
163        block_num: ScopedBlockNum,
164    ) -> Result<AccountStorageMapDetails, DatabaseError> {
165        let details = self
166            .db
167            .reconstruct_storage_map_from_db(
168                account_id,
169                slot_name,
170                block_num,
171                Some(AccountStorageMapDetails::MAX_RETURN_ENTRIES),
172            )
173            .await?;
174
175        if let StorageMapEntries::AllEntries(entries) = &details.entries {
176            self.with_forest_read_blocking(|forest| {
177                forest.cache_storage_map_keys(entries.iter().map(|(raw_key, _)| *raw_key));
178            });
179        }
180
181        Ok(details)
182    }
183
184    /// Fetches the account details (code, vault, storage) for a public account at the specified
185    /// block.
186    ///
187    /// This method queries the database to fetch the account state and processes the detail
188    /// request to return only the requested information.
189    ///
190    /// For specific key queries (`SlotData::MapKeys`), the forest is used to provide SMT proofs.
191    /// Returns an error if the forest doesn't have data for the requested slot.
192    /// All-entries queries (`SlotData::All`) use the forest when all hashed keys are known in the
193    /// reverse-key LRU cache, otherwise they fall back to database reconstruction.
194    #[miden_instrument(
195        target = COMPONENT,
196    )]
197    async fn fetch_public_account_details(
198        &self,
199        account_id: AccountId,
200        scoped_block: ScopedBlockNum,
201        witness: &AccountWitness,
202        detail_request: AccountDetailRequest,
203    ) -> Result<AccountDetails, GetAccountError> {
204        let AccountDetailRequest {
205            code_commitment,
206            asset_vault_commitment,
207            storage_request,
208        } = detail_request;
209
210        if !account_id.is_public() {
211            return Err(GetAccountError::AccountNotPublic(account_id));
212        }
213
214        // Query account header and storage header together in a single DB call
215        let (account_header, storage_header) = self
216            .db
217            .select_account_header_with_storage_header_at_block(account_id, scoped_block)
218            .await?
219            .ok_or(GetAccountError::AccountNotFound(account_id, *scoped_block))?;
220
221        let should_apply_response_budget =
222            matches!(&storage_request, AccountStorageRequest::AllStorageMaps);
223        let storage_requests = expand_account_storage_request(storage_request, &storage_header);
224
225        let account_code = match code_commitment {
226            Some(commitment) if commitment == account_header.code_commitment() => None,
227            Some(_) => {
228                self.db
229                    .select_account_code_by_commitment(account_header.code_commitment())
230                    .await?
231            },
232            None => None,
233        };
234
235        // Query account state forest for vault details on commitment mismatch.
236        //
237        // The forest can only reconstruct the vault if all hashed vault keys are known in the
238        // reverse-key LRU cache. If any hashed key is unknown, the forest returns `None` and we
239        // fall back to reconstructing the vault details from the database.
240        let vault_details = match asset_vault_commitment {
241            Some(commitment) if commitment == account_header.vault_root() => {
242                AccountVaultDetails::empty()
243            },
244            Some(_) => {
245                let forest_details = self.with_forest_read_blocking(|forest| {
246                    forest.get_vault_details(account_id, *scoped_block).map_err(|err| {
247                        DatabaseError::DataCorrupted(format!(
248                            "failed to reconstruct vault for account {account_id} at block {}: {err}",
249                            *scoped_block,
250                        ))
251                    })
252                })?;
253
254                match forest_details {
255                    Some(details) => details,
256                    None => {
257                        self.reconstruct_vault_details_from_db(account_id, scoped_block).await?
258                    },
259                }
260            },
261            None => AccountVaultDetails::empty(),
262        };
263
264        // Split storage map requests into two categories:
265        // - slots with explicit keys (including proofs)
266        // - slots with "all entries"
267        let mut storage_map_details =
268            Vec::<AccountStorageMapDetails>::with_capacity(storage_requests.len());
269        let mut map_keys_requests = Vec::new();
270        let mut all_entries_requests = Vec::new();
271        let mut storage_request_slots = Vec::with_capacity(storage_requests.len());
272
273        for (index, StorageMapRequest { slot_name, slot_data }) in
274            storage_requests.into_iter().enumerate()
275        {
276            storage_request_slots.push(slot_name.clone());
277            match slot_data {
278                SlotData::MapKeys(keys) => {
279                    map_keys_requests.push((index, slot_name, keys));
280                },
281                SlotData::All => {
282                    all_entries_requests.push((index, slot_name));
283                },
284            }
285        }
286
287        let mut storage_map_details_by_index = vec![None; storage_request_slots.len()];
288
289        // Handle slots with explicit key requests
290        if !map_keys_requests.is_empty() {
291            self.with_forest_read_blocking(|forest| {
292                for (index, slot_name, keys) in map_keys_requests {
293                    let details = forest
294                        .get_storage_map_details_for_keys(
295                            account_id,
296                            slot_name.clone(),
297                            *scoped_block,
298                            keys,
299                        )
300                        .ok_or_else(|| DatabaseError::StorageRootNotFound {
301                            account_id,
302                            slot_name: slot_name.to_string(),
303                            block_num: *scoped_block,
304                        })?
305                        .map_err(DatabaseError::MerkleError)?;
306                    storage_map_details_by_index[index] = Some(details);
307                }
308                Ok::<(), DatabaseError>(())
309            })?;
310        }
311
312        // Handle slots with "all entries" requests
313        for (index, slot_name) in all_entries_requests {
314            let details = match self.get_storage_map_details_from_forest(
315                account_id,
316                &slot_name,
317                scoped_block,
318            )? {
319                Some(details) => details,
320                None => {
321                    self.reconstruct_storage_map_details_from_db(
322                        account_id,
323                        slot_name,
324                        scoped_block,
325                    )
326                    .await?
327                },
328            };
329            storage_map_details_by_index[index] = Some(details);
330        }
331
332        for (details, slot_name) in
333            storage_map_details_by_index.into_iter().zip(storage_request_slots.iter())
334        {
335            let details = details.ok_or_else(|| DatabaseError::StorageRootNotFound {
336                account_id,
337                slot_name: slot_name.to_string(),
338                block_num: *scoped_block,
339            })?;
340            storage_map_details.push(details);
341        }
342
343        // In case of an "all storage maps" request we have to be careful: even with the per-slot
344        // limit of [`AccountStorageMapDetails::MAX_RETURN_ENTRIES`] we might go over the response
345        // size limit. Here we make sure that we're within that limit by potentially truncating the
346        // response.
347        if should_apply_response_budget {
348            return Ok(apply_all_storage_maps_response_budget(
349                *scoped_block,
350                witness,
351                account_header,
352                account_code,
353                vault_details,
354                storage_header,
355                storage_map_details,
356                storage_request_slots,
357                MAX_ALL_STORAGE_MAPS_RESPONSE_PAYLOAD_WITH_BUDGET_RESERVED_FOR_LIMIT_EXCEEDED_SLOTS,
358            ));
359        }
360
361        Ok(AccountDetails {
362            account_header,
363            account_code,
364            vault_details,
365            storage_details: AccountStorageDetails {
366                header: storage_header,
367                map_details: storage_map_details,
368            },
369        })
370    }
371}
372
373// HELPERS
374// ================================================================================================
375
376/// Expand [`AccountStorageRequest`] to a vector of slot requests.
377fn expand_account_storage_request(
378    storage_request: AccountStorageRequest,
379    storage_header: &AccountStorageHeader,
380) -> Vec<StorageMapRequest> {
381    match storage_request {
382        AccountStorageRequest::None => Vec::new(),
383        AccountStorageRequest::Explicit(requests) => requests,
384        AccountStorageRequest::AllStorageMaps => storage_header
385            .slots()
386            .filter(|slot| slot.slot_type() == StorageSlotType::Map)
387            .map(|slot| StorageMapRequest {
388                slot_name: slot.name().clone(),
389                slot_data: SlotData::All,
390            })
391            .collect(),
392    }
393}
394
395mod response_budget;
396use response_budget::{
397    MAX_ALL_STORAGE_MAPS_RESPONSE_PAYLOAD_WITH_BUDGET_RESERVED_FOR_LIMIT_EXCEEDED_SLOTS,
398    apply_all_storage_maps_response_budget,
399};
400
401// NETWORK ACCOUNT CLASSIFICATION
402// ================================================================================================
403
404impl StateView {
405    /// Filters `account_ids` down to the subset classified as network accounts.
406    pub async fn filter_network_accounts(
407        &self,
408        account_ids: &[AccountId],
409    ) -> Result<HashSet<AccountId>, DatabaseError> {
410        self.db.filter_network_accounts(account_ids.to_vec()).await
411    }
412}