miden_client/rpc/domain/
account.rs

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