Skip to main content

miden_protocol/account/
header.rs

1use alloc::vec::Vec;
2
3use super::{Account, AccountId, Felt, PartialAccount};
4use crate::Word;
5use crate::crypto::SequentialCommit;
6use crate::errors::AccountError;
7use crate::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14
15// ACCOUNT HEADER
16// ================================================================================================
17
18/// A header of an account which contains information that succinctly describes the state of the
19/// components of the account.
20///
21/// The [AccountHeader] is composed of:
22/// - id: the account ID ([`AccountId`]) of the account.
23/// - nonce: the nonce of the account.
24/// - vault_root: a commitment to the account's vault ([super::AssetVault]).
25/// - storage_commitment: a commitment to the account's storage ([super::AccountStorage]).
26/// - code_commitment: a commitment to the account's code ([super::AccountCode]).
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct AccountHeader {
29    id: AccountId,
30    nonce: Felt,
31    vault_root: Word,
32    storage_commitment: Word,
33    code_commitment: Word,
34}
35
36impl AccountHeader {
37    // CONSTANTS
38    // --------------------------------------------------------------------------------------------
39
40    /// Version 1 of the account header encoding.
41    ///
42    /// The version occupies the first element of the account metadata word, so a reader can get it
43    /// before it interprets the rest of the header. Version 0 is unused, which means an all-zero
44    /// word is never valid account metadata.
45    pub(crate) const VERSION_1: u8 = 1;
46
47    /// The number of elements in an account header.
48    pub(crate) const NUM_ELEMENTS: u8 = 16;
49
50    /// The index of the version in the account header elements.
51    pub(crate) const VERSION_IDX: usize = 0;
52
53    /// The index of the nonce in the account header elements.
54    pub(crate) const NONCE_IDX: usize = 1;
55
56    /// The index of the ID suffix in the account header elements.
57    pub(crate) const ID_SUFFIX_IDX: usize = 2;
58
59    /// The index of the ID prefix in the account header elements.
60    pub(crate) const ID_PREFIX_IDX: usize = 3;
61
62    /// The index at which the vault root word starts in the account header elements.
63    const VAULT_ROOT_IDX: usize = 4;
64
65    /// The index at which the storage commitment word starts in the account header elements.
66    const STORAGE_COMMITMENT_IDX: usize = 8;
67
68    /// The index at which the code commitment word starts in the account header elements.
69    const CODE_COMMITMENT_IDX: usize = 12;
70
71    // CONSTRUCTORS
72    // --------------------------------------------------------------------------------------------
73
74    /// Creates a new [`AccountHeader`].
75    pub fn new(
76        id: AccountId,
77        nonce: Felt,
78        vault_root: Word,
79        storage_commitment: Word,
80        code_commitment: Word,
81    ) -> Self {
82        Self {
83            id,
84            nonce,
85            vault_root,
86            storage_commitment,
87            code_commitment,
88        }
89    }
90
91    /// Parses the account header data returned by the VM into individual account component
92    /// commitments. Returns a tuple of account ID, vault root, storage commitment, code
93    /// commitment, and nonce.
94    pub(crate) fn try_from_elements(elements: &[Felt]) -> Result<AccountHeader, AccountError> {
95        if elements.len() != Self::NUM_ELEMENTS as usize {
96            return Err(AccountError::UnexpectedHeaderLength { actual: elements.len() });
97        }
98
99        let version = elements[Self::VERSION_IDX].as_canonical_u64();
100        if version != u64::from(Self::VERSION_1) {
101            return Err(AccountError::UnsupportedAccountVersion(version));
102        }
103        let nonce = elements[Self::NONCE_IDX];
104        let id = AccountId::try_from_elements(
105            elements[Self::ID_SUFFIX_IDX],
106            elements[Self::ID_PREFIX_IDX],
107        )
108        .map_err(AccountError::FinalAccountHeaderIdParsingFailed)?;
109
110        let vault_root = parse_word(elements, Self::VAULT_ROOT_IDX);
111        let storage_commitment = parse_word(elements, Self::STORAGE_COMMITMENT_IDX);
112        let code_commitment = parse_word(elements, Self::CODE_COMMITMENT_IDX);
113
114        Ok(AccountHeader::new(id, nonce, vault_root, storage_commitment, code_commitment))
115    }
116
117    // PUBLIC ACCESSORS
118    // --------------------------------------------------------------------------------------------
119
120    /// Returns the commitment of this account.
121    ///
122    /// The commitment of an account is computed as a hash over the account header elements returned
123    /// by [`Self::to_elements`]. Computing the account commitment requires 2 permutations of the
124    /// hash function.
125    pub fn to_commitment(&self) -> Word {
126        <Self as SequentialCommit>::to_commitment(self)
127    }
128
129    /// Returns the id of this account.
130    pub fn id(&self) -> AccountId {
131        self.id
132    }
133
134    /// Returns the nonce of this account.
135    pub fn nonce(&self) -> Felt {
136        self.nonce
137    }
138
139    /// Returns the vault root of this account.
140    pub fn vault_root(&self) -> Word {
141        self.vault_root
142    }
143
144    /// Returns the storage commitment of this account.
145    pub fn storage_commitment(&self) -> Word {
146        self.storage_commitment
147    }
148
149    /// Returns the code commitment of this account.
150    pub fn code_commitment(&self) -> Word {
151        self.code_commitment
152    }
153
154    /// Returns the account header encoded to a vector of field elements.
155    ///
156    /// This is a vector of the following field elements:
157    /// ```text
158    /// [
159    ///     [account_version, account_nonce, account_id_suffix, account_id_prefix],
160    ///     VAULT_ROOT,
161    ///     STORAGE_COMMITMENT,
162    ///     CODE_COMMITMENT,
163    /// ]
164    /// ```
165    ///
166    /// `account_version` is an 8-bit version of this encoding. Version 0 is unused.
167    pub fn to_elements(&self) -> Vec<Felt> {
168        <Self as SequentialCommit>::to_elements(self)
169    }
170}
171
172impl From<&PartialAccount> for AccountHeader {
173    fn from(account: &PartialAccount) -> Self {
174        Self {
175            id: account.id(),
176            nonce: account.nonce(),
177            vault_root: account.vault().root(),
178            storage_commitment: account.storage().commitment(),
179            code_commitment: account.code().commitment(),
180        }
181    }
182}
183
184impl From<&Account> for AccountHeader {
185    fn from(account: &Account) -> Self {
186        Self {
187            id: account.id(),
188            nonce: account.nonce(),
189            vault_root: account.vault().root(),
190            storage_commitment: account.storage().to_commitment(),
191            code_commitment: account.code().commitment(),
192        }
193    }
194}
195
196impl SequentialCommit for AccountHeader {
197    type Commitment = Word;
198
199    fn to_elements(&self) -> Vec<Felt> {
200        let mut metadata_word = Word::empty();
201        metadata_word[Self::VERSION_IDX] = Felt::from(Self::VERSION_1);
202        metadata_word[Self::NONCE_IDX] = self.nonce;
203        metadata_word[Self::ID_SUFFIX_IDX] = self.id.suffix();
204        metadata_word[Self::ID_PREFIX_IDX] = self.id.prefix().as_felt();
205
206        [
207            metadata_word.as_elements(),
208            self.vault_root.as_elements(),
209            self.storage_commitment.as_elements(),
210            self.code_commitment.as_elements(),
211        ]
212        .concat()
213    }
214}
215
216// SERIALIZATION
217// ================================================================================================
218
219impl Serializable for AccountHeader {
220    fn write_into<W: ByteWriter>(&self, target: &mut W) {
221        Self::VERSION_1.write_into(target);
222        self.id.write_into(target);
223        self.nonce.write_into(target);
224        self.vault_root.write_into(target);
225        self.storage_commitment.write_into(target);
226        self.code_commitment.write_into(target);
227    }
228}
229
230impl Deserializable for AccountHeader {
231    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
232        let version = u8::read_from(source)?;
233
234        if version != Self::VERSION_1 {
235            return Err(DeserializationError::InvalidValue(format!(
236                "account version is {} but only version {} is supported",
237                version,
238                Self::VERSION_1,
239            )));
240        }
241
242        let id = AccountId::read_from(source)?;
243        let nonce = Felt::read_from(source)?;
244        let vault_root = Word::read_from(source)?;
245        let storage_commitment = Word::read_from(source)?;
246        let code_commitment = Word::read_from(source)?;
247
248        Ok(AccountHeader {
249            id,
250            nonce,
251            vault_root,
252            storage_commitment,
253            code_commitment,
254        })
255    }
256}
257
258// HELPER FUNCTIONS
259// ================================================================================================
260
261/// Creates a new `Word` instance from the slice of `Felt`s using provided offset.
262fn parse_word(data: &[Felt], offset: usize) -> Word {
263    Word::try_from(&data[offset..offset + Word::NUM_ELEMENTS])
264        .expect("we should have sliced off exactly 4 bytes")
265}
266
267// TESTS
268// ================================================================================================
269
270#[cfg(test)]
271mod tests {
272    use anyhow::Context;
273    use assert_matches::assert_matches;
274    use miden_core::Felt;
275
276    use super::AccountHeader;
277    use crate::Word;
278    use crate::account::tests::build_account;
279    use crate::account::{AccountId, StorageSlotContent};
280    use crate::asset::FungibleAsset;
281    use crate::errors::AccountError;
282    use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
283    use crate::utils::serde::{Deserializable, DeserializationError, Serializable};
284
285    /// Builds an account header whose fields are all distinguishable from one another so that a
286    /// swapped element in the encoding is visible.
287    fn mock_header() -> anyhow::Result<AccountHeader> {
288        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
289            .context("failed to build account ID")?;
290
291        Ok(AccountHeader::new(
292            id,
293            Felt::from(42u32),
294            Word::from([1, 2, 3, 4u32]),
295            Word::from([5, 6, 7, 8u32]),
296            Word::from([9, 10, 11, 12u32]),
297        ))
298    }
299
300    #[rstest::rstest]
301    #[case::version_zero(0)]
302    #[case::version_two(2)]
303    // The lower 8 bits encode the supported version, so this guards against the upper bits being
304    // truncated instead of rejected.
305    #[case::version_exceeding_u8((1 << 8) | u32::from(AccountHeader::VERSION_1))]
306    fn account_header_rejects_unsupported_version(#[case] version: u32) -> anyhow::Result<()> {
307        let mut elements = mock_header()?.to_elements();
308        elements[AccountHeader::VERSION_IDX] = Felt::from(version);
309
310        let error = AccountHeader::try_from_elements(&elements)
311            .expect_err("header with an unsupported version should not parse");
312
313        assert_matches!(error, AccountError::UnsupportedAccountVersion(actual) => {
314            assert_eq!(actual, u64::from(version));
315        });
316
317        Ok(())
318    }
319
320    #[test]
321    fn test_serde_account_storage() {
322        let init_nonce = Felt::from(1_u32);
323        let asset_0 = FungibleAsset::mock(99);
324        let word = Word::from([1, 2, 3, 4u32]);
325        let storage_slot = StorageSlotContent::Value(word);
326        let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]);
327
328        let account_header = account.to_header();
329
330        let header_bytes = account_header.to_bytes();
331        let deserialized_header = AccountHeader::read_from_bytes(&header_bytes).unwrap();
332        assert_eq!(deserialized_header, account_header);
333    }
334
335    #[test]
336    fn account_header_deserialization_rejects_unsupported_version() {
337        let error = AccountHeader::read_from_bytes(&[0]).unwrap_err();
338
339        assert_matches!(error, DeserializationError::InvalidValue(message) => {
340            assert!(message.contains("account version is 0"));
341        });
342    }
343}