Skip to main content

miden_protocol/batch/
account_update.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3
4use crate::Word;
5use crate::account::{AccountId, AccountUpdateDetails, validate_new_public_account};
6use crate::errors::BatchAccountUpdateError;
7use crate::transaction::ProvenTransaction;
8use crate::utils::serde::{
9    ByteReader,
10    ByteWriter,
11    Deserializable,
12    DeserializationError,
13    Serializable,
14};
15
16// BATCH ACCOUNT UPDATE
17// ================================================================================================
18
19/// Represents the changes made to an account resulting from executing a batch of transactions.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct BatchAccountUpdate {
22    /// ID of the updated account.
23    account_id: AccountId,
24
25    /// Commitment to the state of the account before this update is applied.
26    ///
27    /// Equal to `Word::empty()` for new accounts.
28    initial_state_commitment: Word,
29
30    /// Commitment to the state of the account after this update is applied.
31    final_state_commitment: Word,
32
33    /// A set of changes which can be applied to the previous account state (i.e. `initial_state`)
34    /// to get the new account state. For private accounts, this is set to
35    /// [`AccountUpdateDetails::Private`].
36    details: AccountUpdateDetails,
37}
38
39impl BatchAccountUpdate {
40    // CONSTRUCTORS
41    // --------------------------------------------------------------------------------------------
42
43    /// Creates a [`BatchAccountUpdate`] by cloning the update and other details from the provided
44    /// [`ProvenTransaction`].
45    pub fn from_transaction(transaction: &ProvenTransaction) -> Self {
46        Self {
47            account_id: transaction.account_id(),
48            initial_state_commitment: transaction.account_update().initial_state_commitment(),
49            final_state_commitment: transaction.account_update().final_state_commitment(),
50            details: transaction.account_update().details().clone(),
51        }
52    }
53
54    /// Creates a validated [`BatchAccountUpdate`] from the provided parts.
55    ///
56    /// This enforces the same public/private account-detail invariants as transaction account
57    /// updates. For a new public account, the patch must contain the complete account state and
58    /// reconstruct to `final_state_commitment`.
59    pub fn new(
60        account_id: AccountId,
61        initial_state_commitment: Word,
62        final_state_commitment: Word,
63        details: AccountUpdateDetails,
64    ) -> Result<Self, BatchAccountUpdateError> {
65        let update = Self {
66            account_id,
67            initial_state_commitment,
68            final_state_commitment,
69            details,
70        };
71
72        update.validate()?;
73
74        Ok(update)
75    }
76
77    /// Validates this account update's size and account-detail invariants.
78    pub(crate) fn validate(&self) -> Result<(), BatchAccountUpdateError> {
79        self.details.validate_size(self.account_id)?;
80
81        let Some(patch) = self.details.validate_for_account(self.account_id)? else {
82            return Ok(());
83        };
84
85        if self.initial_state_commitment.is_empty() {
86            validate_new_public_account(patch, self.final_state_commitment)?;
87        }
88
89        Ok(())
90    }
91
92    /// Creates a [`BatchAccountUpdate`] from the provided parts without checking any consistency.
93    #[cfg(any(feature = "testing", test))]
94    pub fn new_unchecked(
95        account_id: AccountId,
96        initial_state_commitment: Word,
97        final_state_commitment: Word,
98        details: AccountUpdateDetails,
99    ) -> Self {
100        Self {
101            account_id,
102            initial_state_commitment,
103            final_state_commitment,
104            details,
105        }
106    }
107
108    // PUBLIC ACCESSORS
109    // --------------------------------------------------------------------------------------------
110
111    /// Returns the ID of the updated account.
112    pub fn account_id(&self) -> AccountId {
113        self.account_id
114    }
115
116    /// Returns a commitment to the state of the account before this update is applied.
117    ///
118    /// This is equal to [`Word::empty()`] for new accounts.
119    pub fn initial_state_commitment(&self) -> Word {
120        self.initial_state_commitment
121    }
122
123    /// Returns a commitment to the state of the account after this update is applied.
124    pub fn final_state_commitment(&self) -> Word {
125        self.final_state_commitment
126    }
127
128    /// Returns the contained [`AccountUpdateDetails`].
129    ///
130    /// This update can be used to build the new account state from the previous account state.
131    pub fn details(&self) -> &AccountUpdateDetails {
132        &self.details
133    }
134
135    /// Returns `true` if the account update details are for a private account.
136    pub fn is_private(&self) -> bool {
137        self.details.is_private()
138    }
139
140    // MUTATORS
141    // --------------------------------------------------------------------------------------------
142
143    /// Merges the transaction's update into this account update.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if:
148    /// - The account ID of the merging transaction does not match the account ID of the existing
149    ///   update.
150    /// - The merging transaction's initial state commitment does not match the final state
151    ///   commitment of the current update.
152    /// - The underlying [`AccountUpdateDetails::merge`] fails.
153    /// - The merged account update fails the validation performed by [`Self::new`], including the
154    ///   account update size limit and new-public-account commitment checks.
155    pub fn merge_proven_tx(
156        &mut self,
157        tx: &ProvenTransaction,
158    ) -> Result<(), BatchAccountUpdateError> {
159        if self.account_id != tx.account_id() {
160            return Err(BatchAccountUpdateError::AccountUpdateIdMismatch {
161                transaction: tx.id(),
162                expected_account_id: self.account_id,
163                actual_account_id: tx.account_id(),
164            });
165        }
166
167        if self.final_state_commitment != tx.account_update().initial_state_commitment() {
168            return Err(BatchAccountUpdateError::AccountUpdateInitialStateMismatch(tx.id()));
169        }
170
171        let details = self.details.clone().merge(tx.account_update().details().clone()).map_err(
172            |source_err| {
173                BatchAccountUpdateError::TransactionUpdateMergeError(tx.id(), Box::new(source_err))
174            },
175        )?;
176        let merged_update = Self::new(
177            self.account_id,
178            self.initial_state_commitment,
179            tx.account_update().final_state_commitment(),
180            details,
181        )?;
182
183        *self = merged_update;
184
185        Ok(())
186    }
187
188    // CONVERSIONS
189    // --------------------------------------------------------------------------------------------
190
191    /// Consumes the update and returns the underlying [`AccountUpdateDetails`].
192    pub fn into_update(self) -> AccountUpdateDetails {
193        self.details
194    }
195}
196
197// SERIALIZATION
198// ================================================================================================
199
200impl Serializable for BatchAccountUpdate {
201    fn write_into<W: ByteWriter>(&self, target: &mut W) {
202        self.account_id.write_into(target);
203        self.initial_state_commitment.write_into(target);
204        self.final_state_commitment.write_into(target);
205        self.details.write_into(target);
206    }
207}
208
209impl Deserializable for BatchAccountUpdate {
210    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
211        let account_id = AccountId::read_from(source)?;
212        let initial_state_commitment = Word::read_from(source)?;
213        let final_state_commitment = Word::read_from(source)?;
214        let details = AccountUpdateDetails::read_from(source)?;
215        Self::new(account_id, initial_state_commitment, final_state_commitment, details)
216            .map_err(|error| DeserializationError::InvalidValue(error.to_string()))
217    }
218}
219
220// TESTS
221// ================================================================================================
222
223#[cfg(test)]
224mod tests {
225    use alloc::vec::Vec;
226    use core::ops::Range;
227
228    use assert_matches::assert_matches;
229
230    use super::BatchAccountUpdate;
231    use crate::account::{
232        Account,
233        AccountCode,
234        AccountCodePatch,
235        AccountId,
236        AccountPatch,
237        AccountStoragePatch,
238        AccountType,
239        AccountUpdateDetails,
240        AccountVaultPatch,
241        StorageMapKey,
242        StorageSlotName,
243    };
244    use crate::block::BlockNumber;
245    use crate::errors::BatchAccountUpdateError;
246    use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
247    use crate::testing::add_component::AddComponent;
248    use crate::testing::dummy_execution_proof;
249    use crate::testing::noop_auth_component::NoopAuthComponent;
250    use crate::testing::storage::AccountStoragePatchBuilder;
251    use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate};
252    use crate::utils::serde::Serializable;
253    use crate::{ACCOUNT_UPDATE_MAX_SIZE, Felt, Word};
254
255    fn map_update_patch(
256        account_id: AccountId,
257        key_range: Range<u32>,
258        final_nonce: u32,
259    ) -> AccountPatch {
260        let entries =
261            key_range.map(|key| (StorageMapKey::from_index(key), Word::from([key + 1, 1, 2, 3])));
262        let storage = AccountStoragePatchBuilder::new()
263            .update_map(StorageSlotName::mock(4), entries)
264            .build();
265
266        AccountPatch::new(
267            account_id,
268            storage,
269            AccountVaultPatch::default(),
270            AccountCodePatch::default(),
271            Some(Felt::from(final_nonce)),
272        )
273        .unwrap()
274    }
275
276    fn proven_transaction(
277        account_id: AccountId,
278        initial_state_commitment: Word,
279        final_state_commitment: Word,
280        patch: AccountPatch,
281    ) -> ProvenTransaction {
282        let patch_commitment = patch.to_commitment();
283        let update = TxAccountUpdate::new(
284            account_id,
285            initial_state_commitment,
286            final_state_commitment,
287            patch_commitment,
288            AccountUpdateDetails::Public(patch),
289        )
290        .unwrap();
291
292        ProvenTransaction::new(
293            update,
294            Vec::<InputNoteCommitment>::new(),
295            Vec::<OutputNote>::new(),
296            BlockNumber::from(1),
297            Word::empty(),
298            BlockNumber::from(2),
299            dummy_execution_proof(),
300        )
301        .unwrap()
302    }
303
304    #[test]
305    fn merge_rejects_aggregate_update_exceeding_size_limit_atomically() {
306        let account_id =
307            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
308        let initial_state_commitment = Word::from([1_u32, 2, 3, 4]);
309        let intermediate_state_commitment = Word::from([5_u32, 6, 7, 8]);
310        let final_state_commitment = Word::from([9_u32, 10, 11, 12]);
311        let total_entries_to_exceed_limit =
312            ACCOUNT_UPDATE_MAX_SIZE as usize / (StorageMapKey::SERIALIZED_SIZE * 2);
313        let entries_per_tx = total_entries_to_exceed_limit / 2;
314        let second_range_start = u32::try_from(entries_per_tx).unwrap();
315        let second_range_end = u32::try_from(total_entries_to_exceed_limit).unwrap();
316
317        let first_tx = proven_transaction(
318            account_id,
319            initial_state_commitment,
320            intermediate_state_commitment,
321            map_update_patch(account_id, 0..second_range_start, 2),
322        );
323        let second_tx = proven_transaction(
324            account_id,
325            intermediate_state_commitment,
326            final_state_commitment,
327            map_update_patch(account_id, second_range_start..second_range_end, 3),
328        );
329        let merged_details = first_tx
330            .account_update()
331            .details()
332            .clone()
333            .merge(second_tx.account_update().details().clone())
334            .unwrap();
335        let expected_update_size = merged_details.get_size_hint();
336        assert!(expected_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize);
337        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
338        let original_update = update.clone();
339
340        let error = update.merge_proven_tx(&second_tx).unwrap_err();
341
342        assert_matches!(
343            error,
344            BatchAccountUpdateError::AccountUpdateSizeLimitExceeded {
345                account_id: actual_account_id,
346                update_size,
347            } if actual_account_id == account_id && update_size == expected_update_size
348        );
349        assert_eq!(update, original_update);
350    }
351
352    #[test]
353    fn merge_rejects_creation_commitment_mismatch_atomically() {
354        let account = Account::builder([9; 32])
355            .account_type(AccountType::Public)
356            .with_component(NoopAuthComponent)
357            .with_component(AddComponent)
358            .build_existing()
359            .unwrap();
360        let account_commitment = account.to_commitment();
361        let wrong_final_state_commitment = Word::from([9_u32, 10, 11, 12]);
362        assert_ne!(wrong_final_state_commitment, account_commitment);
363        let first_tx = proven_transaction(
364            account.id(),
365            Word::empty(),
366            account_commitment,
367            AccountPatch::try_from(account.clone()).unwrap(),
368        );
369        let second_tx = proven_transaction(
370            account.id(),
371            account_commitment,
372            wrong_final_state_commitment,
373            AccountPatch::empty(account.id()),
374        );
375        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
376        let original_update = update.clone();
377
378        let error = update.merge_proven_tx(&second_tx).unwrap_err();
379
380        assert_matches!(
381            error,
382            BatchAccountUpdateError::AccountFinalCommitmentMismatch {
383                final_state_commitment,
384                account_commitment: actual_account_commitment,
385            } if final_state_commitment == wrong_final_state_commitment
386                && actual_account_commitment == account_commitment
387        );
388        assert_eq!(update, original_update);
389    }
390
391    /// A creation patch merged with a later code upgrade creates the account with the upgraded
392    /// code.
393    #[test]
394    fn merge_creation_with_code_upgrade() -> anyhow::Result<()> {
395        let account = Account::builder([9; 32])
396            .account_type(AccountType::Public)
397            .with_component(NoopAuthComponent)
398            .with_component(AddComponent)
399            .build_existing()?;
400        let upgraded_code = AccountCode::mock();
401        assert_ne!(account.code(), &upgraded_code);
402
403        let upgrade_patch = AccountPatch::new(
404            account.id(),
405            AccountStoragePatch::new(),
406            AccountVaultPatch::default(),
407            AccountCodePatch::new(Some(upgraded_code.clone())),
408            Some(account.nonce() + Felt::ONE),
409        )?;
410        let mut upgraded_account = account.clone();
411        upgraded_account.apply_patch(&upgrade_patch)?;
412
413        let creation_tx = proven_transaction(
414            account.id(),
415            Word::empty(),
416            account.to_commitment(),
417            AccountPatch::try_from(account.clone())?,
418        );
419        let upgrade_tx = proven_transaction(
420            account.id(),
421            account.to_commitment(),
422            upgraded_account.to_commitment(),
423            upgrade_patch,
424        );
425        let mut update = BatchAccountUpdate::from_transaction(&creation_tx);
426
427        update.merge_proven_tx(&upgrade_tx)?;
428
429        assert_eq!(update.details().unwrap_public().code().as_code(), Some(&upgraded_code));
430        update.validate()?;
431
432        Ok(())
433    }
434
435    #[test]
436    fn merge_accepts_valid_aggregate_update() {
437        let account_id =
438            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
439        let initial_state_commitment = Word::from([1_u32, 2, 3, 4]);
440        let intermediate_state_commitment = Word::from([5_u32, 6, 7, 8]);
441        let final_state_commitment = Word::from([9_u32, 10, 11, 12]);
442        let first_tx = proven_transaction(
443            account_id,
444            initial_state_commitment,
445            intermediate_state_commitment,
446            map_update_patch(account_id, 0..1, 2),
447        );
448        let second_tx = proven_transaction(
449            account_id,
450            intermediate_state_commitment,
451            final_state_commitment,
452            map_update_patch(account_id, 1..2, 3),
453        );
454        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
455
456        update.merge_proven_tx(&second_tx).unwrap();
457
458        assert_eq!(update.initial_state_commitment(), initial_state_commitment);
459        assert_eq!(update.final_state_commitment(), final_state_commitment);
460        update.validate().unwrap();
461    }
462}