Skip to main content

miden_node_proto/domain/
account.rs

1use std::collections::HashSet;
2use std::fmt::{Debug, Formatter};
3
4use miden_node_utils::limiter::{QueryParamLimiter, QueryParamStorageMapKeyTotalLimit};
5use miden_protobuf::{ConversionResultExt, DecodeMessage, Decoded, RequiredField, Verify, decode};
6use miden_protocol::Word;
7#[cfg(test)]
8use miden_protocol::account::StorageSlotHeader;
9use miden_protocol::account::{
10    Account,
11    AccountCode,
12    AccountHeader,
13    AccountId,
14    AccountStorageHeader,
15    StorageMap,
16    StorageMapKey,
17    StorageSlotName,
18    StorageSlotType,
19};
20use miden_protocol::asset::Asset;
21use miden_protocol::block::BlockNumber;
22use miden_protocol::block::account_tree::AccountWitness;
23use miden_protocol::crypto::merkle::MerkleError;
24use miden_protocol::crypto::merkle::smt::{PartialSmt, SmtProof};
25
26use crate::errors::ConversionError;
27use crate::generated::{self as proto};
28
29#[cfg(test)]
30mod tests;
31
32// ACCOUNT UPDATE
33// ================================================================================================
34
35#[derive(Debug, PartialEq)]
36pub struct AccountSummary {
37    pub account_id: AccountId,
38    pub account_commitment: Word,
39    pub block_num: BlockNumber,
40}
41
42#[derive(Debug, PartialEq)]
43pub struct AccountInfo {
44    pub summary: AccountSummary,
45    pub details: Option<Account>,
46}
47
48// REGISTER ACCOUNT REQUEST
49// ================================================================================================
50
51impl Debug for proto::miden::node::v1::RegisterAccountRequest {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("RegisterAccountRequest")
54            .field("account_id", &self.account_id)
55            .finish_non_exhaustive()
56    }
57}
58
59/// Decoded registration fields. The invitation code remains unvalidated.
60#[must_use = "decoded fields have not been verified"]
61pub struct DecodedRegisterAccountRequest {
62    pub invitation_code: String,
63    pub account_id: Decoded<proto::account::AccountId>,
64}
65
66impl Debug for DecodedRegisterAccountRequest {
67    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("DecodedRegisterAccountRequest")
69            .field("account_id", &self.account_id)
70            .finish_non_exhaustive()
71    }
72}
73
74impl DecodeMessage for proto::miden::node::v1::RegisterAccountRequest {
75    type Decoded = DecodedRegisterAccountRequest;
76}
77
78impl TryFrom<proto::miden::node::v1::RegisterAccountRequest> for DecodedRegisterAccountRequest {
79    type Error = ConversionError;
80
81    fn try_from(
82        request: proto::miden::node::v1::RegisterAccountRequest,
83    ) -> Result<Self, Self::Error> {
84        Ok(Self {
85            invitation_code: request.invitation_code,
86            account_id: decode(
87                RequiredField::<proto::miden::node::v1::RegisterAccountRequest, _>::new(
88                    "account_id",
89                    request.account_id,
90                ),
91            )?,
92        })
93    }
94}
95
96/// A registration request with a verified account ID. The invitation code remains unvalidated.
97pub struct RegisterAccountRequest {
98    pub invitation_code: String,
99    pub account_id: AccountId,
100}
101
102impl Debug for RegisterAccountRequest {
103    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("RegisterAccountRequest")
105            .field("account_id", &self.account_id)
106            .finish_non_exhaustive()
107    }
108}
109
110impl Verify for DecodedRegisterAccountRequest {
111    type Verified = RegisterAccountRequest;
112    type Error = ConversionError;
113
114    fn verify(self) -> Result<Self::Verified, Self::Error> {
115        Ok(RegisterAccountRequest {
116            invitation_code: self.invitation_code,
117            account_id: self.account_id.verify().context("account_id")?,
118        })
119    }
120}
121
122impl Verify for proto::miden::node::v1::DecodedIsAccountAllowedRequest {
123    type Verified = AccountId;
124    type Error = ConversionError;
125
126    fn verify(self) -> Result<Self::Verified, Self::Error> {
127        self.account_id.verify().context("account_id")
128    }
129}
130
131// IS INVITATION CODE VALID REQUEST
132// ================================================================================================
133
134impl Debug for proto::miden::node::v1::IsInvitationCodeValidRequest {
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("IsInvitationCodeValidRequest").finish_non_exhaustive()
137    }
138}
139
140/// Decoded invitation check fields. The invitation code remains unvalidated.
141#[must_use = "decoded fields have not been verified"]
142pub struct DecodedIsInvitationCodeValidRequest {
143    pub invitation_code: String,
144}
145
146impl Debug for DecodedIsInvitationCodeValidRequest {
147    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("DecodedIsInvitationCodeValidRequest").finish_non_exhaustive()
149    }
150}
151
152impl DecodeMessage for proto::miden::node::v1::IsInvitationCodeValidRequest {
153    type Decoded = DecodedIsInvitationCodeValidRequest;
154}
155
156impl TryFrom<proto::miden::node::v1::IsInvitationCodeValidRequest>
157    for DecodedIsInvitationCodeValidRequest
158{
159    type Error = ConversionError;
160
161    fn try_from(
162        request: proto::miden::node::v1::IsInvitationCodeValidRequest,
163    ) -> Result<Self, Self::Error> {
164        Ok(Self { invitation_code: request.invitation_code })
165    }
166}
167
168/// Returns the raw invitation code. The handler validates the code because an empty code is
169/// accepted when allowlist enforcement is disabled.
170impl Verify for DecodedIsInvitationCodeValidRequest {
171    type Verified = String;
172    type Error = ConversionError;
173
174    fn verify(self) -> Result<Self::Verified, Self::Error> {
175        Ok(self.invitation_code)
176    }
177}
178
179// ACCOUNT REQUEST
180// ================================================================================================
181
182/// Represents a request for an account proof.
183#[derive(Debug)]
184pub struct GetAccountRequest {
185    pub account_id: AccountId,
186    // If not present, the latest account proof references the latest available
187    pub block_num: Option<BlockNumber>,
188    pub details: Option<AccountDetailRequest>,
189}
190
191impl Verify for proto::miden::node::v1::DecodedGetAccountRequest {
192    type Verified = GetAccountRequest;
193    type Error = ConversionError;
194
195    fn verify(self) -> Result<Self::Verified, Self::Error> {
196        let Self { account_id, block_num, details } = self;
197
198        let account_id = account_id.verify().context("account_id")?;
199        let block_num = block_num.verify()?;
200
201        let details = details.verify()?;
202
203        Ok(GetAccountRequest { account_id, block_num, details })
204    }
205}
206
207/// Represents a request for account details alongside specific storage data.
208#[derive(Debug)]
209pub struct AccountDetailRequest {
210    pub code_commitment: Option<Word>,
211    pub asset_vault_commitment: Option<Word>,
212    pub storage_request: AccountStorageRequest,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum AccountStorageRequest {
217    None,
218    AllStorageMaps,
219    Explicit(Vec<StorageMapRequest>),
220}
221
222impl Verify for proto::miden::node::v1::get_account_request::DecodedAccountDetailRequest {
223    type Verified = AccountDetailRequest;
224    type Error = ConversionError;
225
226    fn verify(self) -> Result<Self::Verified, Self::Error> {
227        use proto::miden::node::v1::get_account_request::account_detail_request::DecodedStorageRequest as ProtoStorageRequest;
228
229        let Self {
230            code_commitment,
231            asset_vault_commitment,
232            storage_request,
233        } = self;
234
235        let storage_request = match storage_request.into_inner() {
236            None => AccountStorageRequest::None,
237            Some(ProtoStorageRequest::AllStorageMaps(true)) => {
238                AccountStorageRequest::AllStorageMaps
239            },
240            Some(ProtoStorageRequest::AllStorageMaps(false)) => {
241                return Err(ConversionError::message("all_storage_maps must be true when set"));
242            },
243            Some(ProtoStorageRequest::StorageMaps(requests)) => {
244                let requests =
245                    requests.storage_maps.verify().context("storage_request.storage_maps")?;
246                AccountStorageRequest::Explicit(requests)
247            },
248        };
249
250        Ok(AccountDetailRequest {
251            code_commitment: code_commitment.into_inner(),
252            asset_vault_commitment: asset_vault_commitment.into_inner(),
253            storage_request,
254        })
255    }
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct StorageMapRequest {
260    pub slot_name: StorageSlotName,
261    pub slot_data: SlotData,
262}
263
264impl Verify
265    for proto::miden::node::v1::get_account_request::account_detail_request::DecodedStorageMapDetailRequest
266{
267    type Verified = StorageMapRequest;
268    type Error = ConversionError;
269
270    fn verify(self) -> Result<Self::Verified, Self::Error> {
271        let Self { slot_name, slot_data } = self;
272
273        let slot_name = StorageSlotName::new(slot_name).context("slot_name")?;
274        let slot_data = slot_data.verify().context("slot_data")?;
275
276        Ok(StorageMapRequest { slot_name, slot_data })
277    }
278}
279
280/// Request of slot data values.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum SlotData {
283    All,
284    MapKeys(Vec<StorageMapKey>),
285}
286
287impl Verify for proto::miden::node::v1::get_account_request::account_detail_request::storage_map_detail_request::DecodedSlotData {
288    type Verified = SlotData;
289    type Error = ConversionError;
290
291    fn verify(self) -> Result<Self::Verified, Self::Error> {
292        use proto::miden::node::v1::get_account_request::account_detail_request::storage_map_detail_request::DecodedSlotData as ProtoSlotData;
293
294        Ok(match self {
295            ProtoSlotData::AllEntries(true) => SlotData::All,
296            ProtoSlotData::AllEntries(false) => {
297                return Err(ConversionError::message("enum variant discriminant out of range"));
298            },
299            ProtoSlotData::MapKeys(keys) => {
300                let keys = keys
301                    .map_keys
302                    .map(StorageMapKey::new);
303                if has_duplicate_storage_map_keys(&keys) {
304                    return Err(ConversionError::message(
305                        "storage map key request contains duplicate keys",
306                    ));
307                }
308                SlotData::MapKeys(keys)
309            },
310        })
311    }
312}
313
314fn has_duplicate_storage_map_keys(keys: &[StorageMapKey]) -> bool {
315    let mut seen = HashSet::with_capacity(keys.len());
316    keys.iter().any(|key| !seen.insert(key))
317}
318
319// ACCOUNT VAULT DETAILS
320//================================================================================================
321
322/// Account vault details
323///
324/// When an account contains a large number of assets (>
325/// [`AccountVaultDetails::MAX_RETURN_ENTRIES`]), including all assets in a single RPC response
326/// creates performance issues. In such cases, the `LimitExceeded` variant indicates to the client
327/// to use the `SyncAccountVault` endpoint instead.
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub enum AccountVaultDetails {
330    /// The vault has too many assets to return inline. Clients must use `SyncAccountVault` endpoint
331    /// instead.
332    LimitExceeded,
333
334    /// The assets in the vault (up to `MAX_RETURN_ENTRIES`).
335    Assets(Vec<Asset>),
336}
337
338impl AccountVaultDetails {
339    /// Maximum number of vault entries that can be returned in a single response. Accounts with
340    /// more assets will have `LimitExceeded` variant.
341    pub const MAX_RETURN_ENTRIES: usize = 1000;
342
343    pub fn empty() -> Self {
344        Self::Assets(Vec::new())
345    }
346
347    /// Creates `AccountVaultDetails` from a list of assets.
348    pub fn from_assets(assets: Vec<Asset>) -> Self {
349        if assets.len() > Self::MAX_RETURN_ENTRIES {
350            Self::LimitExceeded
351        } else {
352            Self::Assets(assets)
353        }
354    }
355}
356
357impl Verify for proto::miden::node::v1::DecodedAccountVaultDetails {
358    type Verified = AccountVaultDetails;
359    type Error = ConversionError;
360
361    fn verify(self) -> Result<Self::Verified, Self::Error> {
362        let Self { too_many_assets, assets } = self;
363
364        if too_many_assets {
365            Ok(AccountVaultDetails::LimitExceeded)
366        } else {
367            let parsed_assets = assets.verify()?;
368            Ok(AccountVaultDetails::Assets(parsed_assets))
369        }
370    }
371}
372
373impl From<AccountVaultDetails> for proto::miden::node::v1::AccountVaultDetails {
374    fn from(value: AccountVaultDetails) -> Self {
375        match value {
376            AccountVaultDetails::LimitExceeded => Self {
377                too_many_assets: true,
378                assets: Vec::new(),
379            },
380            AccountVaultDetails::Assets(assets) => Self {
381                too_many_assets: false,
382                assets: assets.into_iter().map(proto::asset::Asset::from).collect::<Vec<_>>(),
383            },
384        }
385    }
386}
387
388// ACCOUNT STORAGE MAP DETAILS
389//================================================================================================
390
391/// Details about an account storage map slot.
392#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct AccountStorageMapDetails {
394    pub slot_name: StorageSlotName,
395    pub entries: StorageMapEntries,
396}
397
398/// Storage map entries for an account storage slot.
399///
400/// When a storage map contains many entries (> [`AccountStorageMapDetails::MAX_RETURN_ENTRIES`]),
401/// returning all entries in a single RPC response creates performance issues. In such cases,
402/// the `LimitExceeded` variant indicates to the client to use the `SyncAccountStorageMaps` endpoint
403/// instead.
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub enum StorageMapEntries {
406    /// The map has too many entries to return inline. Clients must use `SyncAccountStorageMaps`
407    /// endpoint instead.
408    LimitExceeded,
409
410    /// All storage map entries (key-value pairs) without proofs. Used when all entries are
411    /// requested for small maps.
412    AllEntries(Vec<(StorageMapKey, Word)>),
413
414    /// Specific raw map keys covered by a single partial SMT. Used when specific keys are requested
415    /// from the storage map.
416    PartialMap {
417        map_keys: Vec<StorageMapKey>,
418        partial_smt: PartialSmt,
419    },
420}
421
422impl AccountStorageMapDetails {
423    /// Maximum number of storage map entries that can be returned in a single response.
424    pub const MAX_RETURN_ENTRIES: usize = 1000;
425
426    /// Maximum number of SMT proofs that can be returned in a single response.
427    ///
428    /// This limit is more restrictive than [`Self::MAX_RETURN_ENTRIES`] because SMT proofs
429    /// are larger (up to 64 inner nodes each) and more CPU-intensive to generate.
430    ///
431    /// This is defined by [`QueryParamStorageMapKeyTotalLimit::LIMIT`] and used both in RPC
432    /// validation and store-level enforcement to ensure consistent limits.
433    pub const MAX_SMT_PROOF_ENTRIES: usize = QueryParamStorageMapKeyTotalLimit::LIMIT;
434
435    /// Creates storage map details with all entries from the storage map.
436    ///
437    /// If the storage map has too many entries (> `MAX_RETURN_ENTRIES`),
438    /// returns `LimitExceeded` variant.
439    pub fn from_all_entries(slot_name: StorageSlotName, storage_map: &StorageMap) -> Self {
440        if storage_map.num_entries() > Self::MAX_RETURN_ENTRIES {
441            Self {
442                slot_name,
443                entries: StorageMapEntries::LimitExceeded,
444            }
445        } else {
446            let entries = storage_map.entries().map(|(k, v)| (*k, *v)).collect::<Vec<_>>();
447            Self {
448                slot_name,
449                entries: StorageMapEntries::AllEntries(entries),
450            }
451        }
452    }
453
454    /// Creates storage map details from forest-queried entries.
455    ///
456    /// Returns `LimitExceeded` if too many entries.
457    pub fn from_forest_entries(
458        slot_name: StorageSlotName,
459        entries: Vec<(StorageMapKey, Word)>,
460    ) -> Self {
461        if entries.len() > Self::MAX_RETURN_ENTRIES {
462            Self {
463                slot_name,
464                entries: StorageMapEntries::LimitExceeded,
465            }
466        } else {
467            Self {
468                slot_name,
469                entries: StorageMapEntries::AllEntries(entries),
470            }
471        }
472    }
473
474    /// Creates storage map details from pre-computed SMT proofs.
475    ///
476    /// Use this when the caller has already obtained the proofs from an `SmtForest`.
477    /// Returns `LimitExceeded` if too many proofs are provided.
478    pub fn from_proofs(
479        slot_name: StorageSlotName,
480        map_root: Word,
481        map_keys: Vec<StorageMapKey>,
482        proofs: Vec<SmtProof>,
483    ) -> Result<Self, MerkleError> {
484        if map_keys.len() != proofs.len() {
485            return Err(MerkleError::InternalError(format!(
486                "storage map key count {} does not match proof count {}",
487                map_keys.len(),
488                proofs.len()
489            )));
490        }
491        if has_duplicate_storage_map_keys(&map_keys) {
492            return Err(MerkleError::InternalError(
493                "storage map key list contains duplicate keys".into(),
494            ));
495        }
496
497        if map_keys.len() > Self::MAX_SMT_PROOF_ENTRIES {
498            return Ok(Self {
499                slot_name,
500                entries: StorageMapEntries::LimitExceeded,
501            });
502        }
503
504        let partial_smt = if proofs.is_empty() {
505            PartialSmt::new(map_root)
506        } else {
507            PartialSmt::from_proofs(proofs)?
508        };
509
510        if partial_smt.root() != map_root {
511            return Err(MerkleError::ConflictingRoots {
512                expected_root: map_root,
513                actual_root: partial_smt.root(),
514            });
515        }
516
517        for map_key in &map_keys {
518            partial_smt.get_value(&map_key.hash().as_word())?;
519        }
520
521        Ok(Self {
522            slot_name,
523            entries: StorageMapEntries::PartialMap { map_keys, partial_smt },
524        })
525    }
526
527    /// Creates storage map details indicating the limit was exceeded.
528    pub fn limit_exceeded(slot_name: StorageSlotName) -> Self {
529        Self {
530            slot_name,
531            entries: StorageMapEntries::LimitExceeded,
532        }
533    }
534}
535
536impl Verify for proto::miden::node::v1::account_storage_details::DecodedAccountStorageMapDetails {
537    type Verified = AccountStorageMapDetails;
538    type Error = ConversionError;
539
540    fn verify(self) -> Result<Self::Verified, Self::Error> {
541        use proto::miden::node::v1::account_storage_details::account_storage_map_details::{
542            DecodedAllMapEntries,
543            DecodedPartialStorageMap,
544            DecodedResult as ProtoResult,
545        };
546
547        let Self { slot_name, result } = self;
548
549        let slot_name = StorageSlotName::new(slot_name).context("slot_name")?;
550
551        let entries = match result {
552            ProtoResult::TooManyEntries(true) => StorageMapEntries::LimitExceeded,
553            ProtoResult::TooManyEntries(false) => {
554                return Err(ConversionError::message("too_many_entries must be true when set"));
555            },
556            ProtoResult::AllEntries(DecodedAllMapEntries { entries }) => {
557                let entries = entries.map(|entry| (StorageMapKey::new(entry.key), entry.value));
558                StorageMapEntries::AllEntries(entries)
559            },
560            ProtoResult::PartialMap(DecodedPartialStorageMap { map_keys, partial_smt }) => {
561                let map_keys = map_keys.into_inner();
562                if map_keys.len() > AccountStorageMapDetails::MAX_SMT_PROOF_ENTRIES {
563                    return Err(ConversionError::message(format!(
564                        "partial storage map contains {} keys, exceeding the limit of {}",
565                        map_keys.len(),
566                        AccountStorageMapDetails::MAX_SMT_PROOF_ENTRIES
567                    )));
568                }
569                let map_keys = map_keys.into_iter().map(StorageMapKey::new).collect::<Vec<_>>();
570                if has_duplicate_storage_map_keys(&map_keys) {
571                    return Err(ConversionError::message(
572                        "partial storage map contains duplicate keys",
573                    ));
574                }
575                let partial_smt: PartialSmt = partial_smt.verify().context("partial_smt")?;
576                for map_key in &map_keys {
577                    partial_smt.get_value(&map_key.hash().as_word()).context("map_keys")?;
578                }
579                StorageMapEntries::PartialMap { map_keys, partial_smt }
580            },
581        };
582
583        Ok(Self::Verified { slot_name, entries })
584    }
585}
586
587impl From<AccountStorageMapDetails>
588    for proto::miden::node::v1::account_storage_details::AccountStorageMapDetails
589{
590    fn from(value: AccountStorageMapDetails) -> Self {
591        use proto::miden::node::v1::account_storage_details::account_storage_map_details::{
592            AllMapEntries,
593            PartialStorageMap,
594            Result as ProtoResult,
595        };
596
597        let AccountStorageMapDetails { slot_name, entries } = value;
598
599        let result = match entries {
600            StorageMapEntries::LimitExceeded => ProtoResult::TooManyEntries(true),
601            StorageMapEntries::AllEntries(entries) => {
602                let all = AllMapEntries {
603                    entries: entries.into_iter().map(|(key, value)| {
604                        proto::miden::node::v1::account_storage_details::account_storage_map_details::all_map_entries::StorageMapEntry {
605                            key: Some(Word::from(key).into()),
606                            value: Some(value.into()),
607                        }
608                    }).collect::<Vec<_>>(),
609                };
610                ProtoResult::AllEntries(all)
611            },
612            StorageMapEntries::PartialMap { map_keys, partial_smt } => {
613                ProtoResult::PartialMap(PartialStorageMap {
614                    map_keys: map_keys
615                        .into_iter()
616                        .map(|key| proto::primitives::Word::from(Word::from(key)))
617                        .collect(),
618                    partial_smt: Some(partial_smt.into()),
619                })
620            },
621        };
622
623        Self {
624            slot_name: slot_name.to_string(),
625            result: Some(result),
626        }
627    }
628}
629
630#[derive(Debug, Clone, PartialEq)]
631pub struct AccountStorageDetails {
632    pub header: AccountStorageHeader,
633    pub map_details: Vec<AccountStorageMapDetails>,
634}
635
636impl AccountStorageDetails {
637    /// Creates storage details where all map slots indicate limit exceeded.
638    pub fn all_limits_exceeded(
639        header: AccountStorageHeader,
640        slot_names: impl IntoIterator<Item = StorageSlotName>,
641    ) -> Self {
642        Self {
643            header,
644            map_details: slot_names
645                .into_iter()
646                .map(AccountStorageMapDetails::limit_exceeded)
647                .collect::<Vec<_>>(),
648        }
649    }
650}
651
652impl Verify for proto::miden::node::v1::DecodedAccountStorageDetails {
653    type Verified = AccountStorageDetails;
654    type Error = ConversionError;
655
656    fn verify(self) -> Result<Self::Verified, Self::Error> {
657        let Self { header, map_details } = self;
658
659        let header: AccountStorageHeader = header.verify().context("header")?;
660
661        let map_details = map_details.verify()?;
662
663        for map_detail in &map_details {
664            let StorageMapEntries::PartialMap { partial_smt, .. } = &map_detail.entries else {
665                continue;
666            };
667
668            let slot = header.find_slot_header_by_name(&map_detail.slot_name).ok_or_else(|| {
669                ConversionError::message(format!(
670                    "partial storage map references unknown slot {}",
671                    map_detail.slot_name
672                ))
673            })?;
674            if slot.slot_type() != StorageSlotType::Map {
675                return Err(ConversionError::message(format!(
676                    "partial storage map references non-map slot {}",
677                    map_detail.slot_name
678                )));
679            }
680            if partial_smt.root() != slot.value() {
681                return Err(ConversionError::message(format!(
682                    "partial storage map root for slot {} does not match storage header",
683                    map_detail.slot_name
684                )));
685            }
686        }
687
688        Ok(Self::Verified { header, map_details })
689    }
690}
691
692impl From<AccountStorageDetails> for proto::miden::node::v1::AccountStorageDetails {
693    fn from(value: AccountStorageDetails) -> Self {
694        let AccountStorageDetails { header, map_details } = value;
695
696        Self {
697            header: Some(header.into()),
698            map_details: map_details.into_iter().map(Into::into).collect(),
699        }
700    }
701}
702
703// ACCOUNT PROOF RESPONSE
704//================================================================================================
705
706/// Represents the response to an account proof request.
707pub struct GetAccountResponse {
708    pub block_num: BlockNumber,
709    pub witness: AccountWitness,
710    pub details: Option<AccountDetails>,
711}
712
713impl Verify for proto::miden::node::v1::DecodedGetAccountResponse {
714    type Verified = GetAccountResponse;
715    type Error = ConversionError;
716
717    /// Check that supplied details match the account witness. The caller must authenticate the
718    /// witness root and block against trusted chain state.
719    fn verify(self) -> Result<Self::Verified, Self::Error> {
720        let Self { block_num, witness, details } = self;
721
722        let block_num = block_num.verify().context("block_num")?;
723
724        let witness = witness.verify().context("witness")?;
725
726        let details = details.verify()?;
727
728        if let Some(details) = &details {
729            if details.account_header.id() != witness.id() {
730                return Err(ConversionError::message("account ID does not match witness")
731                    .context("details.header.account_id"));
732            }
733            if details.account_header.to_commitment() != witness.state_commitment() {
734                return Err(ConversionError::message("account commitment does not match witness")
735                    .context("details.header"));
736            }
737        }
738
739        Ok(GetAccountResponse { block_num, witness, details })
740    }
741}
742
743impl From<GetAccountResponse> for proto::miden::node::v1::GetAccountResponse {
744    fn from(value: GetAccountResponse) -> Self {
745        let GetAccountResponse { block_num, witness, details } = value;
746
747        Self {
748            witness: Some(witness.into()),
749            details: details.map(Into::into),
750            block_num: Some(block_num.into()),
751        }
752    }
753}
754
755// ACCOUNT DETAILS
756//================================================================================================
757
758/// Represents account details returned in response to an account proof request.
759pub struct AccountDetails {
760    pub account_header: AccountHeader,
761    pub account_code: Option<AccountCode>,
762    pub vault_details: AccountVaultDetails,
763    pub storage_details: AccountStorageDetails,
764}
765
766impl AccountDetails {
767    /// Creates account details where all storage map slots indicate limit exceeded.
768    pub fn with_storage_limits_exceeded(
769        account_header: AccountHeader,
770        account_code: Option<AccountCode>,
771        vault_details: AccountVaultDetails,
772        storage_header: AccountStorageHeader,
773        slot_names: impl IntoIterator<Item = StorageSlotName>,
774    ) -> Self {
775        Self {
776            account_header,
777            account_code,
778            vault_details,
779            storage_details: AccountStorageDetails::all_limits_exceeded(storage_header, slot_names),
780        }
781    }
782}
783
784impl Verify for proto::miden::node::v1::get_account_response::DecodedAccountDetails {
785    type Verified = AccountDetails;
786    type Error = ConversionError;
787
788    /// Verify storage and supplied code commitments against the account header. This does not
789    /// authenticate the header against trusted chain state.
790    fn verify(self) -> Result<Self::Verified, Self::Error> {
791        let Self {
792            header,
793            code,
794            vault_details,
795            storage_details,
796        } = self;
797
798        let account_header = header.verify().context("header")?;
799
800        let storage_details = storage_details.verify().context("storage_details")?;
801        if storage_details.header.to_commitment() != account_header.storage_commitment() {
802            return Err(ConversionError::message(
803                "storage commitment does not match account header",
804            )
805            .context("storage_details.header"));
806        }
807
808        let vault_details = vault_details.verify().context("vault_details")?;
809        let account_code = code.verify()?;
810        if let Some(code) = &account_code
811            && code.commitment() != account_header.code_commitment()
812        {
813            return Err(ConversionError::message("code commitment does not match account header")
814                .context("code"));
815        }
816
817        Ok(AccountDetails {
818            account_header,
819            account_code,
820            vault_details,
821            storage_details,
822        })
823    }
824}
825
826impl From<AccountDetails> for proto::miden::node::v1::get_account_response::AccountDetails {
827    fn from(value: AccountDetails) -> Self {
828        let AccountDetails {
829            account_header,
830            storage_details,
831            account_code,
832            vault_details,
833        } = value;
834
835        let header = Some(proto::account::AccountHeader::from(account_header));
836        let storage_details = Some(storage_details.into());
837        let code = account_code.map(Into::into);
838        let vault_details = Some(vault_details.into());
839
840        Self {
841            header,
842            storage_details,
843            code,
844            vault_details,
845        }
846    }
847}