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