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