1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3use core::fmt::{self, Debug, Formatter};
4
5use miden_protocol::account::{
6 Account, AccountCode, AccountHeader, AccountId, AccountStorage, AccountStorageHeader,
7 PartialAccount, PartialStorage, PartialStorageMap, StorageMap, StorageMapKey, StorageSlot,
8 StorageSlotHeader, StorageSlotName, StorageSlotType,
9};
10use miden_protocol::asset::{Asset, AssetId, AssetVault, PartialVault};
11use miden_protocol::block::account_tree::AccountWitness;
12use miden_protocol::crypto::merkle::SparseMerklePath;
13use miden_protocol::crypto::merkle::smt::PartialSmt;
14use miden_protocol::transaction::AccountInputs;
15use miden_objects::DecodeMessageExt;
16use miden_protocol::{EMPTY_WORD, Word};
17use miden_tx::utils::serde::{Deserializable, DeserializationError, Serializable};
18use thiserror::Error;
19
20use crate::alloc::string::ToString;
21use crate::rpc::{AccountStateAt, RpcError};
22use crate::rpc::domain::MissingFieldHelper;
23use crate::rpc::generated::rpc::get_account_request::account_detail_request::storage_map_detail_request::{MapKeys, SlotData};
24use crate::rpc::generated::rpc::get_account_request::account_detail_request::{
25 StorageMapDetailRequest, StorageMapDetailRequests, StorageRequest, VaultAssetIds, VaultRequest,
26};
27use crate::rpc::generated::{self as proto};
28
29impl Debug for proto::rpc::RegisterAccountRequest {
34 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
35 f.debug_struct("RegisterAccountRequest")
36 .field("account_id", &self.account_id)
37 .finish_non_exhaustive()
38 }
39}
40
41impl Debug for proto::rpc::IsInvitationCodeValidRequest {
43 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44 f.debug_struct("IsInvitationCodeValidRequest").finish_non_exhaustive()
45 }
46}
47
48#[cfg(feature = "tonic")]
52impl proto::rpc::get_account_response::AccountDetails {
53 pub fn into_domain(
68 self,
69 known_account_codes: &BTreeMap<Word, AccountCode>,
70 fetch: &AccountFetch,
71 ) -> Result<AccountDetails, crate::rpc::RpcError> {
72 use crate::rpc::RpcError;
73 use crate::rpc::domain::MissingFieldHelper;
74
75 let proto::rpc::get_account_response::AccountDetails {
76 header,
77 storage_details,
78 code,
79 vault_details,
80 } = self;
81 let header: AccountHeader = header
82 .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
83 header
84 )))?
85 .decode_and_verify()?;
86
87 let storage_details: AccountStorageDetails = storage_details
88 .ok_or(proto::rpc::get_account_response::AccountDetails::missing_field(stringify!(
89 storage_details
90 )))?
91 .try_into()?;
92
93 storage_details.validate_against_request(fetch.storage())?;
94
95 let code = {
99 let received_code: Option<AccountCode> =
100 code.map(DecodeMessageExt::decode_and_verify).transpose()?;
101 match received_code {
102 Some(code) => code,
103 None => known_account_codes
104 .get(&header.code_commitment())
105 .ok_or(RpcError::InvalidResponse(
106 "Account code was not provided, but the response did not contain it either"
107 .into(),
108 ))?
109 .clone(),
110 }
111 };
112
113 let vault_details = AccountVaultDetails::try_from(
114 vault_details
115 .ok_or(proto::rpc::AccountVaultDetails::missing_field(stringify!(vault_details)))?,
116 )?
117 .validate_against_request(header.vault_root(), fetch.vault())?;
118
119 Ok(AccountDetails {
120 header,
121 storage_details,
122 code,
123 vault_details,
124 })
125 }
126}
127
128#[derive(Clone, Debug)]
133pub struct AccountDetails {
134 pub header: AccountHeader,
135 pub storage_details: AccountStorageDetails,
136 pub code: AccountCode,
137 pub vault_details: AccountVaultDetails,
138}
139
140impl TryFrom<AccountDetails> for PartialAccount {
141 type Error = RpcError;
142
143 fn try_from(details: AccountDetails) -> Result<Self, Self::Error> {
152 let AccountDetails {
153 header,
154 storage_details,
155 code,
156 vault_details,
157 } = details;
158 let mut storage_maps = Vec::with_capacity(storage_details.map_details.len());
159 for map_details in storage_details.map_details {
160 let map = match map_details.entries {
161 StorageMapEntries::AllEntries(entries) => {
162 let map = StorageMap::with_entries(
163 entries.iter().map(|entry| (entry.key, entry.value)),
164 )
165 .map_err(|err| {
166 RpcError::InvalidResponse(format!("invalid storage map entries: {err}"))
167 })?;
168 let slot_root = storage_details
169 .header
170 .slots()
171 .find(|slot| *slot.name() == map_details.slot_name)
172 .map(StorageSlotHeader::value);
173 if Some(map.root()) != slot_root {
174 return Err(RpcError::InvalidResponse(format!(
175 "storage map entries for slot '{}' do not match the slot root",
176 map_details.slot_name
177 )));
178 }
179 PartialStorageMap::new_full(map)
180 },
181 StorageMapEntries::PartialMap { map_keys, partial_smt } => {
182 PartialStorageMap::try_from_parts(partial_smt, map_keys).map_err(|err| {
183 RpcError::InvalidResponse(format!("invalid storage map proof: {err}"))
184 })?
185 },
186 StorageMapEntries::LimitExceeded => continue,
187 };
188 storage_maps.push(map);
189 }
190 let vault = match vault_details {
191 AccountVaultDetails::Assets(assets) => {
192 let vault = AssetVault::new(&assets).map_err(|err| {
193 RpcError::InvalidResponse(format!("invalid asset list: {err}"))
194 })?;
195 if vault.root() == header.vault_root() {
196 PartialVault::new_full(vault)
197 } else if assets.is_empty() {
198 PartialVault::new(header.vault_root())
201 } else {
202 return Err(RpcError::InvalidResponse(
203 "asset list does not match the vault root".into(),
204 ));
205 }
206 },
207 AccountVaultDetails::Partial(vault) => {
208 if vault.root() != header.vault_root() {
209 return Err(RpcError::InvalidResponse(
210 "vault proofs do not match the vault root".into(),
211 ));
212 }
213 vault
214 },
215 AccountVaultDetails::TooManyAssets => PartialVault::new(header.vault_root()),
216 };
217 let storage = PartialStorage::new(storage_details.header, storage_maps)
218 .map_err(|err| RpcError::InvalidResponse(format!("invalid partial storage: {err}")))?;
219 let account = PartialAccount::new(header.id(), header.nonce(), code, storage, vault, None)
220 .map_err(|err| RpcError::InvalidResponse(format!("invalid partial account: {err}")))?;
221 if account.to_commitment() != header.to_commitment() {
222 return Err(RpcError::InvalidResponse(
223 "partial account does not match its header".into(),
224 ));
225 }
226 Ok(account)
227 }
228}
229
230impl TryFrom<&AccountDetails> for Account {
231 type Error = RpcError;
232
233 fn try_from(details: &AccountDetails) -> Result<Self, Self::Error> {
239 let assets = match &details.vault_details {
240 AccountVaultDetails::Assets(assets) => assets,
241 AccountVaultDetails::TooManyAssets => {
242 return Err(RpcError::ExpectedDataMissing(
243 "cannot build account: vault has too many assets".into(),
244 ));
245 },
246 AccountVaultDetails::Partial(_) => {
247 return Err(RpcError::ExpectedDataMissing(
248 "cannot build account: only specific vault assets were requested".into(),
249 ));
250 },
251 };
252
253 if let Some(slot_name) = details
254 .storage_details
255 .map_details
256 .iter()
257 .find(|m| m.is_limit_exceeded())
258 .map(|m| &m.slot_name)
259 {
260 return Err(RpcError::ExpectedDataMissing(format!(
261 "cannot build account: storage map slot '{slot_name}' has too many entries",
262 )));
263 }
264
265 let mut slots: Vec<StorageSlot> = Vec::new();
266
267 for slot_header in details.storage_details.header.slots() {
268 match slot_header.slot_type() {
269 StorageSlotType::Value => {
270 slots.push(StorageSlot::with_value(
271 slot_header.name().clone(),
272 slot_header.value(),
273 ));
274 },
275 StorageSlotType::Map => {
276 let map_details = details
277 .storage_details
278 .find_map_details(slot_header.name())
279 .ok_or_else(|| {
280 RpcError::ExpectedDataMissing(format!(
281 "slot '{}' is a map but has no map_details in response",
282 slot_header.name()
283 ))
284 })?;
285
286 let storage_map = map_details
287 .entries
288 .clone()
289 .into_storage_map()
290 .ok_or_else(|| {
291 RpcError::ExpectedDataMissing(format!(
292 "slot '{}' did not come back with all its entries, so the full \
293 account cannot be built",
294 slot_header.name(),
295 ))
296 })?
297 .map_err(|err| {
298 RpcError::InvalidResponse(format!(
299 "the rpc api returned a non-valid map entry: {err}"
300 ))
301 })?;
302
303 slots.push(StorageSlot::with_map(slot_header.name().clone(), storage_map));
304 },
305 }
306 }
307
308 let asset_vault = AssetVault::new(assets).map_err(|err| {
309 RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
310 })?;
311
312 let account_storage = AccountStorage::new(slots).map_err(|err| {
313 RpcError::InvalidResponse(format!("rpc api returned non-valid storage slots: {err}"))
314 })?;
315
316 Account::new(
317 details.header.id(),
318 asset_vault,
319 account_storage,
320 details.code.clone(),
321 details.header.nonce(),
322 None,
323 )
324 .map_err(|err| {
325 RpcError::InvalidResponse(format!(
326 "failed to construct account from rpc api response: {err}"
327 ))
328 })
329 }
330}
331
332#[derive(Clone, Debug)]
337pub struct AccountStorageDetails {
338 pub header: AccountStorageHeader,
340 pub map_details: Vec<AccountStorageMapDetails>,
342}
343
344impl AccountStorageDetails {
345 pub fn find_map_details(&self, target: &StorageSlotName) -> Option<&AccountStorageMapDetails> {
349 self.map_details.iter().find(|map_detail| map_detail.slot_name == *target)
350 }
351
352 pub fn validate_against_request(&self, storage: &StorageMapFetch) -> Result<(), RpcError> {
358 for map_detail in &self.map_details {
359 let StorageMapEntries::PartialMap { map_keys, .. } = &map_detail.entries else {
360 continue;
361 };
362
363 let requested_keys = match storage {
364 StorageMapFetch::Slots(requirements) => {
365 requirements.keys_for_slot(&map_detail.slot_name)
366 },
367 StorageMapFetch::Skip | StorageMapFetch::All => &[],
368 };
369 if let Some(key) = requested_keys.iter().find(|key| !map_keys.contains(key)) {
370 return Err(RpcError::InvalidResponse(format!(
371 "partial storage map for slot '{}' does not cover requested key {}",
372 map_detail.slot_name,
373 key.to_hex(),
374 )));
375 }
376 if let Some(key) = map_keys.iter().find(|key| !requested_keys.contains(key)) {
377 return Err(RpcError::InvalidResponse(format!(
378 "partial storage map for slot '{}' covers key {}, which was not requested",
379 map_detail.slot_name,
380 key.to_hex(),
381 )));
382 }
383 }
384
385 Ok(())
386 }
387}
388
389impl TryFrom<proto::rpc::AccountStorageDetails> for AccountStorageDetails {
390 type Error = RpcError;
391
392 fn try_from(value: proto::rpc::AccountStorageDetails) -> Result<Self, Self::Error> {
393 let header: AccountStorageHeader = value
394 .header
395 .ok_or(proto::account::AccountStorageHeader::missing_field(stringify!(header)))?
396 .decode_and_verify()?;
397 let map_details = value
398 .map_details
399 .into_iter()
400 .map(core::convert::TryInto::try_into)
401 .collect::<Result<Vec<AccountStorageMapDetails>, RpcError>>()?;
402
403 for map_detail in &map_details {
407 let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
408 continue;
409 };
410
411 let slot = header
412 .slots()
413 .find(|slot| *slot.name() == map_detail.slot_name)
414 .ok_or_else(|| {
415 RpcError::InvalidResponse(format!(
416 "partial storage map references slot '{}', which is absent from the \
417 storage header",
418 map_detail.slot_name,
419 ))
420 })?;
421 if slot.slot_type() != StorageSlotType::Map {
422 return Err(RpcError::InvalidResponse(format!(
423 "partial storage map references slot '{}', which is not a map",
424 map_detail.slot_name,
425 )));
426 }
427 if partial_smt.root() != slot.value() {
428 return Err(RpcError::InvalidResponse(format!(
429 "partial storage map for slot '{}' has root {} but the storage header reports \
430 {}",
431 map_detail.slot_name,
432 partial_smt.root(),
433 slot.value(),
434 )));
435 }
436 }
437
438 Ok(Self { header, map_details })
439 }
440}
441
442#[derive(Clone, Debug)]
446pub struct AccountStorageMapDetails {
447 pub slot_name: StorageSlotName,
449 pub entries: StorageMapEntries,
451}
452
453impl AccountStorageMapDetails {
454 pub const MAX_PARTIAL_MAP_KEYS: usize = 64;
457
458 pub fn is_limit_exceeded(&self) -> bool {
462 matches!(self.entries, StorageMapEntries::LimitExceeded)
463 }
464}
465
466impl TryFrom<proto::rpc::account_storage_details::AccountStorageMapDetails>
467 for AccountStorageMapDetails
468{
469 type Error = RpcError;
470
471 fn try_from(
472 value: proto::rpc::account_storage_details::AccountStorageMapDetails,
473 ) -> Result<Self, Self::Error> {
474 use proto::rpc::account_storage_details::account_storage_map_details::Result as ProtoResult;
475
476 let slot_name = StorageSlotName::new(value.slot_name)
477 .map_err(|err| RpcError::ExpectedDataMissing(err.to_string()))?;
478
479 let entries = match value.result {
480 Some(ProtoResult::TooManyEntries(true)) => StorageMapEntries::LimitExceeded,
481 Some(ProtoResult::TooManyEntries(false)) => {
482 return Err(RpcError::InvalidResponse(
483 "too_many_entries must be true when set".into(),
484 ));
485 },
486 Some(ProtoResult::AllEntries(all_entries)) => {
487 let entries = all_entries
488 .entries
489 .into_iter()
490 .map(core::convert::TryInto::try_into)
491 .collect::<Result<Vec<StorageMapEntry>, RpcError>>()?;
492 StorageMapEntries::AllEntries(entries)
493 },
494 Some(ProtoResult::PartialMap(partial_map)) => {
495 if partial_map.map_keys.len() > Self::MAX_PARTIAL_MAP_KEYS {
496 return Err(RpcError::InvalidResponse(format!(
497 "partial storage map for slot '{slot_name}' contains {} keys, exceeding \
498 the limit of {}",
499 partial_map.map_keys.len(),
500 Self::MAX_PARTIAL_MAP_KEYS,
501 )));
502 }
503
504 let map_keys = partial_map
505 .map_keys
506 .into_iter()
507 .map(|key| Word::try_from(key).map(StorageMapKey::new))
508 .collect::<Result<Vec<_>, _>>()?;
509 if let Some(key) = first_duplicate_key(&map_keys) {
510 return Err(RpcError::InvalidResponse(format!(
511 "partial storage map for slot '{slot_name}' repeats key {}",
512 key.to_hex(),
513 )));
514 }
515
516 let partial_smt: PartialSmt = partial_map
517 .partial_smt
518 .ok_or(proto::rpc::account_storage_details::account_storage_map_details::PartialStorageMap::missing_field(
519 stringify!(partial_smt),
520 ))?
521 .decode_and_verify()?;
522
523 for key in &map_keys {
526 partial_smt.get_value(&key.hash().as_word()).map_err(|_| {
527 RpcError::InvalidResponse(format!(
528 "partial storage map for slot '{slot_name}' does not track key {}",
529 key.to_hex(),
530 ))
531 })?;
532 }
533
534 StorageMapEntries::PartialMap { map_keys, partial_smt }
535 },
536 None => {
537 return Err(RpcError::InvalidResponse(format!(
538 "storage map details for slot '{slot_name}' carry no result",
539 )));
540 },
541 };
542
543 Ok(Self { slot_name, entries })
544 }
545}
546
547fn first_duplicate_key(keys: &[StorageMapKey]) -> Option<&StorageMapKey> {
552 keys.iter()
553 .enumerate()
554 .find_map(|(index, key)| keys[..index].contains(key).then_some(key))
555}
556
557#[derive(Clone, Debug)]
562pub struct StorageMapEntry {
563 pub key: StorageMapKey,
564 pub value: Word,
565}
566
567impl TryFrom<proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry>
568 for StorageMapEntry
569{
570 type Error = RpcError;
571
572 fn try_from(value: proto::rpc::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry) -> Result<Self, Self::Error> {
573 let key = Word::try_from(value.key.ok_or(RpcError::ExpectedDataMissing("key".into()))?)
574 .map(StorageMapKey::new)?;
575 let value = value.value.ok_or(RpcError::ExpectedDataMissing("value".into()))?.try_into()?;
576 Ok(Self { key, value })
577 }
578}
579
580#[derive(Clone, Debug)]
586pub enum StorageMapEntries {
587 LimitExceeded,
590 AllEntries(Vec<StorageMapEntry>),
592 PartialMap {
598 map_keys: Vec<StorageMapKey>,
600 partial_smt: PartialSmt,
602 },
603}
604
605impl StorageMapEntries {
606 pub fn into_storage_map(
611 self,
612 ) -> Option<Result<StorageMap, miden_protocol::errors::StorageMapError>> {
613 match self {
614 StorageMapEntries::AllEntries(entries) => {
615 Some(StorageMap::with_entries(entries.into_iter().map(|e| (e.key, e.value))))
616 },
617 StorageMapEntries::LimitExceeded | StorageMapEntries::PartialMap { .. } => None,
618 }
619 }
620}
621
622#[derive(Clone, Debug)]
628pub enum AccountVaultDetails {
629 TooManyAssets,
632 Assets(Vec<Asset>),
638 Partial(PartialVault),
641}
642
643impl AccountVaultDetails {
644 pub fn is_too_many_assets(&self) -> bool {
646 matches!(self, Self::TooManyAssets)
647 }
648
649 pub fn assets(&self) -> Option<&[Asset]> {
651 match self {
652 Self::Assets(assets) => Some(assets),
653 Self::TooManyAssets | Self::Partial(_) => None,
654 }
655 }
656
657 pub fn into_partial_vault(self) -> Option<PartialVault> {
660 match self {
661 Self::Partial(partial_vault) => Some(partial_vault),
662 Self::TooManyAssets | Self::Assets(_) => None,
663 }
664 }
665
666 #[cfg(feature = "tonic")]
680 pub(crate) fn validate_against_request(
681 self,
682 vault_root: Word,
683 vault_request: &VaultFetch,
684 ) -> Result<Self, RpcError> {
685 let VaultFetch::Assets(asset_ids) = vault_request else {
686 if matches!(self, Self::Partial(_)) {
687 return Err(RpcError::InvalidResponse(
688 "the response contains vault proofs that were not requested".into(),
689 ));
690 }
691 return Ok(self);
692 };
693
694 let partial_vault = match self {
695 Self::Partial(partial_vault) => {
696 if let Some(asset_id) =
697 partial_vault.asset_ids().find(|asset_id| !asset_ids.contains(asset_id))
698 {
699 return Err(RpcError::InvalidResponse(format!(
700 "the vault proofs track asset {asset_id}, which was not requested"
701 )));
702 }
703 partial_vault
704 },
705 Self::TooManyAssets => {
706 return Err(RpcError::InvalidResponse(
707 "the node did not prove the requested assets because the vault is too large"
708 .into(),
709 ));
710 },
711 Self::Assets(assets) => {
712 PartialVault::new_full(AssetVault::new(&assets).map_err(|err| {
713 RpcError::InvalidResponse(format!("rpc api returned non-valid assets: {err}"))
714 })?)
715 },
716 };
717
718 if partial_vault.root() != vault_root {
719 return Err(RpcError::InvalidResponse(format!(
720 "the vault proofs verify against root {} but the account header has vault root \
721 {vault_root}",
722 partial_vault.root(),
723 )));
724 }
725
726 if let Some(asset_id) =
727 asset_ids.iter().find(|asset_id| partial_vault.open(**asset_id).is_err())
728 {
729 return Err(RpcError::InvalidResponse(format!(
730 "the vault proofs do not cover requested asset {asset_id}"
731 )));
732 }
733
734 Ok(Self::Partial(partial_vault))
735 }
736}
737
738impl TryFrom<proto::rpc::AccountVaultDetails> for AccountVaultDetails {
739 type Error = RpcError;
740
741 fn try_from(value: proto::rpc::AccountVaultDetails) -> Result<Self, Self::Error> {
746 let proto::rpc::AccountVaultDetails { too_many_assets, assets, partial_vault } = value;
747
748 match (too_many_assets, assets.is_empty(), partial_vault) {
749 (false, _, None) => Ok(Self::Assets(
750 assets
751 .into_iter()
752 .map(DecodeMessageExt::decode_and_verify)
753 .collect::<Result<Vec<Asset>, _>>()?,
754 )),
755 (true, true, None) => Ok(Self::TooManyAssets),
756 (false, true, Some(partial_vault)) => Ok(Self::Partial(partial_vault.try_into()?)),
757 _ => Err(RpcError::InvalidResponse(
758 "the vault details set more than one of the asset list, the vault proofs, and the \
759 too many assets flag"
760 .into(),
761 )),
762 }
763 }
764}
765
766impl TryFrom<proto::rpc::account_vault_details::PartialVault> for PartialVault {
767 type Error = RpcError;
768
769 fn try_from(
774 value: proto::rpc::account_vault_details::PartialVault,
775 ) -> Result<Self, Self::Error> {
776 let asset_ids = value
777 .asset_ids
778 .into_iter()
779 .map(DecodeMessageExt::decode_and_verify)
780 .collect::<Result<Vec<AssetId>, _>>()?;
781
782 let partial_smt: PartialSmt = value
783 .partial_smt
784 .ok_or(proto::rpc::account_vault_details::PartialVault::missing_field(stringify!(
785 partial_smt
786 )))?
787 .decode_and_verify()?;
788
789 PartialVault::try_from_parts(partial_smt, asset_ids).map_err(|err| {
790 RpcError::InvalidResponse(format!("rpc api returned a non-valid partial vault: {err}"))
791 })
792 }
793}
794
795#[derive(Clone, Debug)]
800pub struct AccountProof {
801 account_witness: AccountWitness,
803 state_headers: Option<AccountDetails>,
805}
806
807impl AccountProof {
808 pub fn into_account_inputs(self) -> Result<AccountInputs, RpcError> {
813 let (witness, details) = self.into_parts();
814 let details = details
815 .ok_or_else(|| RpcError::ExpectedDataMissing("public account details".into()))?;
816 Ok(AccountInputs::new(details.try_into()?, witness))
817 }
818
819 pub fn new(
821 account_witness: AccountWitness,
822 account_details: Option<AccountDetails>,
823 ) -> Result<Self, AccountProofError> {
824 if let Some(AccountDetails {
825 header: account_header,
826 storage_details: _,
827 code,
828 ..
829 }) = &account_details
830 {
831 if account_header.to_commitment() != account_witness.state_commitment() {
832 return Err(AccountProofError::InconsistentAccountCommitment);
833 }
834 if account_header.id() != account_witness.id() {
835 return Err(AccountProofError::InconsistentAccountId);
836 }
837 if code.commitment() != account_header.code_commitment() {
838 return Err(AccountProofError::InconsistentCodeCommitment);
839 }
840 }
841
842 Ok(Self {
843 account_witness,
844 state_headers: account_details,
845 })
846 }
847
848 pub fn account_id(&self) -> AccountId {
850 self.account_witness.id()
851 }
852
853 pub fn account_header(&self) -> Option<&AccountHeader> {
855 self.state_headers.as_ref().map(|account_details| &account_details.header)
856 }
857
858 pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
860 self.state_headers
861 .as_ref()
862 .map(|account_details| &account_details.storage_details.header)
863 }
864
865 pub fn storage_details(&self) -> Option<&AccountStorageDetails> {
867 self.state_headers.as_ref().map(|d| &d.storage_details)
868 }
869
870 pub fn vault_details(&self) -> Option<&AccountVaultDetails> {
872 self.state_headers.as_ref().map(|d| &d.vault_details)
873 }
874
875 pub fn find_map_details(
877 &self,
878 slot_name: &StorageSlotName,
879 ) -> Option<&AccountStorageMapDetails> {
880 self.state_headers
881 .as_ref()
882 .and_then(|details| details.storage_details.find_map_details(slot_name))
883 }
884
885 pub fn account_code(&self) -> Option<&AccountCode> {
887 self.state_headers.as_ref().map(|headers| &headers.code)
888 }
889
890 pub fn code_commitment(&self) -> Option<Word> {
892 self.account_code().map(AccountCode::commitment)
893 }
894
895 pub fn account_commitment(&self) -> Word {
897 self.account_witness.state_commitment()
898 }
899
900 pub fn account_witness(&self) -> &AccountWitness {
901 &self.account_witness
902 }
903
904 pub fn merkle_proof(&self) -> &SparseMerklePath {
906 self.account_witness.path()
907 }
908
909 pub fn into_parts(self) -> (AccountWitness, Option<AccountDetails>) {
911 (self.account_witness, self.state_headers)
912 }
913
914 pub fn into_details(self) -> Option<AccountDetails> {
916 self.state_headers
917 }
918
919 pub fn details_mut(&mut self) -> Option<&mut AccountDetails> {
925 self.state_headers.as_mut()
926 }
927}
928
929#[cfg(feature = "tonic")]
930impl TryFrom<proto::rpc::GetAccountResponse> for AccountProof {
931 type Error = RpcError;
932 fn try_from(account_proof: proto::rpc::GetAccountResponse) -> Result<Self, Self::Error> {
933 let Some(witness) = account_proof.witness else {
934 return Err(RpcError::ExpectedDataMissing(
935 "GetAccount returned an account without witness".to_string(),
936 ));
937 };
938
939 let details: Option<AccountDetails> = {
940 match account_proof.details {
941 None => None,
942 Some(details) => {
943 Some(details.into_domain(&BTreeMap::new(), &AccountFetch::default())?)
944 },
945 }
946 };
947 AccountProof::new(witness.decode_and_verify()?, details)
948 .map_err(|err| RpcError::InvalidResponse(format!("{err}")))
949 }
950}
951
952#[derive(Clone, Debug, Default, Eq, PartialEq)]
961pub struct AccountStorageRequirements(BTreeMap<StorageSlotName, Vec<StorageMapKey>>);
962
963impl AccountStorageRequirements {
964 pub fn new<'a>(
970 slots_and_keys: impl IntoIterator<
971 Item = (StorageSlotName, impl IntoIterator<Item = &'a StorageMapKey>),
972 >,
973 ) -> Self {
974 let map = slots_and_keys
975 .into_iter()
976 .map(|(slot_name, keys_iter)| {
977 let mut keys_vec: Vec<StorageMapKey> = Vec::new();
978 for key in keys_iter {
979 if !keys_vec.contains(key) {
980 keys_vec.push(*key);
981 }
982 }
983 (slot_name, keys_vec)
984 })
985 .collect();
986
987 AccountStorageRequirements(map)
988 }
989
990 pub fn all_entries(slot_names: &[StorageSlotName]) -> Self {
993 AccountStorageRequirements(
994 slot_names.iter().map(|name| (name.clone(), Vec::new())).collect(),
995 )
996 }
997
998 pub fn inner(&self) -> &BTreeMap<StorageSlotName, Vec<StorageMapKey>> {
999 &self.0
1000 }
1001
1002 pub fn keys_for_slot(&self, slot_name: &StorageSlotName) -> &[StorageMapKey] {
1004 self.0.get(slot_name).map_or(&[], Vec::as_slice)
1005 }
1006}
1007
1008impl From<AccountStorageRequirements> for Vec<StorageMapDetailRequest> {
1009 fn from(value: AccountStorageRequirements) -> Vec<StorageMapDetailRequest> {
1010 let request_map = value.0;
1011 let mut requests = Vec::with_capacity(request_map.len());
1012 for (slot_name, map_keys) in request_map {
1013 let slot_data = if map_keys.is_empty() {
1014 Some(SlotData::AllEntries(true))
1015 } else {
1016 let keys = map_keys.into_iter().map(|key| Word::from(key).into()).collect();
1017 Some(SlotData::MapKeys(MapKeys { map_keys: keys }))
1018 };
1019 requests.push(StorageMapDetailRequest {
1020 slot_name: slot_name.to_string(),
1021 slot_data,
1022 });
1023 }
1024 requests
1025 }
1026}
1027
1028impl Serializable for AccountStorageRequirements {
1029 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1030 target.write(&self.0);
1031 }
1032}
1033
1034impl Deserializable for AccountStorageRequirements {
1035 fn read_from<R: miden_tx::utils::serde::ByteReader>(
1036 source: &mut R,
1037 ) -> Result<Self, miden_tx::utils::serde::DeserializationError> {
1038 Ok(AccountStorageRequirements(source.read()?))
1039 }
1040}
1041
1042#[derive(Clone, Debug, Default, Eq, PartialEq)]
1047pub enum VaultFetch {
1048 #[default]
1050 Skip,
1051 Always,
1053 IfChangedFrom(Word),
1058 Assets(BTreeSet<AssetId>),
1065}
1066
1067impl Serializable for VaultFetch {
1068 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1069 match self {
1070 Self::Skip => target.write_u8(0),
1071 Self::Always => target.write_u8(1),
1072 Self::IfChangedFrom(root) => {
1073 target.write_u8(2);
1074 target.write(root);
1075 },
1076 Self::Assets(asset_ids) => {
1077 target.write_u8(3);
1078 target.write(asset_ids);
1079 },
1080 }
1081 }
1082}
1083
1084impl Deserializable for VaultFetch {
1085 fn read_from<R: miden_tx::utils::serde::ByteReader>(
1086 source: &mut R,
1087 ) -> Result<Self, DeserializationError> {
1088 match source.read_u8()? {
1089 0 => Ok(Self::Skip),
1090 1 => Ok(Self::Always),
1091 2 => Ok(Self::IfChangedFrom(source.read()?)),
1092 3 => Ok(Self::Assets(source.read()?)),
1093 tag => Err(DeserializationError::InvalidValue(format!(
1094 "invalid vault requirements tag: {tag}"
1095 ))),
1096 }
1097 }
1098}
1099
1100impl From<VaultFetch> for Option<VaultRequest> {
1101 fn from(vault: VaultFetch) -> Self {
1105 let commitment = match vault {
1106 VaultFetch::Skip => return None,
1107 VaultFetch::Always => EMPTY_WORD,
1108 VaultFetch::IfChangedFrom(commitment) => commitment,
1109 VaultFetch::Assets(asset_ids) => {
1110 return Some(VaultRequest::AssetIds(VaultAssetIds {
1111 asset_ids: asset_ids.into_iter().map(Into::into).collect(),
1112 }));
1113 },
1114 };
1115 Some(VaultRequest::AssetVaultCommitment(commitment.into()))
1116 }
1117}
1118
1119#[derive(Clone, Debug, Default, Eq, PartialEq)]
1125pub enum StorageMapFetch {
1126 #[default]
1128 Skip,
1129 All,
1133 Slots(AccountStorageRequirements),
1136}
1137
1138impl Serializable for StorageMapFetch {
1139 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1140 match self {
1141 Self::Skip => target.write_u8(0),
1142 Self::All => target.write_u8(1),
1143 Self::Slots(requirements) => {
1144 target.write_u8(2);
1145 target.write(requirements);
1146 },
1147 }
1148 }
1149}
1150
1151impl Deserializable for StorageMapFetch {
1152 fn read_from<R: miden_tx::utils::serde::ByteReader>(
1153 source: &mut R,
1154 ) -> Result<Self, DeserializationError> {
1155 match source.read_u8()? {
1156 0 => Ok(Self::Skip),
1157 1 => Ok(Self::All),
1158 2 => Ok(Self::Slots(source.read()?)),
1159 tag => Err(DeserializationError::InvalidValue(format!(
1160 "invalid storage map fetch tag: {tag}"
1161 ))),
1162 }
1163 }
1164}
1165
1166impl From<StorageMapFetch> for Option<StorageRequest> {
1167 fn from(storage: StorageMapFetch) -> Self {
1168 match storage {
1169 StorageMapFetch::Skip => None,
1170 StorageMapFetch::All => Some(StorageRequest::AllStorageMaps(true)),
1171 StorageMapFetch::Slots(reqs) => {
1172 Some(StorageRequest::StorageMaps(StorageMapDetailRequests {
1173 storage_maps: reqs.into(),
1174 }))
1175 },
1176 }
1177 }
1178}
1179
1180#[derive(Clone, Debug, Default, Eq, PartialEq)]
1185pub struct AccountFetch {
1186 storage: StorageMapFetch,
1187 vault: VaultFetch,
1188}
1189
1190impl AccountFetch {
1191 #[must_use]
1193 pub fn new() -> Self {
1194 Self::default()
1195 }
1196
1197 #[must_use]
1199 pub fn with_storage(mut self, storage: StorageMapFetch) -> Self {
1200 self.storage = storage;
1201 self
1202 }
1203
1204 #[must_use]
1206 pub fn with_vault(mut self, vault: VaultFetch) -> Self {
1207 self.vault = vault;
1208 self
1209 }
1210
1211 pub fn storage(&self) -> &StorageMapFetch {
1213 &self.storage
1214 }
1215
1216 pub fn vault(&self) -> &VaultFetch {
1218 &self.vault
1219 }
1220}
1221
1222impl Serializable for AccountFetch {
1223 fn write_into<W: miden_tx::utils::serde::ByteWriter>(&self, target: &mut W) {
1224 target.write(&self.storage);
1225 target.write(&self.vault);
1226 }
1227}
1228
1229impl Deserializable for AccountFetch {
1230 fn read_from<R: miden_tx::utils::serde::ByteReader>(
1231 source: &mut R,
1232 ) -> Result<Self, DeserializationError> {
1233 Ok(Self {
1234 storage: source.read()?,
1235 vault: source.read()?,
1236 })
1237 }
1238}
1239
1240#[derive(Clone, Debug)]
1245pub struct GetAccountRequest {
1246 pub account_id: AccountId,
1248 pub fetch: AccountFetch,
1250 pub at: AccountStateAt,
1252 pub known_code: Option<AccountCode>,
1254}
1255
1256impl GetAccountRequest {
1257 #[must_use]
1259 pub fn new(account_id: AccountId) -> Self {
1260 Self {
1261 account_id,
1262 fetch: AccountFetch::default(),
1263 at: AccountStateAt::ChainTip,
1264 known_code: None,
1265 }
1266 }
1267
1268 #[must_use]
1270 pub fn with_fetch(mut self, fetch: AccountFetch) -> Self {
1271 self.fetch = fetch;
1272 self
1273 }
1274
1275 #[must_use]
1277 pub fn at(mut self, at: AccountStateAt) -> Self {
1278 self.at = at;
1279 self
1280 }
1281
1282 #[must_use]
1285 pub fn with_known_code(mut self, known_code: Option<AccountCode>) -> Self {
1286 self.known_code = known_code;
1287 self
1288 }
1289}
1290
1291#[derive(Debug, Error)]
1295pub enum AccountProofError {
1296 #[error(
1297 "the received account commitment doesn't match the received account header's commitment"
1298 )]
1299 InconsistentAccountCommitment,
1300 #[error("the received account id doesn't match the received account header's id")]
1301 InconsistentAccountId,
1302 #[error(
1303 "the received code commitment doesn't match the received account header's code commitment"
1304 )]
1305 InconsistentCodeCommitment,
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310 use miden_protocol::Word;
1311 use miden_protocol::account::{StorageMapKey, StorageSlotName};
1312 use miden_protocol::asset::AssetId;
1313 use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
1314 use miden_tx::utils::serde::{Deserializable, Serializable};
1315
1316 use super::{AccountFetch, AccountStorageRequirements, StorageMapFetch, VaultFetch, proto};
1317 use crate::alloc::string::ToString;
1318
1319 #[test]
1320 fn invitation_code_validation_request_debug_hides_code() {
1321 let code = "secret-invitation-code";
1322 let request =
1323 proto::rpc::IsInvitationCodeValidRequest { invitation_code: code.to_string() };
1324
1325 let debug = format!("{request:?}");
1326 assert!(debug.contains("IsInvitationCodeValidRequest"));
1327 assert!(!debug.contains(code));
1328 }
1329
1330 #[test]
1331 fn account_fetch_roundtrip() {
1332 let slot = StorageSlotName::new("miden::testing::map").unwrap();
1333 let key = StorageMapKey::new(Word::from([1u32; 4]));
1334 let storage = AccountStorageRequirements::new([(slot, &[key])]);
1335 let asset_id = AssetId::new_fungible(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap());
1336
1337 for storage in
1338 [StorageMapFetch::Skip, StorageMapFetch::All, StorageMapFetch::Slots(storage)]
1339 {
1340 let fetch = AccountFetch::new().with_storage(storage);
1341 for fetch in [
1342 fetch.clone(),
1343 fetch.clone().with_vault(VaultFetch::Skip),
1344 fetch.clone().with_vault(VaultFetch::Always),
1345 fetch.clone().with_vault(VaultFetch::IfChangedFrom(Word::from([2u32; 4]))),
1346 fetch.with_vault(VaultFetch::Assets([asset_id].into())),
1347 ] {
1348 let decoded = AccountFetch::read_from_bytes(&fetch.to_bytes()).unwrap();
1349 assert_eq!(decoded, fetch);
1350 }
1351 }
1352 }
1353}
1354
1355#[cfg(all(test, feature = "testing", feature = "tonic"))]
1356mod vault_details_tests {
1357 use alloc::collections::BTreeSet;
1358 use alloc::vec::Vec;
1359
1360 use miden_protocol::account::AccountId;
1361 use miden_protocol::asset::{Asset, AssetId, AssetVault, FungibleAsset, PartialVault};
1362 use miden_protocol::testing::account_id::{
1363 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1364 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1,
1365 };
1366
1367 use super::{AccountVaultDetails, VaultAssetIds, VaultFetch, VaultRequest, proto};
1368
1369 fn vault_with_one_asset() -> (AssetVault, Asset, AssetId) {
1372 let held_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
1373 let absent_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1374 let asset: Asset = FungibleAsset::new(held_faucet, 100).unwrap().into();
1375 let vault = AssetVault::new(&[asset]).unwrap();
1376 (vault, asset, AssetId::new_fungible(absent_faucet))
1377 }
1378
1379 fn partial_vault(vault: &AssetVault, asset_ids: &[AssetId]) -> PartialVault {
1381 PartialVault::with_witnesses(asset_ids.iter().map(|asset_id| vault.open(*asset_id)))
1382 .unwrap()
1383 }
1384
1385 fn proof_details(vault: &AssetVault, asset_ids: &[AssetId]) -> AccountVaultDetails {
1387 AccountVaultDetails::Partial(partial_vault(vault, asset_ids))
1388 }
1389
1390 fn asset_request(asset_ids: &[AssetId]) -> VaultFetch {
1391 VaultFetch::Assets(asset_ids.iter().copied().collect())
1392 }
1393
1394 #[test]
1395 fn asset_request_encodes_the_asset_ids() {
1396 let (_, asset, absent) = vault_with_one_asset();
1397 let asset_ids = BTreeSet::from([asset.id(), absent]);
1398
1399 let request: Option<VaultRequest> = VaultFetch::Assets(asset_ids.clone()).into();
1400
1401 let expected = asset_ids.into_iter().map(Into::into).collect();
1402 assert_eq!(request, Some(VaultRequest::AssetIds(VaultAssetIds { asset_ids: expected })));
1403 }
1404
1405 #[test]
1406 fn proofs_that_cover_the_request_are_accepted() {
1407 let (vault, asset, absent) = vault_with_one_asset();
1408 let request = asset_request(&[asset.id(), absent]);
1409
1410 let details = proof_details(&vault, &[asset.id(), absent])
1411 .validate_against_request(vault.root(), &request)
1412 .unwrap();
1413
1414 let partial_vault = details.into_partial_vault().expect("expected vault proofs");
1415 assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
1416 assert_eq!(partial_vault.get(absent).unwrap(), None);
1417 }
1418
1419 #[test]
1420 fn proofs_against_another_root_are_rejected() {
1421 let (vault, asset, _) = vault_with_one_asset();
1422 let other_root = AssetVault::default().root();
1423
1424 let result = proof_details(&vault, &[asset.id()])
1425 .validate_against_request(other_root, &asset_request(&[asset.id()]));
1426
1427 assert!(result.is_err());
1428 }
1429
1430 #[test]
1433 fn proofs_that_miss_a_requested_asset_are_rejected() {
1434 let (_, asset, _) = vault_with_one_asset();
1435 let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1436 let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
1437 let vault = AssetVault::new(&[asset, other_asset]).unwrap();
1438
1439 let result = proof_details(&vault, &[asset.id()]).validate_against_request(
1440 vault.root(),
1441 &asset_request(&[asset.id(), other_asset.id()]),
1442 );
1443
1444 assert!(result.is_err());
1445 }
1446
1447 #[test]
1448 fn proofs_that_were_not_requested_are_rejected() {
1449 let (vault, asset, _) = vault_with_one_asset();
1450
1451 let result = proof_details(&vault, &[asset.id()])
1452 .validate_against_request(vault.root(), &VaultFetch::Always);
1453
1454 assert!(result.is_err());
1455 }
1456
1457 #[test]
1460 fn full_vault_answer_is_converted_to_proofs() {
1461 let (vault, asset, absent) = vault_with_one_asset();
1462 let details = AccountVaultDetails::Assets(vec![asset])
1463 .validate_against_request(vault.root(), &asset_request(&[asset.id(), absent]))
1464 .unwrap();
1465
1466 let partial_vault = details.into_partial_vault().expect("expected vault proofs");
1467 assert_eq!(partial_vault.get(asset.id()).unwrap(), Some(asset));
1468 assert_eq!(partial_vault.get(absent).unwrap(), None);
1469 }
1470
1471 #[test]
1472 fn too_many_assets_answer_to_an_asset_request_is_rejected() {
1473 let (vault, asset, _) = vault_with_one_asset();
1474 let result = AccountVaultDetails::TooManyAssets
1475 .validate_against_request(vault.root(), &asset_request(&[asset.id()]));
1476
1477 assert!(result.is_err());
1478 }
1479
1480 #[test]
1481 fn partial_vault_round_trips_through_proto() {
1482 let (vault, asset, absent) = vault_with_one_asset();
1483 let expected = partial_vault(&vault, &[asset.id(), absent]);
1484 let response = proto::rpc::AccountVaultDetails {
1485 too_many_assets: false,
1486 assets: Vec::new(),
1487 partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1488 asset_ids: vec![asset.id().into(), absent.into()],
1489 partial_smt: Some(expected.partial_smt().clone().into()),
1490 }),
1491 };
1492
1493 let details = AccountVaultDetails::try_from(response).unwrap();
1494
1495 assert_eq!(details.into_partial_vault(), Some(expected));
1496 }
1497
1498 #[test]
1499 fn partial_vault_with_an_asset_list_is_rejected() {
1500 let (vault, asset, _) = vault_with_one_asset();
1501 let partial_vault = partial_vault(&vault, &[asset.id()]);
1502 let response = proto::rpc::AccountVaultDetails {
1503 too_many_assets: false,
1504 assets: vec![asset.into()],
1505 partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1506 asset_ids: vec![asset.id().into()],
1507 partial_smt: Some(partial_vault.partial_smt().clone().into()),
1508 }),
1509 };
1510
1511 assert!(AccountVaultDetails::try_from(response).is_err());
1512 }
1513
1514 #[test]
1515 fn too_many_assets_flag_converts_to_its_variant() {
1516 let response = proto::rpc::AccountVaultDetails {
1517 too_many_assets: true,
1518 assets: Vec::new(),
1519 partial_vault: None,
1520 };
1521
1522 let details = AccountVaultDetails::try_from(response).unwrap();
1523
1524 assert!(details.is_too_many_assets());
1525 }
1526
1527 #[test]
1528 fn too_many_assets_flag_with_an_asset_list_is_rejected() {
1529 let (_, asset, _) = vault_with_one_asset();
1530 let response = proto::rpc::AccountVaultDetails {
1531 too_many_assets: true,
1532 assets: vec![asset.into()],
1533 partial_vault: None,
1534 };
1535
1536 assert!(AccountVaultDetails::try_from(response).is_err());
1537 }
1538
1539 #[test]
1540 fn partial_vault_with_the_too_many_assets_flag_is_rejected() {
1541 let (vault, asset, _) = vault_with_one_asset();
1542 let partial_vault = partial_vault(&vault, &[asset.id()]);
1543 let response = proto::rpc::AccountVaultDetails {
1544 too_many_assets: true,
1545 assets: Vec::new(),
1546 partial_vault: Some(proto::rpc::account_vault_details::PartialVault {
1547 asset_ids: vec![asset.id().into()],
1548 partial_smt: Some(partial_vault.partial_smt().clone().into()),
1549 }),
1550 };
1551
1552 assert!(AccountVaultDetails::try_from(response).is_err());
1553 }
1554
1555 #[test]
1556 fn proofs_that_track_an_asset_that_was_not_requested_are_rejected() {
1557 let (_, asset, _) = vault_with_one_asset();
1558 let other_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_1).unwrap();
1559 let other_asset: Asset = FungibleAsset::new(other_faucet, 5).unwrap().into();
1560 let vault = AssetVault::new(&[asset, other_asset]).unwrap();
1561
1562 let result = proof_details(&vault, &[asset.id(), other_asset.id()])
1563 .validate_against_request(vault.root(), &asset_request(&[asset.id()]));
1564
1565 assert!(result.is_err());
1566 }
1567}