miden_client/rpc/domain/
account.rs

1use alloc::{collections::BTreeMap, string::String, vec::Vec};
2use core::fmt::{self, Debug, Display, Formatter};
3
4use miden_objects::{
5    Digest,
6    account::{Account, AccountCode, AccountHeader, AccountId, AccountStorageHeader},
7    block::{AccountWitness, BlockNumber},
8    crypto::merkle::{MerklePath, SmtProof},
9};
10use miden_tx::utils::{Deserializable, Serializable, ToHex};
11use thiserror::Error;
12
13use crate::rpc::{
14    RpcError,
15    errors::RpcConversionError,
16    generated::{
17        account::{AccountHeader as ProtoAccountHeader, AccountId as ProtoAccountId},
18        requests::get_account_proofs_request,
19        responses::{
20            AccountStateHeader as ProtoAccountStateHeader, AccountWitness as ProtoAccountWitness,
21        },
22    },
23};
24
25// FETCHED ACCOUNT
26// ================================================================================================
27
28/// Describes the possible responses from the `GetAccountDetails` endpoint for an account.
29pub enum FetchedAccount {
30    /// Private accounts are stored off-chain. Only a commitment to the state of the account is
31    /// shared with the network. The full account state is to be tracked locally.
32    Private(AccountId, AccountUpdateSummary),
33    /// Public accounts are recorded on-chain. As such, its state is shared with the network and
34    /// can always be retrieved through the appropriate RPC method.
35    Public(Account, AccountUpdateSummary),
36}
37
38impl FetchedAccount {
39    /// Returns the account ID.
40    pub fn account_id(&self) -> AccountId {
41        match self {
42            Self::Private(account_id, _) => *account_id,
43            Self::Public(account, _) => account.id(),
44        }
45    }
46
47    // Returns the account update summary commitment
48    pub fn commitment(&self) -> Digest {
49        match self {
50            Self::Private(_, summary) | Self::Public(_, summary) => summary.commitment,
51        }
52    }
53
54    // Returns the associated account if the account is public, otherwise none
55    pub fn account(&self) -> Option<&Account> {
56        match self {
57            Self::Private(..) => None,
58            Self::Public(account, _) => Some(account),
59        }
60    }
61}
62
63impl From<FetchedAccount> for Option<Account> {
64    fn from(acc: FetchedAccount) -> Self {
65        match acc {
66            FetchedAccount::Private(..) => None,
67            FetchedAccount::Public(account, _) => Some(account),
68        }
69    }
70}
71
72// ACCOUNT UPDATE SUMMARY
73// ================================================================================================
74
75/// Contains public updated information about the account requested.
76pub struct AccountUpdateSummary {
77    /// Commitment of the account, that represents a commitment to its updated state.
78    pub commitment: Digest,
79    /// Block number of last account update.
80    pub last_block_num: u32,
81}
82
83impl AccountUpdateSummary {
84    /// Creates a new [`AccountUpdateSummary`].
85    pub fn new(commitment: Digest, last_block_num: u32) -> Self {
86        Self { commitment, last_block_num }
87    }
88}
89
90// ACCOUNT ID
91// ================================================================================================
92
93impl Display for ProtoAccountId {
94    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
95        f.write_fmt(format_args!("0x{}", self.id.to_hex()))
96    }
97}
98
99impl Debug for ProtoAccountId {
100    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
101        Display::fmt(self, f)
102    }
103}
104
105// INTO PROTO ACCOUNT ID
106// ================================================================================================
107
108impl From<AccountId> for ProtoAccountId {
109    fn from(account_id: AccountId) -> Self {
110        Self { id: account_id.to_bytes() }
111    }
112}
113
114// FROM PROTO ACCOUNT ID
115// ================================================================================================
116
117impl TryFrom<ProtoAccountId> for AccountId {
118    type Error = RpcConversionError;
119
120    fn try_from(account_id: ProtoAccountId) -> Result<Self, Self::Error> {
121        AccountId::read_from_bytes(&account_id.id).map_err(|_| RpcConversionError::NotAValidFelt)
122    }
123}
124
125// ACCOUNT HEADER
126// ================================================================================================
127
128impl ProtoAccountHeader {
129    #[cfg(any(feature = "tonic", feature = "web-tonic"))]
130    pub fn into_domain(self, account_id: AccountId) -> Result<AccountHeader, crate::rpc::RpcError> {
131        use alloc::string::String;
132
133        use miden_objects::Felt;
134
135        use crate::rpc::RpcError;
136
137        let ProtoAccountHeader {
138            nonce,
139            vault_root,
140            storage_commitment,
141            code_commitment,
142        } = self;
143        let vault_root = vault_root
144            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountHeader.VaultRoot")))?
145            .try_into()?;
146        let storage_commitment = storage_commitment
147            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountHeader.StorageCommitment")))?
148            .try_into()?;
149        let code_commitment = code_commitment
150            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountHeader.CodeCommitment")))?
151            .try_into()?;
152
153        Ok(AccountHeader::new(
154            account_id,
155            Felt::new(nonce),
156            vault_root,
157            storage_commitment,
158            code_commitment,
159        ))
160    }
161}
162
163// FROM PROTO ACCOUNT HEADERS
164// ================================================================================================
165
166impl ProtoAccountStateHeader {
167    /// Converts the RPC response into `StateHeaders`.
168    ///
169    /// The RPC response may omit unchanged account codes. If so, this function uses
170    /// `known_account_codes` to fill in the missing code. If a required code cannot be found in
171    /// the response or `known_account_codes`, an error is returned.
172    ///
173    /// # Errors
174    /// - If account code is missing both on `self` and `known_account_codes`
175    /// - If data cannot be correctly deserialized
176    #[cfg(any(feature = "tonic", feature = "web-tonic"))]
177    pub fn into_domain(
178        self,
179        account_id: AccountId,
180        known_account_codes: &BTreeMap<Digest, AccountCode>,
181    ) -> Result<StateHeaders, crate::rpc::RpcError> {
182        use crate::rpc::{RpcError, generated::responses::StorageSlotMapProof};
183
184        let ProtoAccountStateHeader {
185            header,
186            storage_header,
187            account_code,
188            storage_maps,
189        } = self;
190        let account_header = header
191            .ok_or(RpcError::ExpectedDataMissing("Account.StateHeader".into()))?
192            .into_domain(account_id)?;
193
194        let storage_header = AccountStorageHeader::read_from_bytes(&storage_header)?;
195
196        // If an account code was received, it means the previously known account code is no longer
197        // valid. If it was not, it means we sent a code commitment that matched and so our code
198        // is still valid
199        let code = {
200            let received_code =
201                account_code.map(|c| AccountCode::read_from_bytes(&c)).transpose()?;
202            match received_code {
203                Some(code) => code,
204                None => known_account_codes
205                    .get(&account_header.code_commitment())
206                    .ok_or(RpcError::InvalidResponse(
207                        "Account code was not provided, but the response did not contain it either"
208                            .into(),
209                    ))?
210                    .clone(),
211            }
212        };
213
214        // Get map values into slot |-> (key, value, proof) mapping
215        let mut storage_slot_proofs: BTreeMap<u8, Vec<SmtProof>> = BTreeMap::new();
216        for StorageSlotMapProof { storage_slot, smt_proof } in storage_maps {
217            let proof = SmtProof::read_from_bytes(&smt_proof)?;
218            match storage_slot_proofs
219                .get_mut(&(u8::try_from(storage_slot).expect("there are no more than 256 slots")))
220            {
221                Some(list) => list.push(proof),
222                None => {
223                    _ = storage_slot_proofs.insert(
224                        u8::try_from(storage_slot).expect("only 256 storage slots"),
225                        vec![proof],
226                    );
227                },
228            }
229        }
230
231        Ok(StateHeaders {
232            account_header,
233            storage_header,
234            code,
235            storage_slots: storage_slot_proofs,
236        })
237    }
238}
239
240// ACCOUNT PROOF
241// ================================================================================================
242
243/// Contains a block number, and a list of account proofs at that block.
244pub type AccountProofs = (BlockNumber, Vec<AccountProof>);
245
246/// Account state headers.
247#[derive(Clone, Debug)]
248pub struct StateHeaders {
249    // TODO: should this be renamed? or storage_slots moved to AccountProof
250    pub account_header: AccountHeader,
251    pub storage_header: AccountStorageHeader,
252    pub code: AccountCode,
253    pub storage_slots: BTreeMap<StorageSlotIndex, Vec<SmtProof>>,
254}
255
256/// Represents a proof of existence of an account's state at a specific block number.
257#[derive(Clone, Debug)]
258pub struct AccountProof {
259    /// Account witness.
260    account_witness: AccountWitness,
261    /// State headers of public accounts.
262    state_headers: Option<StateHeaders>,
263}
264
265impl AccountProof {
266    /// Creates a new [`AccountProof`].
267    pub fn new(
268        account_witness: AccountWitness,
269        state_headers: Option<StateHeaders>,
270    ) -> Result<Self, AccountProofError> {
271        if let Some(StateHeaders {
272            account_header, storage_header: _, code, ..
273        }) = &state_headers
274        {
275            if account_header.commitment() != account_witness.state_commitment() {
276                return Err(AccountProofError::InconsistentAccountCommitment);
277            }
278            if account_header.id() != account_witness.id() {
279                return Err(AccountProofError::InconsistentAccountId);
280            }
281            if code.commitment() != account_header.code_commitment() {
282                return Err(AccountProofError::InconsistentCodeCommitment);
283            }
284        }
285
286        Ok(Self { account_witness, state_headers })
287    }
288
289    /// Returns the account ID related to the account proof.
290    pub fn account_id(&self) -> AccountId {
291        self.account_witness.id()
292    }
293
294    /// Returns the account header, if present.
295    pub fn account_header(&self) -> Option<&AccountHeader> {
296        self.state_headers.as_ref().map(|headers| &headers.account_header)
297    }
298
299    /// Returns the storage header, if present.
300    pub fn storage_header(&self) -> Option<&AccountStorageHeader> {
301        self.state_headers.as_ref().map(|headers| &headers.storage_header)
302    }
303
304    /// Returns the account code, if present.
305    pub fn account_code(&self) -> Option<&AccountCode> {
306        self.state_headers.as_ref().map(|headers| &headers.code)
307    }
308
309    /// Returns the code commitment, if account code is present in the state headers.
310    pub fn code_commitment(&self) -> Option<Digest> {
311        self.account_code().map(AccountCode::commitment)
312    }
313
314    /// Returns the current state commitment of the account.
315    pub fn account_commitment(&self) -> Digest {
316        self.account_witness.state_commitment()
317    }
318
319    pub fn account_witness(&self) -> &AccountWitness {
320        &self.account_witness
321    }
322
323    /// Returns the proof of the account's inclusion.
324    pub fn merkle_proof(&self) -> &MerklePath {
325        self.account_witness.path()
326    }
327
328    /// Deconstructs `AccountProof` into its individual parts.
329    pub fn into_parts(self) -> (AccountWitness, Option<StateHeaders>) {
330        (self.account_witness, self.state_headers)
331    }
332}
333
334// ACCOUNT WITNESS
335// ================================================================================================
336
337impl TryFrom<ProtoAccountWitness> for AccountWitness {
338    type Error = RpcError;
339
340    fn try_from(account_witness: ProtoAccountWitness) -> Result<Self, Self::Error> {
341        let state_commitment = account_witness
342            .commitment
343            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountWitness.StateCommitment")))?
344            .try_into()?;
345        let merkle_path = account_witness
346            .path
347            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountWitness.MerklePath")))?
348            .try_into()?;
349        let account_id = account_witness
350            .witness_id
351            .ok_or(RpcError::ExpectedDataMissing(String::from("AccountWitness.WitnessId")))?
352            .try_into()?;
353
354        let witness = AccountWitness::new(account_id, state_commitment, merkle_path).unwrap();
355        Ok(witness)
356    }
357}
358
359// ACCOUNT STORAGE REQUEST
360// ================================================================================================
361
362pub type StorageSlotIndex = u8;
363pub type StorageMapKey = Digest;
364
365/// Describes storage slots indices to be requested, as well as a list of keys for each of those
366/// slots.
367#[derive(Clone, Debug, Default, Eq, PartialEq)]
368pub struct AccountStorageRequirements(BTreeMap<StorageSlotIndex, Vec<StorageMapKey>>);
369
370impl AccountStorageRequirements {
371    pub fn new<'a>(
372        slots_and_keys: impl IntoIterator<
373            Item = (StorageSlotIndex, impl IntoIterator<Item = &'a StorageMapKey>),
374        >,
375    ) -> Self {
376        let map = slots_and_keys
377            .into_iter()
378            .map(|(slot_index, keys_iter)| {
379                let keys_vec: Vec<StorageMapKey> = keys_iter.into_iter().copied().collect();
380                (slot_index, keys_vec)
381            })
382            .collect();
383
384        AccountStorageRequirements(map)
385    }
386
387    pub fn inner(&self) -> &BTreeMap<StorageSlotIndex, Vec<StorageMapKey>> {
388        &self.0
389    }
390}
391
392impl From<AccountStorageRequirements> for Vec<get_account_proofs_request::StorageRequest> {
393    fn from(value: AccountStorageRequirements) -> Vec<get_account_proofs_request::StorageRequest> {
394        let mut requests = Vec::with_capacity(value.0.len());
395        for (slot_index, map_keys) in value.0 {
396            requests.push(get_account_proofs_request::StorageRequest {
397                storage_slot_index: u32::from(slot_index),
398                map_keys: map_keys
399                    .into_iter()
400                    .map(crate::rpc::generated::digest::Digest::from)
401                    .collect(),
402            });
403        }
404        requests
405    }
406}
407
408impl Serializable for AccountStorageRequirements {
409    fn write_into<W: miden_tx::utils::ByteWriter>(&self, target: &mut W) {
410        target.write(&self.0);
411    }
412}
413
414impl Deserializable for AccountStorageRequirements {
415    fn read_from<R: miden_tx::utils::ByteReader>(
416        source: &mut R,
417    ) -> Result<Self, miden_tx::utils::DeserializationError> {
418        Ok(AccountStorageRequirements(source.read()?))
419    }
420}
421
422// ERRORS
423// ================================================================================================
424
425#[derive(Debug, Error)]
426pub enum AccountProofError {
427    #[error(
428        "the received account commitment doesn't match the received account header's commitment"
429    )]
430    InconsistentAccountCommitment,
431    #[error("the received account id doesn't match the received account header's id")]
432    InconsistentAccountId,
433    #[error(
434        "the received code commitment doesn't match the received account header's code commitment"
435    )]
436    InconsistentCodeCommitment,
437}