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 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}