miden_client/rpc/domain/
account.rs

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