Skip to main content

miden_node_store/state/
account.rs

1use miden_node_proto::domain::account::{
2    AccountDetailRequest,
3    AccountDetails,
4    AccountRequest,
5    AccountResponse,
6    AccountStorageDetails,
7    AccountStorageMapDetails,
8    AccountStorageRequest,
9    AccountVaultDetails,
10    SlotData,
11    StorageMapEntries,
12    StorageMapRequest,
13};
14use miden_node_proto::generated as proto;
15use miden_node_proto::prost::Message as _;
16use miden_node_proto::prost::encoding::{encoded_len_varint, key_len};
17use miden_node_utils::limiter::MAX_RESPONSE_PAYLOAD_BYTES;
18use miden_node_utils::tracing::miden_instrument;
19use miden_protocol::account::{
20    AccountHeader,
21    AccountId,
22    AccountStorageHeader,
23    StorageSlotName,
24    StorageSlotType,
25};
26use miden_protocol::block::BlockNumber;
27use miden_protocol::block::account_tree::AccountWitness;
28use tracing::Instrument;
29
30use super::State;
31use crate::COMPONENT;
32use crate::account_state_forest::AccountStorageMapResult;
33use crate::errors::{DatabaseError, GetAccountError};
34
35impl State {
36    /// Returns an account witness and optionally account details at a specific block.
37    ///
38    /// The witness is a Merkle proof of inclusion in the account tree, proving the account's
39    /// state commitment. If `details` is requested, the method also returns the account's code,
40    /// vault assets, and storage data. Account details are only available for public accounts.
41    ///
42    /// If `block_num` is provided, returns the state at that historical block; otherwise, returns
43    /// the latest state. Note that historical states are only available for recent blocks close
44    /// to the chain tip.
45    #[miden_instrument(
46        target = COMPONENT,
47        skip_all,
48    )]
49    pub async fn get_account(
50        &self,
51        account_request: AccountRequest,
52    ) -> Result<AccountResponse, GetAccountError> {
53        let AccountRequest { block_num, account_id, details } = account_request;
54
55        if details.is_some() && !account_id.is_public() {
56            return Err(GetAccountError::AccountNotPublic(account_id));
57        }
58
59        let (block_num, witness) = self.get_account_witness(block_num, account_id).await?;
60
61        let details = if let Some(request) = details {
62            Some(
63                self.fetch_public_account_details(account_id, block_num, &witness, request)
64                    .await?,
65            )
66        } else {
67            None
68        };
69
70        Ok(AccountResponse { block_num, witness, details })
71    }
72
73    /// Returns an account witness (Merkle proof of inclusion in the account tree).
74    ///
75    /// If `block_num` is provided, returns the witness at that historical block;
76    /// otherwise, returns the witness at the latest block.
77    #[miden_instrument(
78        target = COMPONENT,
79        skip_all,
80    )]
81    async fn get_account_witness(
82        &self,
83        block_num: Option<BlockNumber>,
84        account_id: AccountId,
85    ) -> Result<(BlockNumber, AccountWitness), GetAccountError> {
86        self.with_inner_read_blocking(|inner_state| {
87            // Determine which block to query
88            let (block_num, witness) = if let Some(requested_block) = block_num {
89                // Historical query: use the account tree with history
90                let witness = inner_state
91                    .account_tree
92                    .open_at(account_id, requested_block)
93                    .ok_or_else(|| {
94                        let latest_block = inner_state.account_tree.block_number_latest();
95                        if requested_block > latest_block {
96                            GetAccountError::UnknownBlock(requested_block)
97                        } else {
98                            GetAccountError::BlockPruned(requested_block)
99                        }
100                    })?;
101                (requested_block, witness)
102            } else {
103                // Latest query: use the latest state
104                let block_num = inner_state.account_tree.block_number_latest();
105                let witness = inner_state.account_tree.open_latest(account_id);
106                (block_num, witness)
107            };
108
109            Ok((block_num, witness))
110        })
111    }
112
113    /// Returns storage map details from the forest for a specific account and storage slot.
114    ///
115    /// The forest can only be used if all hashed keys in the storage map are known in the
116    /// reverse-key LRU cache. If any hashed key is unknown, the method returns `Ok(None)` to signal
117    /// that the caller should fall back to reconstructing the storage map details from the
118    /// database.
119    #[miden_instrument(
120        target = COMPONENT,
121        skip_all,
122    )]
123    fn get_storage_map_details_from_forest(
124        &self,
125        account_id: AccountId,
126        slot_name: &StorageSlotName,
127        block_num: BlockNumber,
128    ) -> Result<Option<AccountStorageMapDetails>, DatabaseError> {
129        self.with_forest_read_blocking(|forest| {
130            match forest
131                .get_storage_map_details_for_all_entries(account_id, slot_name.clone(), block_num)
132                .map_err(DatabaseError::MerkleError)?
133            {
134                AccountStorageMapResult::NotFound => Err(DatabaseError::StorageRootNotFound {
135                    account_id,
136                    slot_name: slot_name.to_string(),
137                    block_num,
138                }),
139                AccountStorageMapResult::Details(details) => Ok(Some(details)),
140                AccountStorageMapResult::CannotReconstructKeysFromCache => Ok(None),
141            }
142        })
143    }
144
145    /// Returns vault details by reconstructing the vault from the database.
146    async fn reconstruct_vault_details_from_db(
147        &self,
148        account_id: AccountId,
149        block_num: BlockNumber,
150    ) -> Result<AccountVaultDetails, DatabaseError> {
151        let assets = self.db.select_account_vault_at_block(account_id, block_num).await?;
152
153        if assets.len() > AccountVaultDetails::MAX_RETURN_ENTRIES {
154            return Ok(AccountVaultDetails::LimitExceeded);
155        }
156
157        let keys = assets.iter().map(miden_protocol::asset::Asset::id);
158
159        let forest = self.forest.write().await;
160
161        forest
162            .vault_key_cache
163            .put_many(keys.into_iter().map(|raw_key| (raw_key.hash(), raw_key)));
164
165        Ok(AccountVaultDetails::from_assets(assets))
166    }
167
168    /// Returns storage map details by reconstructing the storage map from the database.
169    async fn reconstruct_storage_map_details_from_db(
170        &self,
171        account_id: AccountId,
172        slot_name: StorageSlotName,
173        block_num: BlockNumber,
174    ) -> Result<AccountStorageMapDetails, DatabaseError> {
175        let details = self
176            .db
177            .reconstruct_storage_map_from_db(
178                account_id,
179                slot_name,
180                block_num,
181                Some(AccountStorageMapDetails::MAX_RETURN_ENTRIES),
182            )
183            .await?;
184
185        if let StorageMapEntries::AllEntries(entries) = &details.entries {
186            self.forest
187                .write()
188                .await
189                .cache_storage_map_keys(entries.iter().map(|(raw_key, _)| *raw_key));
190        }
191
192        Ok(details)
193    }
194
195    /// Fetches the account details (code, vault, storage) for a public account at the specified
196    /// block.
197    ///
198    /// This method queries the database to fetch the account state and processes the detail
199    /// request to return only the requested information.
200    ///
201    /// For specific key queries (`SlotData::MapKeys`), the forest is used to provide SMT proofs.
202    /// Returns an error if the forest doesn't have data for the requested slot.
203    /// All-entries queries (`SlotData::All`) use the forest when all hashed keys are known in the
204    /// reverse-key LRU cache, otherwise they fall back to database reconstruction.
205    #[miden_instrument(
206        target = COMPONENT,
207        skip_all,
208    )]
209    async fn fetch_public_account_details(
210        &self,
211        account_id: AccountId,
212        block_num: BlockNumber,
213        witness: &AccountWitness,
214        detail_request: AccountDetailRequest,
215    ) -> Result<AccountDetails, GetAccountError> {
216        let AccountDetailRequest {
217            code_commitment,
218            asset_vault_commitment,
219            storage_request,
220        } = detail_request;
221
222        if !account_id.is_public() {
223            return Err(GetAccountError::AccountNotPublic(account_id));
224        }
225
226        // Validate block exists in the blockchain before querying the database
227        {
228            let inner = self.inner.read().instrument(tracing::info_span!("acquire_inner")).await;
229            let latest_block_num = inner.latest_block_num();
230
231            if block_num > latest_block_num {
232                return Err(GetAccountError::UnknownBlock(block_num));
233            }
234        }
235
236        // Query account header and storage header together in a single DB call
237        let (account_header, storage_header) = self
238            .db
239            .select_account_header_with_storage_header_at_block(account_id, block_num)
240            .await?
241            .ok_or(GetAccountError::AccountNotFound(account_id, block_num))?;
242
243        let should_apply_response_budget =
244            matches!(&storage_request, AccountStorageRequest::AllStorageMaps);
245        let storage_requests = expand_account_storage_request(storage_request, &storage_header);
246
247        let account_code = match code_commitment {
248            Some(commitment) if commitment == account_header.code_commitment() => None,
249            Some(_) => {
250                self.db
251                    .select_account_code_by_commitment(account_header.code_commitment())
252                    .await?
253            },
254            None => None,
255        };
256
257        // Query account state forest for vault details on commitment mismatch.
258        //
259        // The forest can only reconstruct the vault if all hashed vault keys are known in the
260        // reverse-key LRU cache. If any hashed key is unknown, the forest returns `None` and we
261        // fall back to reconstructing the vault details from the database.
262        let vault_details = match asset_vault_commitment {
263            Some(commitment) if commitment == account_header.vault_root() => {
264                AccountVaultDetails::empty()
265            },
266            Some(_) => {
267                let forest_details = self.with_forest_read_blocking(|forest| {
268                    forest.get_vault_details(account_id, block_num).map_err(|err| {
269                        DatabaseError::DataCorrupted(format!(
270                            "failed to reconstruct vault for account {account_id} at block {block_num}: {err}"
271                        ))
272                    })
273                })?;
274
275                match forest_details {
276                    Some(details) => details,
277                    None => self.reconstruct_vault_details_from_db(account_id, block_num).await?,
278                }
279            },
280            None => AccountVaultDetails::empty(),
281        };
282
283        // Split storage map requests into two categories:
284        // - slots with explicit keys (including proofs)
285        // - slots with "all entries"
286        let mut storage_map_details =
287            Vec::<AccountStorageMapDetails>::with_capacity(storage_requests.len());
288        let mut map_keys_requests = Vec::new();
289        let mut all_entries_requests = Vec::new();
290        let mut storage_request_slots = Vec::with_capacity(storage_requests.len());
291
292        for (index, StorageMapRequest { slot_name, slot_data }) in
293            storage_requests.into_iter().enumerate()
294        {
295            storage_request_slots.push(slot_name.clone());
296            match slot_data {
297                SlotData::MapKeys(keys) => {
298                    map_keys_requests.push((index, slot_name, keys));
299                },
300                SlotData::All => {
301                    all_entries_requests.push((index, slot_name));
302                },
303            }
304        }
305
306        let mut storage_map_details_by_index = vec![None; storage_request_slots.len()];
307
308        // Handle slots with explicit key requests
309        if !map_keys_requests.is_empty() {
310            self.with_forest_read_blocking(|forest| {
311                for (index, slot_name, keys) in map_keys_requests {
312                    let details = forest
313                        .get_storage_map_details_for_keys(
314                            account_id,
315                            slot_name.clone(),
316                            block_num,
317                            &keys,
318                        )
319                        .ok_or_else(|| DatabaseError::StorageRootNotFound {
320                            account_id,
321                            slot_name: slot_name.to_string(),
322                            block_num,
323                        })?
324                        .map_err(DatabaseError::MerkleError)?;
325                    storage_map_details_by_index[index] = Some(details);
326                }
327                Ok::<(), DatabaseError>(())
328            })?;
329        }
330
331        // Handle slots with "all entries" requests
332        for (index, slot_name) in all_entries_requests {
333            let details = match self
334                .get_storage_map_details_from_forest(account_id, &slot_name, block_num)?
335            {
336                Some(details) => details,
337                None => {
338                    self.reconstruct_storage_map_details_from_db(account_id, slot_name, block_num)
339                        .await?
340                },
341            };
342            storage_map_details_by_index[index] = Some(details);
343        }
344
345        for (details, slot_name) in
346            storage_map_details_by_index.into_iter().zip(storage_request_slots.iter())
347        {
348            let details = details.ok_or_else(|| DatabaseError::StorageRootNotFound {
349                account_id,
350                slot_name: slot_name.to_string(),
351                block_num,
352            })?;
353            storage_map_details.push(details);
354        }
355
356        // In case of an "all storage maps" request we have to be careful: even with the per-slot
357        // limit of [`AccountStorageMapDetails::MAX_RETURN_ENTRIES`] we might go over the response
358        // size limit. Here we make sure that we're within that limit by potentially truncating the
359        // response.
360        if should_apply_response_budget {
361            return Ok(apply_all_storage_maps_response_budget(
362                block_num,
363                witness,
364                account_header,
365                account_code,
366                vault_details,
367                storage_header,
368                storage_map_details,
369                storage_request_slots,
370                MAX_ALL_STORAGE_MAPS_RESPONSE_PAYLOAD_WITH_BUDGET_RESERVED_FOR_LIMIT_EXCEEDED_SLOTS,
371            ));
372        }
373
374        Ok(AccountDetails {
375            account_header,
376            account_code,
377            vault_details,
378            storage_details: AccountStorageDetails {
379                header: storage_header,
380                map_details: storage_map_details,
381            },
382        })
383    }
384}
385
386// HELPERS
387// ================================================================================================
388
389/// Expand [`AccountStorageRequest`] to a vector of slot requests.
390fn expand_account_storage_request(
391    storage_request: AccountStorageRequest,
392    storage_header: &AccountStorageHeader,
393) -> Vec<StorageMapRequest> {
394    match storage_request {
395        AccountStorageRequest::None => Vec::new(),
396        AccountStorageRequest::Explicit(requests) => requests,
397        AccountStorageRequest::AllStorageMaps => storage_header
398            .slots()
399            .filter(|slot| slot.slot_type() == StorageSlotType::Map)
400            .map(|slot| StorageMapRequest {
401                slot_name: slot.name().clone(),
402                slot_data: SlotData::All,
403            })
404            .collect(),
405    }
406}
407
408// This is intentionally conservative. Storage slot names can be up to u8::MAX bytes, and a
409// `limit_exceeded` map detail stores only the slot name plus the `too_many_entries` flag.
410const STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN: usize = 263;
411
412// A conservative limit that makes sure that limit exceeded messages can be appended for all slots
413// in the response.
414const MAX_ALL_STORAGE_MAPS_RESPONSE_PAYLOAD_WITH_BUDGET_RESERVED_FOR_LIMIT_EXCEEDED_SLOTS: usize =
415    MAX_RESPONSE_PAYLOAD_BYTES - 256 * STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN - 8192;
416
417// Conservative max length for storage map entries: key-value pairs, each one is four `fixed64`
418// values plus Protobuf overhead.
419const STORAGE_MAP_ENTRY_MAX_LEN: usize = 78;
420
421fn protobuf_bytes_field_len(field_number: u32, len: usize) -> usize {
422    key_len(field_number) + encoded_len_varint(len as u64) + len
423}
424
425/// Give an upper estimate for the encoded size of a single storage map.
426fn estimate_storage_map_details_field_len(details: &AccountStorageMapDetails) -> usize {
427    match &details.entries {
428        StorageMapEntries::LimitExceeded => STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN,
429        StorageMapEntries::AllEntries(entries) => {
430            let slot_name_len = details.slot_name.as_str().len();
431            let slot_name_field_len = protobuf_bytes_field_len(1, slot_name_len);
432            let all_entries_payload_len = entries.len() * STORAGE_MAP_ENTRY_MAX_LEN;
433            let all_entries_field_len = protobuf_bytes_field_len(3, all_entries_payload_len);
434            let details_len = slot_name_field_len + all_entries_field_len;
435
436            protobuf_bytes_field_len(2, details_len)
437        },
438        // `apply_all_storage_maps_response_budget()` is only used for `all_storage_maps` requests,
439        // which never request proofs. Be conservative and force the fallback path if this changes.
440        StorageMapEntries::EntriesWithProofs(_) => usize::MAX,
441    }
442}
443
444/// Limit response size to a payload budget.
445///
446/// Ensures that the [`AccountDetails`] response fits into `max_response_payload_bytes` when encoded.
447/// We iterate over the individual storage map slots and:
448/// - keep the map contents is we're still within our response size budget
449/// - replace the contents with "limit exceeded" if we're past the response size budget.
450///
451/// We reserve space for the "limit exceeded" responses in advance so we're safe to start appending
452/// "limit exceeded" at any point during iteration.
453#[expect(clippy::too_many_arguments)]
454fn apply_all_storage_maps_response_budget(
455    block_num: BlockNumber,
456    witness: &AccountWitness,
457    account_header: AccountHeader,
458    account_code: Option<Vec<u8>>,
459    vault_details: AccountVaultDetails,
460    storage_header: AccountStorageHeader,
461    ordered_map_details: Vec<AccountStorageMapDetails>,
462    ordered_map_slot_names: Vec<StorageSlotName>,
463    max_response_payload_bytes: usize,
464) -> AccountDetails {
465    let mut accepted_map_details = Vec::with_capacity(ordered_map_details.len());
466    let base_response_size_without_map_details =
467        proto::rpc::AccountResponse::from(AccountResponse {
468            block_num,
469            witness: witness.clone(),
470            details: Some(AccountDetails {
471                account_header: account_header.clone(),
472                account_code: account_code.clone(),
473                vault_details: vault_details.clone(),
474                storage_details: AccountStorageDetails {
475                    header: storage_header.clone(),
476                    map_details: vec![],
477                },
478            }),
479        })
480        .encoded_len();
481    let available_map_details_budget =
482        max_response_payload_bytes.saturating_sub(base_response_size_without_map_details);
483    let reserved_limit_exceeded_budget =
484        ordered_map_slot_names.len() * STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN;
485    let mut extra_budget_for_full_maps =
486        available_map_details_budget.saturating_sub(reserved_limit_exceeded_budget);
487
488    for (details, slot_name) in ordered_map_details.into_iter().zip(ordered_map_slot_names) {
489        let estimated_details_len = estimate_storage_map_details_field_len(&details);
490        let extra_cost_over_limit_exceeded =
491            estimated_details_len.saturating_sub(STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN);
492
493        if extra_cost_over_limit_exceeded <= extra_budget_for_full_maps {
494            extra_budget_for_full_maps -= extra_cost_over_limit_exceeded;
495            accepted_map_details.push(details);
496        } else {
497            accepted_map_details.push(AccountStorageMapDetails::limit_exceeded(slot_name));
498        }
499    }
500
501    AccountDetails {
502        account_header,
503        account_code,
504        vault_details,
505        storage_details: AccountStorageDetails {
506            header: storage_header,
507            map_details: accepted_map_details,
508        },
509    }
510}
511
512// TESTS
513// ================================================================================================
514
515#[cfg(test)]
516mod tests {
517    use miden_node_proto::domain::account::{
518        AccountDetails,
519        AccountResponse,
520        AccountStorageDetails,
521        AccountStorageMapDetails,
522        AccountStorageRequest,
523        AccountVaultDetails,
524        SlotData,
525        StorageMapEntries,
526        StorageMapRequest,
527    };
528    use miden_protocol::account::{
529        AccountHeader,
530        AccountId,
531        AccountStorageHeader,
532        StorageMapKey,
533        StorageSlotHeader,
534        StorageSlotName,
535        StorageSlotType,
536    };
537    use miden_protocol::block::BlockNumber;
538    use miden_protocol::block::account_tree::{AccountIdKey, AccountTree, AccountWitness};
539    use miden_protocol::crypto::merkle::smt::{LargeSmt, MemoryStorage};
540    use miden_protocol::testing::account_id::AccountIdBuilder;
541    use miden_protocol::{EMPTY_WORD, Felt, Word};
542
543    use super::{apply_all_storage_maps_response_budget, expand_account_storage_request};
544
545    fn storage_header() -> AccountStorageHeader {
546        AccountStorageHeader::new(vec![
547            StorageSlotHeader::new(StorageSlotName::mock(0), StorageSlotType::Value, EMPTY_WORD),
548            StorageSlotHeader::new(StorageSlotName::mock(1), StorageSlotType::Map, EMPTY_WORD),
549            StorageSlotHeader::new(StorageSlotName::mock(2), StorageSlotType::Map, EMPTY_WORD),
550        ])
551        .unwrap()
552    }
553
554    fn account_id() -> AccountId {
555        AccountIdBuilder::new().build_with_seed([42; 32])
556    }
557
558    fn account_header(account_id: AccountId) -> AccountHeader {
559        AccountHeader::new(account_id, Felt::ZERO, EMPTY_WORD, EMPTY_WORD, EMPTY_WORD)
560    }
561
562    fn account_witness(account_id: AccountId) -> AccountWitness {
563        let smt = LargeSmt::with_entries(
564            MemoryStorage::default(),
565            [(AccountIdKey::from(account_id).as_word(), EMPTY_WORD)],
566        )
567        .unwrap();
568        AccountTree::new(smt).unwrap().open(account_id)
569    }
570
571    fn map_details(slot_name: StorageSlotName, value: Word) -> AccountStorageMapDetails {
572        AccountStorageMapDetails {
573            slot_name,
574            entries: StorageMapEntries::AllEntries(vec![(StorageMapKey::from_index(1), value)]),
575        }
576    }
577
578    fn map_details_with_entries(
579        slot_name: StorageSlotName,
580        entry_count: u8,
581    ) -> AccountStorageMapDetails {
582        AccountStorageMapDetails {
583            slot_name,
584            entries: StorageMapEntries::AllEntries(
585                (1..=entry_count)
586                    .map(|index| {
587                        (
588                            StorageMapKey::from_index(u32::from(index)),
589                            Word::from([u32::from(index), 0, 0, 0]),
590                        )
591                    })
592                    .collect(),
593            ),
594        }
595    }
596
597    #[test]
598    fn all_storage_maps_expands_only_map_slots() {
599        let requests = expand_account_storage_request(
600            AccountStorageRequest::AllStorageMaps,
601            &storage_header(),
602        );
603
604        assert_eq!(requests.len(), 2);
605        assert_eq!(requests[0].slot_name, StorageSlotName::mock(1));
606        assert_eq!(requests[1].slot_name, StorageSlotName::mock(2));
607        assert!(requests.iter().all(|request| request.slot_data == SlotData::All));
608    }
609
610    #[test]
611    fn explicit_storage_maps_are_preserved() {
612        let slot_name = StorageSlotName::mock(2);
613        let explicit = vec![StorageMapRequest {
614            slot_name: slot_name.clone(),
615            slot_data: SlotData::All,
616        }];
617
618        let requests = expand_account_storage_request(
619            AccountStorageRequest::Explicit(explicit.clone()),
620            &storage_header(),
621        );
622
623        assert_eq!(requests, explicit);
624        assert_eq!(requests[0].slot_name, slot_name);
625    }
626
627    #[test]
628    fn absent_storage_slot_data_expands_to_no_requests() {
629        let requests =
630            expand_account_storage_request(AccountStorageRequest::None, &storage_header());
631
632        assert!(requests.is_empty());
633    }
634
635    #[test]
636    fn limit_exceeded_max_size_covers_max_slot_name() {
637        use miden_node_proto::prost::Message;
638
639        let max_slot_name = StorageSlotName::new(format!("a::{}", "a".repeat(252))).unwrap();
640
641        let details = super::proto::rpc::account_storage_details::AccountStorageMapDetails::from(
642            AccountStorageMapDetails::limit_exceeded(max_slot_name),
643        );
644
645        assert!(super::STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN >= details.encoded_len());
646    }
647
648    #[test]
649    fn all_entries_size_estimate_covers_actual_protobuf_size() {
650        use miden_node_proto::prost::Message;
651
652        let details = map_details(StorageSlotName::mock(1), Word::from([1u32, 0, 0, 0]));
653        let actual = super::proto::rpc::account_storage_details::AccountStorageMapDetails::from(
654            details.clone(),
655        )
656        .encoded_len();
657
658        assert!(super::estimate_storage_map_details_field_len(&details) >= actual);
659    }
660
661    #[test]
662    fn all_storage_maps_budget_marks_maps_as_limit_exceeded_when_budget_is_exhausted() {
663        use miden_node_proto::prost::Message;
664
665        let account_id = account_id();
666        let witness = account_witness(account_id);
667        let header = account_header(account_id);
668        let storage_header = storage_header();
669        let slot_1 = StorageSlotName::mock(1);
670        let slot_2 = StorageSlotName::mock(2);
671        let marker_only_budget = super::proto::rpc::AccountResponse::from(AccountResponse {
672            block_num: BlockNumber::GENESIS,
673            witness: witness.clone(),
674            details: Some(AccountDetails {
675                account_header: header.clone(),
676                account_code: None,
677                vault_details: AccountVaultDetails::empty(),
678                storage_details: AccountStorageDetails {
679                    header: storage_header.clone(),
680                    map_details: vec![
681                        AccountStorageMapDetails::limit_exceeded(slot_1.clone()),
682                        AccountStorageMapDetails::limit_exceeded(slot_2.clone()),
683                    ],
684                },
685            }),
686        })
687        .encoded_len();
688        let details = apply_all_storage_maps_response_budget(
689            BlockNumber::GENESIS,
690            &witness,
691            header,
692            None,
693            AccountVaultDetails::empty(),
694            storage_header,
695            vec![
696                map_details_with_entries(slot_1.clone(), 8),
697                map_details_with_entries(slot_2.clone(), 8),
698            ],
699            vec![slot_1.clone(), slot_2.clone()],
700            marker_only_budget,
701        );
702
703        assert_eq!(details.storage_details.map_details.len(), 2);
704        assert_eq!(details.storage_details.map_details[0].slot_name, slot_1);
705        assert_eq!(
706            details.storage_details.map_details[0].entries,
707            StorageMapEntries::LimitExceeded
708        );
709        assert_eq!(details.storage_details.map_details[1].slot_name, slot_2);
710        assert_eq!(
711            details.storage_details.map_details[1].entries,
712            StorageMapEntries::LimitExceeded
713        );
714    }
715
716    #[test]
717    fn all_storage_maps_budget_stays_under_hard_cap_with_many_limit_exceeded_maps() {
718        use miden_node_proto::prost::Message;
719
720        let account_id = account_id();
721        let witness = account_witness(account_id);
722        let header = account_header(account_id);
723        let mut slot_names: Vec<_> = (1..10).map(StorageSlotName::mock).collect();
724        slot_names.sort();
725        let storage_header = AccountStorageHeader::new(
726            slot_names
727                .iter()
728                .cloned()
729                .map(|slot_name| {
730                    StorageSlotHeader::new(slot_name, StorageSlotType::Map, EMPTY_WORD)
731                })
732                .collect(),
733        )
734        .unwrap();
735        let marker_only_hard_cap = super::proto::rpc::AccountResponse::from(AccountResponse {
736            block_num: BlockNumber::GENESIS,
737            witness: witness.clone(),
738            details: Some(AccountDetails {
739                account_header: header.clone(),
740                account_code: None,
741                vault_details: AccountVaultDetails::empty(),
742                storage_details: AccountStorageDetails {
743                    header: storage_header.clone(),
744                    map_details: slot_names
745                        .iter()
746                        .cloned()
747                        .map(AccountStorageMapDetails::limit_exceeded)
748                        .collect(),
749                },
750            }),
751        })
752        .encoded_len();
753
754        let details = apply_all_storage_maps_response_budget(
755            BlockNumber::GENESIS,
756            &witness,
757            header,
758            None,
759            AccountVaultDetails::empty(),
760            storage_header,
761            slot_names
762                .iter()
763                .cloned()
764                .map(|slot_name| map_details_with_entries(slot_name, 8))
765                .collect(),
766            slot_names.clone(),
767            marker_only_hard_cap,
768        );
769
770        assert_eq!(details.storage_details.map_details.len(), slot_names.len());
771        assert!(
772            details
773                .storage_details
774                .map_details
775                .iter()
776                .all(|details| details.entries == StorageMapEntries::LimitExceeded)
777        );
778        assert!(
779            super::proto::rpc::AccountResponse::from(AccountResponse {
780                block_num: BlockNumber::GENESIS,
781                witness,
782                details: Some(details),
783            })
784            .encoded_len()
785                <= marker_only_hard_cap
786        );
787    }
788
789    #[test]
790    fn all_storage_maps_budget_keeps_entries_that_fit() {
791        let account_id = account_id();
792        let slot_1 = StorageSlotName::mock(1);
793        let details = apply_all_storage_maps_response_budget(
794            BlockNumber::GENESIS,
795            &account_witness(account_id),
796            account_header(account_id),
797            None,
798            AccountVaultDetails::empty(),
799            storage_header(),
800            vec![map_details(slot_1.clone(), Word::from([1u32, 0, 0, 0]))],
801            vec![slot_1.clone()],
802            usize::MAX,
803        );
804
805        assert_eq!(details.storage_details.map_details.len(), 1);
806        assert_eq!(details.storage_details.map_details[0].slot_name, slot_1);
807        assert!(matches!(
808            details.storage_details.map_details[0].entries,
809            StorageMapEntries::AllEntries(_)
810        ));
811    }
812}