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 #[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 #[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 let (block_num, witness) = if let Some(requested_block) = block_num {
82 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 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 #[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 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 #[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 {
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 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 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 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 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 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 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
340fn 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
362const STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN: usize = 263;
365
366const 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
371const 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
379fn 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 StorageMapEntries::EntriesWithProofs(_) => usize::MAX,
395 }
396}
397
398#[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#[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}