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 #[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 #[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 let (block_num, witness) = if let Some(requested_block) = block_num {
89 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 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 #[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 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 #[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 {
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 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 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 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 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 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 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
352fn 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
374const STORAGE_MAP_LIMIT_EXCEEDED_FIELD_MAX_LEN: usize = 263;
377
378const 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
383const 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
391fn 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 StorageMapEntries::EntriesWithProofs(_) => usize::MAX,
407 }
408}
409
410#[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#[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}