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#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct BatchAccountUpdate {
22 account_id: AccountId,
24
25 initial_state_commitment: Word,
29
30 final_state_commitment: Word,
32
33 details: AccountUpdateDetails,
37}
38
39impl BatchAccountUpdate {
40 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 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 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 #[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 pub fn account_id(&self) -> AccountId {
113 self.account_id
114 }
115
116 pub fn initial_state_commitment(&self) -> Word {
120 self.initial_state_commitment
121 }
122
123 pub fn final_state_commitment(&self) -> Word {
125 self.final_state_commitment
126 }
127
128 pub fn details(&self) -> &AccountUpdateDetails {
132 &self.details
133 }
134
135 pub fn is_private(&self) -> bool {
137 self.details.is_private()
138 }
139
140 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 pub fn into_update(self) -> AccountUpdateDetails {
193 self.details
194 }
195}
196
197impl 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#[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 #[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}