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