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