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        AccountId,
234        AccountPatch,
235        AccountType,
236        AccountUpdateDetails,
237        AccountVaultPatch,
238        StorageMapKey,
239        StorageSlotName,
240    };
241    use crate::block::BlockNumber;
242    use crate::errors::BatchAccountUpdateError;
243    use crate::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
244    use crate::testing::add_component::AddComponent;
245    use crate::testing::dummy_execution_proof;
246    use crate::testing::noop_auth_component::NoopAuthComponent;
247    use crate::testing::storage::AccountStoragePatchBuilder;
248    use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate};
249    use crate::utils::serde::Serializable;
250    use crate::{ACCOUNT_UPDATE_MAX_SIZE, Felt, Word};
251
252    fn map_update_patch(
253        account_id: AccountId,
254        key_range: Range<u32>,
255        final_nonce: u32,
256    ) -> AccountPatch {
257        let entries =
258            key_range.map(|key| (StorageMapKey::from_index(key), Word::from([key + 1, 1, 2, 3])));
259        let storage = AccountStoragePatchBuilder::new()
260            .update_map(StorageSlotName::mock(4), entries)
261            .build();
262
263        AccountPatch::new(
264            account_id,
265            storage,
266            AccountVaultPatch::default(),
267            None,
268            Some(Felt::from(final_nonce)),
269        )
270        .unwrap()
271    }
272
273    fn proven_transaction(
274        account_id: AccountId,
275        initial_state_commitment: Word,
276        final_state_commitment: Word,
277        patch: AccountPatch,
278    ) -> ProvenTransaction {
279        let patch_commitment = patch.to_commitment();
280        let update = TxAccountUpdate::new(
281            account_id,
282            initial_state_commitment,
283            final_state_commitment,
284            patch_commitment,
285            AccountUpdateDetails::Public(patch),
286        )
287        .unwrap();
288
289        ProvenTransaction::new(
290            update,
291            Vec::<InputNoteCommitment>::new(),
292            Vec::<OutputNote>::new(),
293            BlockNumber::from(1),
294            Word::empty(),
295            BlockNumber::from(2),
296            dummy_execution_proof(),
297        )
298        .unwrap()
299    }
300
301    #[test]
302    fn merge_rejects_aggregate_update_exceeding_size_limit_atomically() {
303        let account_id =
304            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
305        let initial_state_commitment = Word::from([1_u32, 2, 3, 4]);
306        let intermediate_state_commitment = Word::from([5_u32, 6, 7, 8]);
307        let final_state_commitment = Word::from([9_u32, 10, 11, 12]);
308        let total_entries_to_exceed_limit =
309            ACCOUNT_UPDATE_MAX_SIZE as usize / (StorageMapKey::SERIALIZED_SIZE * 2);
310        let entries_per_tx = total_entries_to_exceed_limit / 2;
311        let second_range_start = u32::try_from(entries_per_tx).unwrap();
312        let second_range_end = u32::try_from(total_entries_to_exceed_limit).unwrap();
313
314        let first_tx = proven_transaction(
315            account_id,
316            initial_state_commitment,
317            intermediate_state_commitment,
318            map_update_patch(account_id, 0..second_range_start, 2),
319        );
320        let second_tx = proven_transaction(
321            account_id,
322            intermediate_state_commitment,
323            final_state_commitment,
324            map_update_patch(account_id, second_range_start..second_range_end, 3),
325        );
326        let merged_details = first_tx
327            .account_update()
328            .details()
329            .clone()
330            .merge(second_tx.account_update().details().clone())
331            .unwrap();
332        let expected_update_size = merged_details.get_size_hint();
333        assert!(expected_update_size > ACCOUNT_UPDATE_MAX_SIZE as usize);
334        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
335        let original_update = update.clone();
336
337        let error = update.merge_proven_tx(&second_tx).unwrap_err();
338
339        assert_matches!(
340            error,
341            BatchAccountUpdateError::AccountUpdateSizeLimitExceeded {
342                account_id: actual_account_id,
343                update_size,
344            } if actual_account_id == account_id && update_size == expected_update_size
345        );
346        assert_eq!(update, original_update);
347    }
348
349    #[test]
350    fn merge_rejects_full_state_commitment_mismatch_atomically() {
351        let account = Account::builder([9; 32])
352            .account_type(AccountType::Public)
353            .with_component(NoopAuthComponent)
354            .with_component(AddComponent)
355            .build_existing()
356            .unwrap();
357        let account_commitment = account.to_commitment();
358        let wrong_final_state_commitment = Word::from([9_u32, 10, 11, 12]);
359        assert_ne!(wrong_final_state_commitment, account_commitment);
360        let first_tx = proven_transaction(
361            account.id(),
362            Word::empty(),
363            account_commitment,
364            AccountPatch::try_from(account.clone()).unwrap(),
365        );
366        let second_tx = proven_transaction(
367            account.id(),
368            account_commitment,
369            wrong_final_state_commitment,
370            AccountPatch::empty(account.id()),
371        );
372        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
373        let original_update = update.clone();
374
375        let error = update.merge_proven_tx(&second_tx).unwrap_err();
376
377        assert_matches!(
378            error,
379            BatchAccountUpdateError::AccountFinalCommitmentMismatch {
380                final_state_commitment,
381                account_commitment: actual_account_commitment,
382            } if final_state_commitment == wrong_final_state_commitment
383                && actual_account_commitment == account_commitment
384        );
385        assert_eq!(update, original_update);
386    }
387
388    #[test]
389    fn merge_accepts_valid_aggregate_update() {
390        let account_id =
391            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
392        let initial_state_commitment = Word::from([1_u32, 2, 3, 4]);
393        let intermediate_state_commitment = Word::from([5_u32, 6, 7, 8]);
394        let final_state_commitment = Word::from([9_u32, 10, 11, 12]);
395        let first_tx = proven_transaction(
396            account_id,
397            initial_state_commitment,
398            intermediate_state_commitment,
399            map_update_patch(account_id, 0..1, 2),
400        );
401        let second_tx = proven_transaction(
402            account_id,
403            intermediate_state_commitment,
404            final_state_commitment,
405            map_update_patch(account_id, 1..2, 3),
406        );
407        let mut update = BatchAccountUpdate::from_transaction(&first_tx);
408
409        update.merge_proven_tx(&second_tx).unwrap();
410
411        assert_eq!(update.initial_state_commitment(), initial_state_commitment);
412        assert_eq!(update.final_state_commitment(), final_state_commitment);
413        update.validate().unwrap();
414    }
415}