1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5use core::cmp::Ordering;
6
7use async_trait::async_trait;
8use miden_protocol::Word;
9use miden_protocol::account::{Account, AccountHeader, AccountId, StorageSlotType};
10use miden_protocol::block::account_tree::AccountIdKey;
11use miden_protocol::block::{BlockHeader, BlockNumber};
12use miden_protocol::crypto::merkle::mmr::{MmrDelta, PartialMmr};
13use miden_protocol::note::{NoteAttachments, NoteId, NoteTag, NoteType, Nullifier};
14use tracing::info;
15
16use super::state_sync_update::{TransactionUpdateTracker, build_account_patch};
17use super::{
18 AccountUpdates,
19 NoteObserver,
20 PartialBlockchainUpdates,
21 PublicAccountUpdate,
22 StateSyncUpdate,
23};
24use crate::ClientError;
25use crate::note::{NoteConsumption, NoteUpdateTracker};
26use crate::rpc::domain::account::{
27 AccountDetails,
28 AccountProof,
29 GetAccountRequest,
30 StorageMapFetch,
31 VaultFetch,
32};
33use crate::rpc::domain::note::{CommittedNote, NoteSyncBlock, SyncedNoteDetails};
34use crate::rpc::domain::sync::{ChainMmrInfo, SyncTarget};
35use crate::rpc::domain::transaction::TransactionRecord as RpcTransactionRecord;
36use crate::rpc::{AccountStateAt, NodeRpcClient};
37use crate::store::{InputNoteRecord, OutputNoteRecord, StoreError};
38use crate::transaction::TransactionRecord;
39
40enum PublicAccountSync {
45 Apply(Box<PublicAccountUpdate>),
47 Superseded,
49 Ignore,
51}
52
53struct FetchedSyncData {
59 mmr_delta: MmrDelta,
61 chain_tip_header: BlockHeader,
63 note_blocks: Vec<NoteSyncBlock>,
65 synced_notes: BTreeMap<NoteId, SyncedNoteDetails>,
68 transactions: Vec<RpcTransactionRecord>,
70}
71
72pub struct StateSyncInput {
88 pub accounts: Vec<AccountHeader>,
90 pub note_tags: BTreeSet<NoteTag>,
92 pub input_notes: Vec<InputNoteRecord>,
94 pub output_notes: Vec<OutputNoteRecord>,
96 pub uncommitted_transactions: Vec<TransactionRecord>,
98}
99
100#[allow(clippy::large_enum_variant)]
105pub enum NoteUpdateAction {
106 Commit(CommittedNote),
109 Insert(InputNoteRecord),
111 Discard,
113}
114
115#[async_trait(?Send)]
116pub trait OnNoteReceived {
117 async fn on_note_received(
128 &self,
129 committed_note: CommittedNote,
130 public_note: Option<InputNoteRecord>,
131 ) -> Result<NoteUpdateAction, ClientError>;
132}
133#[derive(Clone)]
140pub struct StateSync {
141 rpc_api: Arc<dyn NodeRpcClient>,
143 note_screener: Arc<dyn OnNoteReceived>,
146 note_observers: Vec<Arc<dyn NoteObserver>>,
149 tx_discard_delta: Option<u32>,
152 sync_nullifiers: bool,
155}
156
157impl StateSync {
158 pub fn new(
169 rpc_api: Arc<dyn NodeRpcClient>,
170 note_screener: Arc<dyn OnNoteReceived>,
171 tx_discard_delta: Option<u32>,
172 ) -> Self {
173 Self {
174 rpc_api,
175 note_screener,
176 note_observers: Vec::new(),
177 tx_discard_delta,
178 sync_nullifiers: true,
179 }
180 }
181
182 #[must_use]
186 pub fn with_note_observer(mut self, observer: Arc<dyn NoteObserver>) -> Self {
187 self.note_observers.push(observer);
188 self
189 }
190
191 pub fn disable_nullifier_sync(&mut self) {
197 self.sync_nullifiers = false;
198 }
199
200 pub fn enable_nullifier_sync(&mut self) {
202 self.sync_nullifiers = true;
203 }
204
205 pub(crate) async fn run_apply_hooks(
212 &self,
213 state_sync_update: &StateSyncUpdate,
214 ) -> Result<(), ClientError> {
215 for observer in &self.note_observers {
216 crate::errors::log_observer_failure(
217 observer.name(),
218 "NoteObserver::apply",
219 observer.apply(state_sync_update).await,
220 );
221 }
222 Ok(())
223 }
224
225 pub async fn sync_state(
243 &self,
244 current_partial_mmr: &mut PartialMmr,
245 input: StateSyncInput,
246 ) -> Result<StateSyncUpdate, ClientError> {
247 let StateSyncInput {
248 accounts,
249 note_tags,
250 input_notes,
251 output_notes,
252 uncommitted_transactions,
253 } = input;
254 let block_num = u32::try_from(current_partial_mmr.forest().num_leaves().saturating_sub(1))
255 .map_err(|_| ClientError::InvalidPartialMmrForest)?
256 .into();
257
258 let note_tags = Arc::new(note_tags);
259 let account_ids: Vec<AccountId> = accounts.iter().map(AccountHeader::id).collect();
260
261 let mut state_sync_update = StateSyncUpdate {
262 block_num,
263 note_updates: NoteUpdateTracker::new(input_notes, output_notes),
264 transaction_updates: TransactionUpdateTracker::new(uncommitted_transactions),
265 ..Default::default()
266 };
267 let Some(sync_data) = self
268 .fetch_sync_data(state_sync_update.block_num, &account_ids, ¬e_tags)
269 .await?
270 else {
271 return Ok(state_sync_update);
273 };
274
275 state_sync_update.block_num = sync_data.chain_tip_header.block_num();
276
277 let new_commitments = derive_account_commitments(&sync_data.transactions);
278 let superseded_states = self
279 .account_state_sync(
280 &mut state_sync_update.account_updates,
281 &accounts,
282 &new_commitments,
283 block_num,
284 &sync_data.chain_tip_header,
285 )
286 .await?;
287
288 for superseded_state in superseded_states {
290 state_sync_update
291 .transaction_updates
292 .apply_superseded_account_state(superseded_state);
293 }
294
295 self.apply_sync_result(sync_data, &mut state_sync_update, current_partial_mmr)
297 .await?;
298
299 if self.sync_nullifiers {
300 self.nullifiers_state_sync(&mut state_sync_update, block_num).await?;
301 }
302
303 Ok(state_sync_update)
304 }
305
306 async fn fetch_sync_data(
315 &self,
316 current_block_num: BlockNumber,
317 account_ids: &[AccountId],
318 note_tags: &Arc<BTreeSet<NoteTag>>,
319 ) -> Result<Option<FetchedSyncData>, ClientError> {
320 let chain_mmr_info = self
322 .rpc_api
323 .sync_chain_mmr(current_block_num, SyncTarget::CommittedChainTip)
324 .await?;
325 let chain_tip = chain_mmr_info.block_to;
326
327 Self::validate_chain_mmr_response(&chain_mmr_info, current_block_num)?;
329
330 if chain_tip == current_block_num {
332 info!(block_num = %current_block_num, "Already at chain tip, nothing to sync.");
333 return Ok(None);
334 }
335
336 info!(
337 block_from = %current_block_num,
338 block_to = %chain_tip,
339 "Syncing state.",
340 );
341
342 let (note_blocks, synced_notes) = if note_tags.is_empty() {
347 (Vec::new(), BTreeMap::new())
348 } else {
349 self.rpc_api
350 .sync_notes_with_details(current_block_num + 1, chain_tip, note_tags.as_ref())
351 .await?
352 };
353
354 Self::validate_note_blocks_range(¬e_blocks, current_block_num, chain_tip)?;
356
357 let note_count: usize = note_blocks.iter().map(|b| b.notes.len()).sum();
358 info!(
359 blocks_with_notes = note_blocks.len(),
360 notes = note_count,
361 synced_notes = synced_notes.len(),
362 "Fetched note sync data.",
363 );
364
365 let transaction_records = if account_ids.is_empty() {
368 Vec::new()
369 } else {
370 self.rpc_api
371 .sync_transactions(current_block_num + 1, chain_tip, account_ids.to_vec())
372 .await?
373 };
374
375 Self::validate_transaction_records_range(
376 &transaction_records,
377 current_block_num,
378 chain_tip,
379 )?;
380
381 Ok(Some(FetchedSyncData {
382 mmr_delta: chain_mmr_info.mmr_delta,
383 chain_tip_header: chain_mmr_info.block_header,
384 note_blocks,
385 synced_notes,
386 transactions: transaction_records,
387 }))
388 }
389
390 async fn apply_sync_result(
400 &self,
401 sync_data: FetchedSyncData,
402 state_sync_update: &mut StateSyncUpdate,
403 current_partial_mmr: &mut PartialMmr,
404 ) -> Result<(), ClientError> {
405 let FetchedSyncData {
406 mmr_delta,
407 chain_tip_header,
408 note_blocks,
409 synced_notes,
410 transactions,
411 } = sync_data;
412
413 let mut working_mmr = current_partial_mmr.clone();
416
417 Self::advance_mmr(
418 mmr_delta,
419 &chain_tip_header,
420 &mut working_mmr,
421 &mut state_sync_update.partial_blockchain_updates,
422 )?;
423
424 self.screen_note_blocks(note_blocks, synced_notes, state_sync_update, &mut working_mmr)
425 .await?;
426
427 self.apply_transactions_and_nullifiers(
428 &chain_tip_header,
429 &transactions,
430 state_sync_update,
431 )?;
432
433 *current_partial_mmr = working_mmr;
435
436 Ok(())
437 }
438
439 fn validate_chain_mmr_response(
441 chain_mmr_info: &ChainMmrInfo,
442 current_block_num: BlockNumber,
443 ) -> Result<(), ClientError> {
444 if chain_mmr_info.block_header.block_num() != chain_mmr_info.block_to {
445 return Err(ClientError::ChainValidationError(format!(
446 "sync_chain_mmr block_header.block_num ({}) does not match block_to ({})",
447 chain_mmr_info.block_header.block_num(),
448 chain_mmr_info.block_to
449 )));
450 }
451 if chain_mmr_info.block_from != current_block_num {
452 return Err(ClientError::ChainValidationError(format!(
453 "sync_chain_mmr block_from mismatch: expected {current_block_num}, got {}",
454 chain_mmr_info.block_from
455 )));
456 }
457 if chain_mmr_info.block_to < current_block_num {
458 return Err(ClientError::ChainValidationError(format!(
459 "sync_chain_mmr block_to ({}) is behind current block {current_block_num}",
460 chain_mmr_info.block_to
461 )));
462 }
463 Ok(())
464 }
465
466 fn validate_note_blocks_range(
469 note_blocks: &[NoteSyncBlock],
470 current_block_num: BlockNumber,
471 chain_tip: BlockNumber,
472 ) -> Result<(), ClientError> {
473 for block in note_blocks {
474 let block_num = block.block_header.block_num();
475 if block_num <= current_block_num || block_num > chain_tip {
476 return Err(ClientError::ChainValidationError(format!(
477 "sync_notes returned block {block_num} outside requested range ({current_block_num}, {chain_tip}]"
478 )));
479 }
480 }
481 Ok(())
482 }
483
484 fn validate_transaction_records_range(
487 records: &[RpcTransactionRecord],
488 current_block_num: BlockNumber,
489 chain_tip: BlockNumber,
490 ) -> Result<(), ClientError> {
491 for record in records {
492 let block_num = record.block_num;
493 if block_num <= current_block_num || block_num > chain_tip {
494 return Err(ClientError::ChainValidationError(format!(
495 "sync_transactions returned block {block_num} outside requested range ({current_block_num}, {chain_tip}]"
496 )));
497 }
498 }
499 Ok(())
500 }
501
502 fn advance_mmr(
509 mmr_delta: MmrDelta,
510 chain_tip_header: &BlockHeader,
511 current_partial_mmr: &mut PartialMmr,
512 partial_blockchain_updates: &mut PartialBlockchainUpdates,
513 ) -> Result<(), ClientError> {
514 let mut new_authentication_nodes =
515 current_partial_mmr.apply(mmr_delta).map_err(StoreError::MmrError)?;
516 let new_peaks = current_partial_mmr.peaks();
517
518 let peaks_commitment = new_peaks.hash_peaks();
522 if peaks_commitment != chain_tip_header.chain_commitment() {
523 return Err(ClientError::ChainValidationError(format!(
524 "MMR peaks commitment is {} and does not match block header chain commitment {}",
525 peaks_commitment.to_hex(),
526 chain_tip_header.chain_commitment().to_hex()
527 )));
528 }
529
530 partial_blockchain_updates.new_peaks = new_peaks;
531
532 new_authentication_nodes.append(
537 &mut current_partial_mmr
538 .add(chain_tip_header.commitment(), false)
539 .map_err(StoreError::MmrError)?,
540 );
541
542 partial_blockchain_updates.insert(
543 chain_tip_header.clone(),
544 false,
545 new_authentication_nodes,
546 );
547
548 Ok(())
549 }
550
551 async fn screen_note_blocks(
555 &self,
556 note_blocks: Vec<NoteSyncBlock>,
557 synced_notes: BTreeMap<NoteId, SyncedNoteDetails>,
558 state_sync_update: &mut StateSyncUpdate,
559 current_partial_mmr: &mut PartialMmr,
560 ) -> Result<(), ClientError> {
561 let private_attachments: BTreeMap<NoteId, NoteAttachments> = synced_notes
564 .iter()
565 .filter_map(|(id, synced)| match synced {
566 SyncedNoteDetails::Private(Some(attachments)) => Some((*id, attachments.clone())),
567 _ => None,
568 })
569 .collect();
570 let public_note_records = Self::build_public_note_records(synced_notes, ¬e_blocks);
571
572 for block in note_blocks {
573 let found_relevant_note = self
574 .note_state_sync(
575 &mut state_sync_update.note_updates,
576 block.notes,
577 &block.block_header,
578 &public_note_records,
579 &private_attachments,
580 )
581 .await?;
582
583 if found_relevant_note {
584 let block_pos = block.block_header.block_num().as_usize();
585
586 let nodes_before: BTreeMap<_, _> =
587 current_partial_mmr.nodes().map(|(k, v)| (*k, *v)).collect();
588
589 if !current_partial_mmr.is_tracked(block_pos) {
590 current_partial_mmr
591 .track(block_pos, block.block_header.commitment(), &block.mmr_path)
592 .map_err(StoreError::MmrError)?;
593 }
594
595 let track_auth_nodes: Vec<_> = current_partial_mmr
600 .nodes()
601 .filter(|(k, _)| !nodes_before.contains_key(k))
602 .map(|(k, v)| (*k, *v))
603 .collect();
604
605 state_sync_update.partial_blockchain_updates.insert(
606 block.block_header,
607 true,
608 track_auth_nodes,
609 );
610 }
611 }
612
613 Ok(())
614 }
615
616 fn apply_transactions_and_nullifiers(
620 &self,
621 chain_tip_header: &BlockHeader,
622 transactions: &[RpcTransactionRecord],
623 state_sync_update: &mut StateSyncUpdate,
624 ) -> Result<(), ClientError> {
625 state_sync_update
626 .note_updates
627 .extend_nullifiers(compute_ordered_nullifiers(transactions));
628
629 for record in transactions {
630 state_sync_update
631 .transaction_updates
632 .apply_transaction_inclusion(record, u64::from(chain_tip_header.timestamp())); }
634 state_sync_update
635 .transaction_updates
636 .apply_sync_height_update(chain_tip_header.block_num(), self.tx_discard_delta);
637
638 for transaction in transactions {
639 state_sync_update
643 .note_updates
644 .apply_output_note_inclusion_proofs(&transaction.output_notes)?;
645
646 Self::mark_erased_notes_as_consumed(state_sync_update, transaction);
648 }
649
650 Ok(())
651 }
652
653 fn mark_erased_notes_as_consumed(
659 state_sync_update: &mut StateSyncUpdate,
660 transaction: &RpcTransactionRecord,
661 ) {
662 for note_header in &transaction.erased_output_notes {
663 let _ = state_sync_update
665 .note_updates
666 .mark_erased_note_as_consumed(note_header, transaction.block_num);
667 }
668 }
669
670 async fn account_state_sync(
683 &self,
684 account_updates: &mut AccountUpdates,
685 accounts: &[AccountHeader],
686 account_commitment_updates: &[(AccountId, Word)],
687 block_from: BlockNumber,
688 chain_tip_header: &BlockHeader,
689 ) -> Result<Vec<Word>, ClientError> {
690 let (public_accounts, private_accounts): (Vec<_>, Vec<_>) =
693 accounts.iter().partition(|header| !header.id().is_private());
694
695 let superseded_states = self
696 .sync_public_accounts(
697 account_updates,
698 account_commitment_updates,
699 &public_accounts,
700 block_from,
701 chain_tip_header,
702 )
703 .await?;
704
705 let mut mismatched_private_accounts = Vec::new();
708 for header in &private_accounts {
709 let account_id = header.id();
710 let local_commitment = header.to_commitment();
711 let record_diverges = account_commitment_updates
712 .iter()
713 .any(|(id, digest)| *id == account_id && *digest != local_commitment);
714 if !record_diverges {
715 continue;
716 }
717
718 if let Some(proven_commitment) = self
719 .verify_private_account_mismatch(account_id, local_commitment, chain_tip_header)
720 .await?
721 {
722 mismatched_private_accounts.push((account_id, proven_commitment));
723 }
724 }
725
726 account_updates.extend(AccountUpdates::new(Vec::new(), mismatched_private_accounts));
727
728 Ok(superseded_states)
729 }
730
731 async fn verify_private_account_mismatch(
742 &self,
743 account_id: AccountId,
744 local_commitment: Word,
745 chain_tip_header: &BlockHeader,
746 ) -> Result<Option<Word>, ClientError> {
747 let chain_tip = chain_tip_header.block_num();
748 let (proof_block_num, proof) = self
749 .rpc_api
750 .get_account(account_id, GetAccountRequest::new().at(AccountStateAt::Block(chain_tip)))
751 .await?;
752
753 if proof_block_num != chain_tip {
754 return Err(ClientError::ChainValidationError(format!(
755 "get_account returned a proof at block {proof_block_num}, expected chain tip {chain_tip}"
756 )));
757 }
758
759 let (witness, _) = proof.into_parts();
760 let witness_id = witness.id();
761 let proven_commitment = witness.state_commitment();
762 if witness.into_proof().compute_root() != chain_tip_header.account_root() {
765 return Err(ClientError::ChainValidationError(format!(
766 "account witness for {account_id} does not verify against the chain tip account root"
767 )));
768 }
769
770 if witness_id != account_id
773 || proven_commitment == Word::empty()
774 || proven_commitment == local_commitment
775 {
776 return Ok(None);
777 }
778
779 Ok(Some(proven_commitment))
780 }
781
782 async fn sync_public_accounts(
791 &self,
792 account_updates: &mut AccountUpdates,
793 commitment_updates: &[(AccountId, Word)],
794 current_public_accounts: &[&AccountHeader],
795 block_from: BlockNumber,
796 chain_tip_header: &BlockHeader,
797 ) -> Result<Vec<Word>, ClientError> {
798 let local_headers: BTreeMap<AccountId, &AccountHeader> =
799 current_public_accounts.iter().map(|header| (header.id(), *header)).collect();
800 let mut superseded_states = Vec::new();
802 for (id, commitment) in commitment_updates {
803 let Some(local_header) = local_headers.get(id).copied() else {
804 continue;
805 };
806
807 if local_header.to_commitment() == *commitment {
808 continue;
809 }
810
811 match self
812 .sync_public_account(*id, local_header, block_from, chain_tip_header)
813 .await?
814 {
815 PublicAccountSync::Apply(public_update) => {
816 account_updates.extend(AccountUpdates::new(vec![*public_update], Vec::new()));
817 },
818 PublicAccountSync::Superseded => {
819 superseded_states.push(local_header.to_commitment());
820 },
821 PublicAccountSync::Ignore => {},
822 }
823 }
824
825 Ok(superseded_states)
826 }
827
828 async fn sync_public_account(
842 &self,
843 account_id: AccountId,
844 local_header: &AccountHeader,
845 block_from: BlockNumber,
846 chain_tip_header: &BlockHeader,
847 ) -> Result<PublicAccountSync, ClientError> {
848 let target_block_num = chain_tip_header.block_num();
849
850 let (proof_block_num, proof) = self
853 .rpc_api
854 .get_account(
855 account_id,
856 GetAccountRequest::new()
857 .at(AccountStateAt::Block(target_block_num))
858 .with_storage(StorageMapFetch::All)
859 .with_vault(VaultFetch::Always),
860 )
861 .await
862 .map_err(ClientError::RpcError)?;
863
864 let details =
865 Self::validate_account_proof(proof, proof_block_num, account_id, chain_tip_header)?;
866
867 match details
868 .header
869 .nonce()
870 .as_canonical_u64()
871 .cmp(&local_header.nonce().as_canonical_u64())
872 {
873 Ordering::Less => return Ok(PublicAccountSync::Ignore),
876 Ordering::Equal => return Ok(PublicAccountSync::Superseded),
878 Ordering::Greater => {},
880 }
881
882 let vault_oversized = details.vault_details.too_many_assets;
883 let any_map_oversized =
884 details.storage_details.map_details.iter().any(|m| m.too_many_entries);
885
886 let public_update = if vault_oversized || any_map_oversized {
890 self.build_patch_update(account_id, &details, block_from, proof_block_num)
892 .await?
893 } else {
894 let account = Account::try_from(&details).map_err(ClientError::RpcError)?;
896 PublicAccountUpdate::Full(account)
897 };
898
899 Ok(PublicAccountSync::Apply(Box::new(public_update)))
900 }
901
902 fn validate_account_proof(
906 proof: AccountProof,
907 proof_block_num: BlockNumber,
908 account_id: AccountId,
909 chain_tip_header: &BlockHeader,
910 ) -> Result<AccountDetails, ClientError> {
911 let target_block_num = chain_tip_header.block_num();
912
913 if proof_block_num != target_block_num {
914 return Err(ClientError::ChainValidationError(format!(
915 "get_account returned block {proof_block_num} but {target_block_num} was requested"
916 )));
917 }
918
919 let (witness, details) = proof.into_parts();
920
921 if witness.id() != account_id {
923 return Err(ClientError::ChainValidationError(format!(
924 "get_account returned account {} but {account_id} was requested",
925 witness.id()
926 )));
927 }
928
929 let account_key = AccountIdKey::from(account_id).as_word();
930 let state_commitment = witness.state_commitment();
931 witness
932 .into_proof()
933 .verify_presence(&account_key, &state_commitment, &chain_tip_header.account_root())
934 .map_err(|err| {
935 ClientError::ChainValidationError(format!(
936 "get_account witness for account {account_id} does not open under block \
937 {target_block_num} account root: {err}"
938 ))
939 })?;
940
941 Ok(details.expect("node returned no details for a public account"))
942 }
943
944 async fn build_patch_update(
947 &self,
948 account_id: AccountId,
949 details: &AccountDetails,
950 block_from: BlockNumber,
951 block_to: BlockNumber,
952 ) -> Result<PublicAccountUpdate, ClientError> {
953 let value_slot_updates: Vec<(_, Word)> = details
954 .storage_details
955 .header
956 .slots()
957 .filter(|slot| slot.slot_type() == StorageSlotType::Value)
958 .map(|slot| (slot.name().clone(), slot.value()))
959 .collect();
960
961 let map_info = self
964 .rpc_api
965 .sync_storage_maps(block_from + 1, block_to, account_id)
966 .await
967 .map_err(ClientError::RpcError)?;
968 let vault_info = self
969 .rpc_api
970 .sync_account_vault(block_from + 1, block_to, account_id)
971 .await
972 .map_err(ClientError::RpcError)?;
973
974 let patch = build_account_patch(
975 &details.header,
976 value_slot_updates,
977 map_info.map_entries,
978 vault_info.vault_patch,
979 details.code.clone(),
980 )
981 .map_err(StoreError::AccountPatchError)?;
982
983 Ok(PublicAccountUpdate::Patch {
984 new_header: details.header.clone(),
985 patch,
986 })
987 }
988
989 async fn note_state_sync(
1006 &self,
1007 note_updates: &mut NoteUpdateTracker,
1008 note_inclusions: BTreeMap<NoteId, CommittedNote>,
1009 block_header: &BlockHeader,
1010 public_notes: &BTreeMap<NoteId, InputNoteRecord>,
1011 private_attachments: &BTreeMap<NoteId, NoteAttachments>,
1012 ) -> Result<bool, ClientError> {
1013 let mut found_relevant_note = false;
1015
1016 for (_, committed_note) in note_inclusions {
1017 let public_note = (committed_note.note_type() != NoteType::Private)
1018 .then(|| public_notes.get(committed_note.note_id()))
1019 .flatten()
1020 .cloned();
1021
1022 if !self.note_observers.is_empty() {
1027 let note_attachments = if committed_note.note_type() == NoteType::Private {
1031 private_attachments.get(committed_note.note_id())
1032 } else {
1033 public_note.as_ref().map(InputNoteRecord::attachments)
1034 };
1035 for obs in &self.note_observers {
1036 match obs.observe(&committed_note, note_attachments).await {
1037 Ok(true) => found_relevant_note = true,
1038 Ok(false) => {},
1039 Err(err) => {
1040 tracing::warn!(
1041 observer = obs.name(),
1042 error = ?err,
1043 "note observer failed; sync continues",
1044 );
1045 },
1046 }
1047 }
1048 }
1049
1050 match self.note_screener.on_note_received(committed_note, public_note).await? {
1051 NoteUpdateAction::Commit(committed_note) => {
1052 let attachments = private_attachments.get(committed_note.note_id());
1056 found_relevant_note |= note_updates.apply_committed_note_state_transitions(
1057 &committed_note,
1058 block_header,
1059 attachments,
1060 )?;
1061 },
1062 NoteUpdateAction::Insert(public_note) => {
1063 found_relevant_note = true;
1064
1065 note_updates.apply_new_public_note(public_note, block_header)?;
1066 },
1067 NoteUpdateAction::Discard => {},
1068 }
1069 }
1070
1071 Ok(found_relevant_note)
1072 }
1073
1074 async fn nullifiers_state_sync(
1080 &self,
1081 state_sync_update: &mut StateSyncUpdate,
1082 current_block_num: BlockNumber,
1083 ) -> Result<(), ClientError> {
1084 let nullifiers_tags: Vec<u16> = state_sync_update
1090 .note_updates
1091 .unspent_nullifiers()
1092 .map(|nullifier| nullifier.prefix())
1093 .collect();
1094
1095 let mut new_nullifiers = self
1096 .rpc_api
1097 .sync_nullifiers(&nullifiers_tags, current_block_num + 1, state_sync_update.block_num)
1098 .await?;
1099
1100 new_nullifiers.retain(|update| update.block_num <= state_sync_update.block_num);
1103
1104 let consumptions: Vec<NoteConsumption> = new_nullifiers
1106 .into_iter()
1107 .map(|update| NoteConsumption {
1108 external_consumer: state_sync_update
1109 .transaction_updates
1110 .external_nullifier_account(&update.nullifier),
1111 nullifier: update.nullifier,
1112 block_num: update.block_num,
1113 })
1114 .collect();
1115
1116 for consumption in consumptions {
1117 state_sync_update.note_updates.apply_note_consumption(
1118 &consumption,
1119 state_sync_update.transaction_updates.committed_transactions(),
1120 )?;
1121
1122 state_sync_update
1126 .transaction_updates
1127 .apply_input_note_nullified(consumption.nullifier);
1128 }
1129
1130 Ok(())
1131 }
1132
1133 fn build_public_note_records(
1136 synced_notes: BTreeMap<NoteId, SyncedNoteDetails>,
1137 note_blocks: &[NoteSyncBlock],
1138 ) -> BTreeMap<NoteId, InputNoteRecord> {
1139 let mut records = BTreeMap::new();
1140 for (note_id, synced) in synced_notes {
1141 let SyncedNoteDetails::Public(note) = synced else {
1142 continue;
1143 };
1144 let inclusion_proof = note_blocks
1145 .iter()
1146 .find_map(|b| b.notes.get(¬e_id))
1147 .map(|committed| committed.inclusion_proof().clone());
1148
1149 if let Some(inclusion_proof) = inclusion_proof {
1150 let state = crate::store::input_note_states::UnverifiedNoteState {
1151 metadata: *note.metadata(),
1152 inclusion_proof,
1153 }
1154 .into();
1155 let attachments = note.attachments().clone();
1156 let record = InputNoteRecord::new(note.into(), attachments, None, state);
1157 let id = record.id().expect("CommittedNoteState carries metadata, so id() is Some");
1158 records.insert(id, record);
1159 }
1160 }
1161 records
1162 }
1163}
1164
1165fn group_txs_by_account_block(
1170 transaction_records: &[RpcTransactionRecord],
1171) -> BTreeMap<(AccountId, BlockNumber), Vec<&RpcTransactionRecord>> {
1172 let mut groups: BTreeMap<(AccountId, BlockNumber), Vec<&RpcTransactionRecord>> =
1173 BTreeMap::new();
1174 for record in transaction_records {
1175 let account_id = record.transaction_header.account_id();
1176 groups.entry((account_id, record.block_num)).or_default().push(record);
1177 }
1178 groups
1179}
1180
1181fn walk_execution_chain<'a>(
1187 txs: &'a [&'a RpcTransactionRecord],
1188) -> impl Iterator<Item = &'a RpcTransactionRecord> + 'a {
1189 let (self_loops, chained): (Vec<&RpcTransactionRecord>, Vec<&RpcTransactionRecord>) =
1190 txs.iter().copied().partition(|tx| {
1191 tx.transaction_header.initial_state_commitment()
1192 == tx.transaction_header.final_state_commitment()
1193 });
1194
1195 let final_states: BTreeSet<Word> = chained
1196 .iter()
1197 .map(|tx| tx.transaction_header.final_state_commitment())
1198 .collect();
1199
1200 let mut init_to_tx: BTreeMap<Word, &RpcTransactionRecord> = chained
1201 .iter()
1202 .map(|tx| (tx.transaction_header.initial_state_commitment(), *tx))
1203 .collect();
1204
1205 let start = chained
1206 .iter()
1207 .find(|tx| !final_states.contains(&tx.transaction_header.initial_state_commitment()))
1208 .copied();
1209
1210 assert!(start.is_some() || chained.is_empty(), "cannot walk cyclic execution chain");
1211
1212 let mut current =
1213 start.and_then(|tx| init_to_tx.remove(&tx.transaction_header.initial_state_commitment()));
1214 let mut self_loops_iter = self_loops.into_iter();
1215
1216 core::iter::from_fn(move || {
1217 if let Some(tx) = current {
1218 current = init_to_tx.remove(&tx.transaction_header.final_state_commitment());
1219 return Some(tx);
1220 }
1221 self_loops_iter.next()
1222 })
1223}
1224
1225fn derive_account_commitments(
1230 transaction_records: &[RpcTransactionRecord],
1231) -> Vec<(AccountId, Word)> {
1232 let mut latest_by_account: BTreeMap<AccountId, (BlockNumber, Word)> = BTreeMap::new();
1233
1234 for ((account_id, block_num), txs) in &group_txs_by_account_block(transaction_records) {
1235 let terminal_state = walk_execution_chain(txs)
1236 .last()
1237 .expect("account must have a final state")
1238 .transaction_header
1239 .final_state_commitment();
1240
1241 latest_by_account
1242 .entry(*account_id)
1243 .and_modify(|(existing_block, existing_state)| {
1244 if *block_num > *existing_block {
1245 *existing_block = *block_num;
1246 *existing_state = terminal_state;
1247 }
1248 })
1249 .or_insert((*block_num, terminal_state));
1250 }
1251
1252 latest_by_account
1253 .into_iter()
1254 .map(|(account_id, (_, state))| (account_id, state))
1255 .collect()
1256}
1257
1258fn compute_ordered_nullifiers(transaction_records: &[RpcTransactionRecord]) -> Vec<Nullifier> {
1265 let mut result = Vec::new();
1266
1267 for txs in group_txs_by_account_block(transaction_records).values() {
1268 for tx in walk_execution_chain(txs) {
1269 for commitment in tx.transaction_header.input_notes().iter() {
1270 result.push(commitment.nullifier());
1271 }
1272 }
1273 }
1274
1275 result
1276}
1277
1278#[cfg(all(test, feature = "testing"))]
1279mod tests {
1280 use alloc::collections::BTreeSet;
1281 use alloc::sync::Arc;
1282
1283 use async_trait::async_trait;
1284 use miden_protocol::account::Account;
1285 use miden_protocol::assembly::DefaultSourceManager;
1286 use miden_protocol::asset::{Asset, FungibleAsset};
1287 use miden_protocol::block::BlockNumber;
1288 use miden_protocol::crypto::merkle::MerklePath;
1289 use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, PartialMmr};
1290 use miden_protocol::note::{
1291 Note,
1292 NoteAssets,
1293 NoteAttachment,
1294 NoteAttachments,
1295 NoteDetails,
1296 NoteHeader,
1297 NoteMetadata,
1298 NoteRecipient,
1299 NoteStorage,
1300 NoteTag,
1301 NoteType,
1302 PartialNoteMetadata,
1303 };
1304 use miden_protocol::testing::account_id::{
1305 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1306 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1307 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
1308 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
1309 ACCOUNT_ID_SENDER,
1310 };
1311 use miden_protocol::transaction::{InputNotes, TransactionArgs, TransactionHeader};
1312 use miden_protocol::vm::AdviceMap;
1313 use miden_protocol::{EMPTY_WORD, Felt, Word, ZERO};
1314 use miden_standards::code_builder::CodeBuilder;
1315 use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint};
1316 use miden_testing::{MockChainBuilder, TxContextInput};
1317
1318 use super::*;
1319 use crate::store::{OutputNoteRecord, OutputNoteState};
1320 use crate::test_utils::mock::MockRpcApi;
1321
1322 struct MockScreener;
1324
1325 #[async_trait(?Send)]
1326 impl OnNoteReceived for MockScreener {
1327 async fn on_note_received(
1328 &self,
1329 _committed_note: CommittedNote,
1330 _public_note: Option<InputNoteRecord>,
1331 ) -> Result<NoteUpdateAction, ClientError> {
1332 Ok(NoteUpdateAction::Discard)
1333 }
1334 }
1335
1336 fn empty() -> StateSyncInput {
1337 StateSyncInput {
1338 accounts: vec![],
1339 note_tags: BTreeSet::new(),
1340 input_notes: vec![],
1341 output_notes: vec![],
1342 uncommitted_transactions: vec![],
1343 }
1344 }
1345
1346 fn word(n: u64) -> miden_protocol::Word {
1347 [
1348 Felt::new(n).expect("test value should fit into the base field"),
1349 ZERO,
1350 ZERO,
1351 ZERO,
1352 ]
1353 .into()
1354 }
1355
1356 fn header_with_account_root(header: &BlockHeader, account_root: Word) -> BlockHeader {
1357 BlockHeader::new(
1358 header.version(),
1359 header.prev_block_commitment(),
1360 header.block_num(),
1361 header.chain_commitment(),
1362 account_root,
1363 header.nullifier_root(),
1364 header.note_root(),
1365 header.tx_commitment(),
1366 header.tx_kernel_commitment(),
1367 header.validator_keys().clone(),
1368 header.fee_parameters().clone(),
1369 header.timestamp(),
1370 )
1371 }
1372
1373 #[tokio::test]
1374 async fn sync_public_accounts_ignores_older_node_snapshot() {
1375 let mut builder = MockChainBuilder::new();
1376 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1377 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1378 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1379 let state_sync = StateSync::new(Arc::new(rpc_api), Arc::new(MockScreener), None);
1380
1381 let local_header =
1384 AccountHeader::new(account.id(), Felt::from(2u32), EMPTY_WORD, EMPTY_WORD, EMPTY_WORD);
1385 let current_public_accounts = vec![&local_header];
1386 let commitment_updates = vec![(account.id(), account.to_commitment())];
1387 let mut account_updates = AccountUpdates::default();
1388
1389 let superseded = state_sync
1390 .sync_public_accounts(
1391 &mut account_updates,
1392 &commitment_updates,
1393 ¤t_public_accounts,
1394 BlockNumber::GENESIS,
1395 &chain_tip_header,
1396 )
1397 .await
1398 .unwrap();
1399
1400 assert!(
1401 account_updates.updated_public_accounts().is_empty(),
1402 "public account sync should ignore node snapshots that are older than local"
1403 );
1404 assert!(
1405 superseded.is_empty(),
1406 "an older node snapshot must not supersede the local state"
1407 );
1408 }
1409
1410 #[tokio::test]
1411 async fn sync_public_accounts_marks_same_nonce_mismatch_as_superseded() {
1412 let mut builder = MockChainBuilder::new();
1413 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1414 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1415 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1416 let state_sync = StateSync::new(Arc::new(rpc_api), Arc::new(MockScreener), None);
1417
1418 let local_header =
1421 AccountHeader::new(account.id(), account.nonce(), EMPTY_WORD, EMPTY_WORD, EMPTY_WORD);
1422 let current_public_accounts = vec![&local_header];
1423 let commitment_updates = vec![(account.id(), account.to_commitment())];
1424 let mut account_updates = AccountUpdates::default();
1425
1426 let superseded = state_sync
1427 .sync_public_accounts(
1428 &mut account_updates,
1429 &commitment_updates,
1430 ¤t_public_accounts,
1431 BlockNumber::GENESIS,
1432 &chain_tip_header,
1433 )
1434 .await
1435 .unwrap();
1436
1437 assert!(
1438 account_updates.updated_public_accounts().is_empty(),
1439 "a same-nonce fork must not overwrite the account while its tx is still pending"
1440 );
1441 assert_eq!(
1442 superseded,
1443 vec![local_header.to_commitment()],
1444 "the superseded local state should be reported so its transaction is discarded"
1445 );
1446 }
1447
1448 #[test]
1454 fn validate_transaction_records_range_rejects_out_of_range_blocks() {
1455 let account_id: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap();
1456 let current = BlockNumber::from(5u32);
1457 let chain_tip = BlockNumber::from(10u32);
1458
1459 StateSync::validate_transaction_records_range(
1460 &[make_tx_record(account_id, 7)],
1461 current,
1462 chain_tip,
1463 )
1464 .unwrap();
1465
1466 let result = StateSync::validate_transaction_records_range(
1467 &[make_tx_record(account_id, 11)],
1468 current,
1469 chain_tip,
1470 );
1471 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1472
1473 let result = StateSync::validate_transaction_records_range(
1474 &[make_tx_record(account_id, 5)],
1475 current,
1476 chain_tip,
1477 );
1478 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1479 }
1480
1481 #[tokio::test]
1484 async fn verify_private_account_mismatch_ignores_forged_commitment() {
1485 let mut builder = MockChainBuilder::new();
1486 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1487 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1488 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1489 let on_chain_commitment = account.to_commitment();
1490 let state_sync = StateSync::new(Arc::new(rpc_api), Arc::new(MockScreener), None);
1491
1492 let result = state_sync
1493 .verify_private_account_mismatch(account.id(), on_chain_commitment, &chain_tip_header)
1494 .await
1495 .unwrap();
1496
1497 assert!(
1498 result.is_none(),
1499 "an unproven commitment must not lock an account whose on-chain state matches local"
1500 );
1501 }
1502
1503 #[tokio::test]
1506 async fn verify_private_account_mismatch_reports_proven_divergence() {
1507 let mut builder = MockChainBuilder::new();
1508 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1509 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1510 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1511 let on_chain_commitment = account.to_commitment();
1512 let state_sync = StateSync::new(Arc::new(rpc_api), Arc::new(MockScreener), None);
1513 let stale_local_commitment = word(0xdead_beef);
1514
1515 let result = state_sync
1516 .verify_private_account_mismatch(
1517 account.id(),
1518 stale_local_commitment,
1519 &chain_tip_header,
1520 )
1521 .await
1522 .unwrap();
1523
1524 assert_eq!(
1525 result,
1526 Some(on_chain_commitment),
1527 "a proven divergence should return the proven commitment to lock with"
1528 );
1529 }
1530
1531 #[tokio::test]
1534 async fn verify_private_account_mismatch_rejects_unverifiable_proof() {
1535 let mut builder = MockChainBuilder::new();
1536 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1537 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1538 let real_header = rpc_api.mock_chain.read().latest_block_header();
1539 let state_sync = StateSync::new(Arc::new(rpc_api), Arc::new(MockScreener), None);
1540
1541 let tampered_header = BlockHeader::new(
1544 real_header.version(),
1545 real_header.prev_block_commitment(),
1546 real_header.block_num(),
1547 real_header.chain_commitment(),
1548 word(0xbad0_bad0),
1549 real_header.nullifier_root(),
1550 real_header.note_root(),
1551 real_header.tx_commitment(),
1552 real_header.tx_kernel_commitment(),
1553 real_header.validator_keys().clone(),
1554 real_header.fee_parameters().clone(),
1555 real_header.timestamp(),
1556 );
1557
1558 let result = state_sync
1559 .verify_private_account_mismatch(
1560 account.id(),
1561 account.to_commitment(),
1562 &tampered_header,
1563 )
1564 .await;
1565 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1566 }
1567 async fn get_account_proof(
1569 rpc_api: &MockRpcApi,
1570 account_id: AccountId,
1571 ) -> (BlockNumber, AccountProof) {
1572 rpc_api
1573 .get_account(
1574 account_id,
1575 GetAccountRequest::new()
1576 .with_storage(StorageMapFetch::All)
1577 .with_vault(VaultFetch::Always),
1578 )
1579 .await
1580 .unwrap()
1581 }
1582
1583 #[tokio::test]
1585 async fn validate_account_proof_rejects_mismatched_account() {
1586 let mut builder = MockChainBuilder::new();
1587 let account_a = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1588 let account_b = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1589 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1590 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1591
1592 let (proof_block_num, proof) = get_account_proof(&rpc_api, account_b.id()).await;
1594 let result = StateSync::validate_account_proof(
1595 proof,
1596 proof_block_num,
1597 account_a.id(),
1598 &chain_tip_header,
1599 );
1600
1601 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1602 }
1603
1604 #[tokio::test]
1606 async fn validate_account_proof_rejects_wrong_account_root() {
1607 let mut builder = MockChainBuilder::new();
1608 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1609 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1610 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1611 let wrong_header = header_with_account_root(&chain_tip_header, word(999));
1612
1613 let (proof_block_num, proof) = get_account_proof(&rpc_api, account.id()).await;
1615 let result =
1616 StateSync::validate_account_proof(proof, proof_block_num, account.id(), &wrong_header);
1617
1618 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1619 }
1620
1621 #[tokio::test]
1623 async fn validate_account_proof_rejects_wrong_block() {
1624 let mut builder = MockChainBuilder::new();
1625 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1626 let rpc_api = MockRpcApi::new(builder.build().unwrap());
1627 let chain_tip_header = rpc_api.mock_chain.read().latest_block_header();
1628
1629 let (proof_block_num, proof) = get_account_proof(&rpc_api, account.id()).await;
1631 let result = StateSync::validate_account_proof(
1632 proof,
1633 proof_block_num + 1,
1634 account.id(),
1635 &chain_tip_header,
1636 );
1637
1638 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
1639 }
1640
1641 mod compute_nullifiers_tests {
1645 use alloc::vec;
1646
1647 use miden_protocol::block::BlockNumber;
1648 use miden_protocol::note::Nullifier;
1649 use miden_protocol::transaction::{InputNoteCommitment, InputNotes, TransactionHeader};
1650
1651 use super::word;
1652 use crate::rpc::domain::transaction::TransactionRecord as RpcTransactionRecord;
1653
1654 fn make_rpc_tx(
1655 init_state: u64,
1656 final_state: u64,
1657 nullifier_vals: &[u64],
1658 block_number: u32,
1659 ) -> RpcTransactionRecord {
1660 let account_id = miden_protocol::account::AccountId::try_from(
1661 miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
1662 )
1663 .unwrap();
1664
1665 let input_notes = InputNotes::new_unchecked(
1666 nullifier_vals
1667 .iter()
1668 .map(|v| InputNoteCommitment::from(Nullifier::from_raw(word(*v))))
1669 .collect(),
1670 );
1671
1672 RpcTransactionRecord {
1673 block_num: BlockNumber::from(block_number),
1674 transaction_header: TransactionHeader::new(
1675 account_id,
1676 word(init_state),
1677 word(final_state),
1678 input_notes,
1679 vec![],
1680 ),
1681 output_notes: vec![],
1682 erased_output_notes: vec![],
1683 }
1684 }
1685
1686 #[test]
1687 fn chains_rpc_transactions_by_state_commitment() {
1688 let tx_a = make_rpc_tx(1, 2, &[10], 5);
1691 let tx_b = make_rpc_tx(2, 3, &[20], 5);
1692 let tx_c = make_rpc_tx(3, 4, &[30], 5);
1693
1694 let result = super::super::compute_ordered_nullifiers(&[tx_c, tx_a, tx_b]);
1695
1696 assert_eq!(result[0], Nullifier::from_raw(word(10)));
1697 assert_eq!(result[1], Nullifier::from_raw(word(20)));
1698 assert_eq!(result[2], Nullifier::from_raw(word(30)));
1699 }
1700
1701 #[test]
1702 fn groups_independently_by_account_and_block() {
1703 let tx_a1 = make_rpc_tx(1, 2, &[10], 5);
1705 let tx_a2 = make_rpc_tx(2, 3, &[20], 5);
1706
1707 let tx_a3 = make_rpc_tx(3, 4, &[30], 6);
1709
1710 let account_b = miden_protocol::account::AccountId::try_from(
1712 miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1713 )
1714 .unwrap();
1715
1716 let tx_b1 = RpcTransactionRecord {
1717 block_num: BlockNumber::from(5u32),
1718 transaction_header: TransactionHeader::new(
1719 account_b,
1720 word(100),
1721 word(200),
1722 InputNotes::new_unchecked(vec![InputNoteCommitment::from(
1723 Nullifier::from_raw(word(40)),
1724 )]),
1725 vec![],
1726 ),
1727 output_notes: vec![],
1728 erased_output_notes: vec![],
1729 };
1730
1731 let result = super::super::compute_ordered_nullifiers(&[tx_a2, tx_b1, tx_a3, tx_a1]);
1732
1733 let pos = |val: u64| -> usize {
1736 result.iter().position(|n| *n == Nullifier::from_raw(word(val))).unwrap()
1737 };
1738
1739 assert!(pos(10) < pos(20)); assert!(result.contains(&Nullifier::from_raw(word(30)))); assert!(result.contains(&Nullifier::from_raw(word(40)))); }
1745
1746 #[test]
1747 fn multiple_nullifiers_per_transaction_are_consecutive() {
1748 let tx = make_rpc_tx(1, 2, &[10, 20, 30], 5);
1750
1751 let result = super::super::compute_ordered_nullifiers(&[tx]);
1752
1753 assert_eq!(result.len(), 3);
1754 assert!(result.contains(&Nullifier::from_raw(word(10))));
1755 assert!(result.contains(&Nullifier::from_raw(word(20))));
1756 assert!(result.contains(&Nullifier::from_raw(word(30))));
1757 }
1758
1759 #[test]
1760 fn empty_input_returns_empty_vec() {
1761 let result = super::super::compute_ordered_nullifiers(&[]);
1762 assert!(result.is_empty());
1763 }
1764 }
1765
1766 #[test]
1777 fn derive_account_commitments_walks_chains_per_account() {
1778 let make_tx = |account: AccountId, init_state: u64, final_state: u64, block_num: u32| {
1779 RpcTransactionRecord {
1780 block_num: BlockNumber::from(block_num),
1781 transaction_header: TransactionHeader::new(
1782 account,
1783 word(init_state),
1784 word(final_state),
1785 InputNotes::new_unchecked(vec![]),
1786 vec![],
1787 ),
1788 output_notes: vec![],
1789 erased_output_notes: vec![],
1790 }
1791 };
1792
1793 let account_a: AccountId =
1794 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE.try_into().unwrap();
1795 let account_b: AccountId = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap();
1796
1797 let tx_a_b5_1 = make_tx(account_a, 1, 2, 5);
1798 let tx_a_b5_2 = make_tx(account_a, 2, 3, 5);
1799 let tx_a_b6_1 = make_tx(account_a, 3, 4, 6);
1800 let tx_a_b6_2 = make_tx(account_a, 4, 5, 6);
1801 let tx_b_b6 = make_tx(account_b, 10, 20, 6);
1802
1803 let result = super::derive_account_commitments(&[
1805 tx_a_b6_1, tx_b_b6, tx_a_b5_2, tx_a_b6_2, tx_a_b5_1,
1806 ]);
1807
1808 assert_eq!(result.len(), 2, "one entry per account");
1809 assert!(
1810 result.contains(&(account_a, word(5))),
1811 "account A: must walk block 6's chain, not return block 5 or an intermediate",
1812 );
1813 assert!(
1814 result.contains(&(account_b, word(20))),
1815 "account B: must be resolved independently of account A",
1816 );
1817 }
1818
1819 struct CommitAllScreener;
1825
1826 #[async_trait(?Send)]
1827 impl OnNoteReceived for CommitAllScreener {
1828 async fn on_note_received(
1829 &self,
1830 committed_note: CommittedNote,
1831 _public_note: Option<InputNoteRecord>,
1832 ) -> Result<NoteUpdateAction, ClientError> {
1833 Ok(NoteUpdateAction::Commit(committed_note))
1834 }
1835 }
1836
1837 async fn build_chain_with_chained_consume_txs() -> (miden_testing::MockChain, Account, [Note; 3])
1841 {
1842 let sender_id: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
1843 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap();
1844
1845 let mut builder = MockChainBuilder::new();
1846 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1847 let account_id = account.id();
1848
1849 let asset = Asset::Fungible(FungibleAsset::new(faucet_id, 100u64).unwrap());
1850 let note1 = builder
1851 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
1852 .unwrap();
1853 let note2 = builder
1854 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
1855 .unwrap();
1856 let note3 = builder
1857 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
1858 .unwrap();
1859
1860 let mut chain = builder.build().unwrap();
1861 chain.prove_next_block().unwrap(); let mut current_account = account.clone();
1865 for note in [¬e1, ¬e2, ¬e3] {
1866 let tx = Box::pin(
1867 chain
1868 .build_tx_context(
1869 TxContextInput::Account(current_account.clone()),
1870 &[],
1871 core::slice::from_ref(note),
1872 )
1873 .unwrap()
1874 .build()
1875 .unwrap()
1876 .execute(),
1877 )
1878 .await
1879 .unwrap();
1880 current_account.apply_patch(tx.account_patch()).unwrap();
1881 chain.add_pending_executed_transaction(&tx).unwrap();
1882 }
1883
1884 chain.prove_next_block().unwrap(); (chain, account, [note1, note2, note3])
1886 }
1887
1888 #[tokio::test]
1891 async fn sync_state_sets_consumed_tx_order_for_chained_transactions() {
1892 use miden_protocol::note::NoteMetadata;
1893
1894 let (chain, account, [note1, note2, note3]) = build_chain_with_chained_consume_txs().await;
1895
1896 let mock_rpc = MockRpcApi::new(chain);
1897 let state_sync =
1898 StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(CommitAllScreener), None);
1899
1900 let genesis_peaks =
1901 mock_rpc.get_mmr().peaks_at(Forest::new(1).expect("valid forest")).unwrap();
1902 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
1903
1904 let input_notes: Vec<InputNoteRecord> = [¬e1, ¬e2, ¬e3]
1905 .into_iter()
1906 .map(|n| InputNoteRecord::from(n.clone()))
1907 .collect();
1908
1909 let note_tags: BTreeSet<NoteTag> =
1910 input_notes.iter().filter_map(|n| n.metadata().map(NoteMetadata::tag)).collect();
1911
1912 let account_id = account.id();
1913 let sync_input = StateSyncInput {
1914 accounts: vec![AccountHeader::from(account)],
1915 note_tags,
1916 input_notes,
1917 output_notes: vec![],
1918 uncommitted_transactions: vec![],
1919 };
1920
1921 let update = state_sync.sync_state(&mut partial_mmr, sync_input).await.unwrap();
1922
1923 let updated_notes: Vec<_> = update.note_updates.updated_input_notes().collect();
1924
1925 let find_order = |details_commitment| -> Option<u32> {
1926 updated_notes
1927 .iter()
1928 .find(|n| n.inner().details_commitment() == details_commitment)
1929 .and_then(|n| n.consumed_tx_order())
1930 };
1931
1932 assert_eq!(find_order(note1.details_commitment()), Some(0), "note1 should have tx_order 0");
1933 assert_eq!(find_order(note2.details_commitment()), Some(1), "note2 should have tx_order 1");
1934 assert_eq!(find_order(note3.details_commitment()), Some(2), "note3 should have tx_order 2");
1935
1936 for note in &updated_notes {
1939 let record = note.inner();
1940 assert!(record.is_consumed(), "note should be in a consumed state");
1941 assert_eq!(
1942 record.consumer_account(),
1943 Some(account_id),
1944 "externally-consumed notes by a tracked account should have consumer_account set",
1945 );
1946 }
1947 }
1948
1949 #[tokio::test]
1950 async fn sync_state_across_multiple_iterations_with_same_mmr() {
1951 let mock_rpc = MockRpcApi::default();
1953 mock_rpc.advance_blocks(3);
1954 let chain_tip_1 = mock_rpc.get_chain_tip_block_num();
1955
1956 let state_sync = StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(MockScreener), None);
1957
1958 let genesis_peaks =
1960 mock_rpc.get_mmr().peaks_at(Forest::new(1).expect("valid forest")).unwrap();
1961 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
1962 assert_eq!(partial_mmr.forest().num_leaves(), 1);
1963
1964 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1966
1967 assert_eq!(update.block_num, chain_tip_1);
1968 let forest_1 = partial_mmr.forest();
1969 assert_eq!(forest_1.num_leaves(), chain_tip_1.as_u32() as usize + 1);
1971
1972 mock_rpc.advance_blocks(2);
1974 let chain_tip_2 = mock_rpc.get_chain_tip_block_num();
1975
1976 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1977
1978 assert_eq!(update.block_num, chain_tip_2);
1979 let forest_2 = partial_mmr.forest();
1980 assert!(forest_2 > forest_1);
1981 assert_eq!(forest_2.num_leaves(), chain_tip_2.as_u32() as usize + 1);
1982
1983 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1985
1986 assert_eq!(update.block_num, chain_tip_2);
1987 assert_eq!(partial_mmr.forest(), forest_2);
1988 }
1989
1990 async fn build_chain_with_mint_notes(
1993 num_blocks: u64,
1994 ) -> (miden_testing::MockChain, BTreeSet<NoteTag>) {
1995 let mut builder = MockChainBuilder::new();
1996 let faucet = builder
1997 .add_existing_basic_faucet(
1998 miden_testing::Auth::BasicAuth {
1999 auth_scheme: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2,
2000 },
2001 "TST",
2002 10_000,
2003 None,
2004 )
2005 .unwrap();
2006 let _target = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
2007 let mut chain = builder.build().unwrap();
2008
2009 let note_script = CodeBuilder::new()
2014 .compile_note_script("@note_script\npub proc main\n nop\nend")
2015 .unwrap();
2016 let note_recipient = NoteRecipient::new(
2017 Word::from([1u32, 2, 3, 4]),
2018 note_script,
2019 NoteStorage::new(vec![]).unwrap(),
2020 );
2021 let recipient = note_recipient.digest();
2022 let note_details = NoteDetails::new(NoteAssets::new(vec![]).unwrap(), note_recipient);
2025 let mut recipient_args = TransactionArgs::new(AdviceMap::default());
2026 recipient_args.add_output_note_recipient(¬e_details);
2027 let recipient_advice = recipient_args.advice_inputs().clone();
2028
2029 let tag = NoteTag::default();
2030 let mut faucet_account = faucet.clone();
2031 let mut note_tags = BTreeSet::new();
2032
2033 for i in 0..num_blocks {
2034 let amount = 100 + i;
2035 let source_manager = Arc::new(DefaultSourceManager::default());
2036 let mint_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap();
2040 let asset_id_word = mint_asset.id().to_word();
2041 let asset_value_word = mint_asset.to_value_word();
2042 let tx_script_code = format!(
2043 "
2044 @transaction_script
2045 pub proc main
2046 push.{recipient}
2047 push.{note_type}
2048 push.{tag}
2049 push.{asset_value}
2050 push.{asset_id}
2051 call.::miden::standards::faucets::fungible::mint_and_send
2052 dropw dropw dropw dropw
2053 end
2054 ",
2055 recipient = recipient,
2056 note_type = NoteType::Private as u8,
2057 tag = u32::from(tag),
2058 asset_value = asset_value_word,
2059 asset_id = asset_id_word,
2060 );
2061 let tx_script = CodeBuilder::with_source_manager(source_manager.clone())
2062 .compile_tx_script(tx_script_code)
2063 .unwrap();
2064 let tx = Box::pin(
2065 chain
2066 .build_tx_context(
2067 miden_testing::TxContextInput::Account(faucet_account.clone()),
2068 &[],
2069 &[],
2070 )
2071 .unwrap()
2072 .extend_advice_inputs(recipient_advice.clone())
2073 .tx_script(tx_script)
2074 .with_source_manager(source_manager)
2075 .build()
2076 .unwrap()
2077 .execute(),
2078 )
2079 .await
2080 .unwrap();
2081
2082 for output_note in tx.output_notes().iter() {
2083 note_tags.insert(output_note.metadata().tag());
2084 }
2085
2086 faucet_account.apply_patch(tx.account_patch()).unwrap();
2087 chain.add_pending_executed_transaction(&tx).unwrap();
2088 chain.prove_next_block().unwrap();
2089 }
2090
2091 (chain, note_tags)
2092 }
2093
2094 #[tokio::test]
2104 async fn sync_state_tracks_note_blocks_in_mmr() {
2105 let (chain, note_tags) = build_chain_with_mint_notes(3).await;
2106 let mock_rpc = MockRpcApi::new(chain);
2107 let chain_tip = mock_rpc.get_chain_tip_block_num();
2108
2109 let note_blocks = mock_rpc
2111 .sync_notes(BlockNumber::from(0u32), chain_tip, ¬e_tags)
2112 .await
2113 .unwrap();
2114 assert!(
2115 note_blocks.len() >= 2,
2116 "expected notes in multiple blocks, got {}",
2117 note_blocks.len()
2118 );
2119
2120 let note_block_nums: BTreeSet<BlockNumber> =
2122 note_blocks.iter().map(|b| b.block_header.block_num()).collect();
2123
2124 let state_sync = StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(MockScreener), None);
2127
2128 let genesis_peaks =
2129 mock_rpc.get_mmr().peaks_at(Forest::new(1).expect("valid forest")).unwrap();
2130 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
2131
2132 let sync_data = state_sync
2133 .fetch_sync_data(BlockNumber::GENESIS, &[], &Arc::new(note_tags.clone()))
2134 .await
2135 .unwrap()
2136 .expect("should have progressed past genesis");
2137
2138 assert_eq!(sync_data.chain_tip_header.block_num(), chain_tip);
2140 assert!(!sync_data.note_blocks.is_empty(), "should have note blocks");
2141
2142 let _auth_nodes: Vec<(InOrderIndex, Word)> =
2144 partial_mmr.apply(sync_data.mmr_delta).map_err(StoreError::MmrError).unwrap();
2145 partial_mmr
2146 .add(sync_data.chain_tip_header.commitment(), false)
2147 .expect("chain tip should append to the partial MMR");
2148
2149 assert_eq!(partial_mmr.forest().num_leaves(), chain_tip.as_u32() as usize + 1);
2150
2151 for block in &sync_data.note_blocks {
2153 let bn = block.block_header.block_num();
2154 partial_mmr
2155 .track(bn.as_usize(), block.block_header.commitment(), &block.mmr_path)
2156 .map_err(StoreError::MmrError)
2157 .unwrap();
2158
2159 assert!(
2160 partial_mmr.is_tracked(bn.as_usize()),
2161 "block {bn} should be tracked after calling track()"
2162 );
2163 }
2164
2165 for &bn in ¬e_block_nums {
2167 assert!(
2168 partial_mmr.is_tracked(bn.as_usize()),
2169 "block {bn} with notes should be tracked in partial MMR"
2170 );
2171 }
2172 }
2173
2174 #[tokio::test]
2175 async fn sync_notes_with_details_fetches_inclusive_upper_bound_page() {
2176 let (chain, note_tags) = build_chain_with_mint_notes(10).await;
2177 let mock_rpc = MockRpcApi::new(chain);
2178
2179 let (blocks, _synced_notes) = mock_rpc
2180 .sync_notes_with_details(4_u32.into(), 10_u32.into(), ¬e_tags)
2181 .await
2182 .expect("sync notes should succeed");
2183
2184 assert_eq!(blocks.last().unwrap().block_header.block_num(), BlockNumber::from(10u32));
2185 assert!(
2186 blocks
2187 .iter()
2188 .any(|block| block.block_header.block_num() == BlockNumber::from(9u32))
2189 );
2190 }
2191
2192 #[tokio::test]
2198 async fn erased_notes_are_marked_as_consumed() {
2199 let sender_id: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
2201 let partial_metadata = PartialNoteMetadata::new(sender_id, NoteType::Public);
2202 let metadata = NoteMetadata::new(partial_metadata, &NoteAttachments::empty());
2203 let script = CodeBuilder::new()
2204 .compile_note_script("@note_script\npub proc main\n nop\nend")
2205 .unwrap();
2206 let recipient = NoteRecipient::new(
2207 Word::from([1u32, 2, 3, 4]),
2208 script,
2209 NoteStorage::new(vec![]).unwrap(),
2210 );
2211 let output_note = OutputNoteRecord::new(
2212 recipient.digest(),
2213 NoteAssets::new(vec![]).unwrap(),
2214 metadata,
2215 OutputNoteState::ExpectedFull { recipient },
2216 BlockNumber::from(1u32),
2217 NoteAttachments::default(),
2218 );
2219 let note_id = output_note.id();
2220 let note_header = NoteHeader::new(output_note.details_commitment(), metadata);
2221
2222 let mut note_updates = NoteUpdateTracker::new(vec![], vec![output_note]);
2224
2225 let block_num = BlockNumber::from(3u32);
2227 note_updates
2228 .mark_erased_note_as_consumed(¬e_header, block_num)
2229 .expect("marking erased note should succeed");
2230
2231 let updated = note_updates
2232 .updated_output_notes()
2233 .find(|n| n.id() == note_id)
2234 .expect("output note should be in the update");
2235
2236 assert!(
2237 updated.inner().is_consumed(),
2238 "output note should be consumed after erasure detection, but state is: {}",
2239 updated.inner().state()
2240 );
2241 }
2242
2243 #[allow(clippy::too_many_lines)]
2260 #[ignore = "consumer derivation removed; see comment above"]
2261 #[tokio::test]
2262 async fn erased_notes_are_marked_as_consumed_by_network_account() {
2263 let mut builder = MockChainBuilder::new();
2266 let p2id_sender: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
2267 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap();
2268 let sender_account =
2269 builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
2270 let sender_id = sender_account.id();
2271
2272 let asset = Asset::Fungible(FungibleAsset::new(faucet_id, 100u64).unwrap());
2273 let note = builder
2274 .add_p2id_note(p2id_sender, sender_id, &[asset], NoteType::Public)
2275 .unwrap();
2276
2277 let mut chain = builder.build().unwrap();
2278 chain.prove_next_block().unwrap();
2279
2280 let tx = Box::pin(
2281 chain
2282 .build_tx_context(
2283 TxContextInput::Account(sender_account.clone()),
2284 &[],
2285 core::slice::from_ref(¬e),
2286 )
2287 .unwrap()
2288 .build()
2289 .unwrap()
2290 .execute(),
2291 )
2292 .await
2293 .unwrap();
2294 chain.add_pending_executed_transaction(&tx).unwrap();
2295 chain.prove_next_block().unwrap();
2296
2297 let network_account_id: AccountId =
2299 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE.try_into().unwrap();
2300 let target =
2301 NetworkAccountTarget::new(network_account_id, NoteExecutionHint::Always).unwrap();
2302 let attachment: NoteAttachment = target.into();
2303 let attachments = NoteAttachments::new(vec![attachment]).unwrap();
2304 let partial_metadata = PartialNoteMetadata::new(sender_id, NoteType::Public);
2305 let metadata = NoteMetadata::new(partial_metadata, &attachments);
2306 let script = CodeBuilder::new()
2307 .compile_note_script("@note_script\npub proc main\n nop\nend")
2308 .unwrap();
2309 let recipient = NoteRecipient::new(
2310 Word::from([7u32, 8, 9, 10]),
2311 script,
2312 NoteStorage::new(vec![]).unwrap(),
2313 );
2314 let recipient_digest = recipient.digest();
2315 let assets = NoteAssets::new(vec![]).unwrap();
2316
2317 let output_note = OutputNoteRecord::new(
2320 recipient_digest,
2321 assets.clone(),
2322 metadata,
2323 OutputNoteState::ExpectedFull { recipient },
2324 BlockNumber::from(1u32),
2325 NoteAttachments::default(),
2326 );
2327 let erased_note_id = output_note.id();
2328 let erased_note_header = NoteHeader::new(output_note.details_commitment(), metadata);
2329
2330 let mock_rpc = MockRpcApi::new(chain);
2331 mock_rpc.mark_note_as_erased(erased_note_header);
2332
2333 let network_header =
2336 AccountHeader::new(network_account_id, ZERO, EMPTY_WORD, EMPTY_WORD, EMPTY_WORD);
2337
2338 let state_sync = StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(MockScreener), None);
2339
2340 let genesis_peaks =
2341 mock_rpc.get_mmr().peaks_at(Forest::new(1).expect("valid forest")).unwrap();
2342 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
2343
2344 let sync_input = StateSyncInput {
2345 accounts: vec![AccountHeader::from(sender_account), network_header],
2346 note_tags: BTreeSet::new(),
2347 input_notes: vec![],
2348 output_notes: vec![output_note],
2349 uncommitted_transactions: vec![],
2350 };
2351
2352 let update = state_sync.sync_state(&mut partial_mmr, sync_input).await.unwrap();
2353
2354 let updated_output = update
2356 .note_updates
2357 .updated_output_notes()
2358 .find(|n| n.id() == erased_note_id)
2359 .expect("output note should be in the update");
2360 assert!(
2361 updated_output.inner().is_consumed(),
2362 "output note should be consumed, got: {}",
2363 updated_output.inner().state()
2364 );
2365
2366 let input_note_update = update
2368 .note_updates
2369 .updated_input_notes()
2370 .find(|n| n.id() == Some(erased_note_id))
2371 .expect("input note should be created from the erased output note");
2372
2373 let inner = input_note_update.inner();
2374 assert!(
2375 inner.is_consumed(),
2376 "input note should be in a consumed state, got: {}",
2377 inner.state()
2378 );
2379 assert_eq!(
2380 inner.consumer_account(),
2381 Some(network_account_id),
2382 "consumer should be the tracked network account"
2383 );
2384 }
2385
2386 #[tokio::test]
2389 async fn validate_chain_mmr_response_rejects_tampered_responses() {
2390 let mock_rpc = MockRpcApi::default();
2391 mock_rpc.advance_blocks(3);
2392 let chain_tip = mock_rpc.get_chain_tip_block_num();
2393 let current = BlockNumber::GENESIS;
2394
2395 let header_of =
2396 |block_num: u32| mock_rpc.mock_chain.read().block_header(block_num as usize);
2397 let chain_mmr_response = || async {
2398 mock_rpc.sync_chain_mmr(current, SyncTarget::CommittedChainTip).await.unwrap()
2399 };
2400
2401 let response = chain_mmr_response().await;
2403 StateSync::validate_chain_mmr_response(&response, current).unwrap();
2404
2405 let mut response = chain_mmr_response().await;
2407 response.block_header = header_of(chain_tip.as_u32() - 1);
2408 let result = StateSync::validate_chain_mmr_response(&response, current);
2409 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
2410
2411 let mut response = chain_mmr_response().await;
2413 response.block_from = current + 1;
2414 let result = StateSync::validate_chain_mmr_response(&response, current);
2415 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
2416
2417 let mut response = chain_mmr_response().await;
2419 response.block_from = chain_tip;
2420 response.block_to = BlockNumber::GENESIS;
2421 response.block_header = header_of(0);
2422 let result = StateSync::validate_chain_mmr_response(&response, chain_tip);
2423 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
2424 }
2425
2426 #[test]
2429 fn validate_note_blocks_range_rejects_out_of_range_blocks() {
2430 let mock_rpc = MockRpcApi::default();
2431 mock_rpc.advance_blocks(3);
2432 let chain_tip = mock_rpc.get_chain_tip_block_num();
2433 let current = BlockNumber::GENESIS;
2434
2435 StateSync::validate_note_blocks_range(&[], current, chain_tip).unwrap();
2437
2438 let genesis_note_block = NoteSyncBlock {
2440 block_header: mock_rpc.mock_chain.read().block_header(0),
2441 mmr_path: MerklePath::new(Vec::new()),
2442 notes: BTreeMap::new(),
2443 };
2444 let result =
2445 StateSync::validate_note_blocks_range(&[genesis_note_block], current, chain_tip);
2446 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
2447 }
2448
2449 #[test]
2452 fn advance_mmr_rejects_delta_inconsistent_with_chain_commitment() {
2453 let mock_rpc = MockRpcApi::default();
2454 mock_rpc.advance_blocks(3);
2455 let chain_tip = mock_rpc.get_chain_tip_block_num();
2456
2457 let chain_tip_header = mock_rpc.mock_chain.read().block_header(chain_tip.as_usize());
2458 let genesis_partial_mmr = || {
2459 let peaks = mock_rpc.get_mmr().peaks_at(Forest::new(1).expect("valid forest")).unwrap();
2460 PartialMmr::from_peaks(peaks)
2461 };
2462
2463 let full_delta = mock_rpc
2465 .get_mmr()
2466 .get_delta(Forest::new(1).unwrap(), Forest::new(chain_tip.as_usize()).unwrap())
2467 .unwrap();
2468 StateSync::advance_mmr(
2469 full_delta,
2470 &chain_tip_header,
2471 &mut genesis_partial_mmr(),
2472 &mut PartialBlockchainUpdates::default(),
2473 )
2474 .unwrap();
2475
2476 let truncated_delta = mock_rpc
2478 .get_mmr()
2479 .get_delta(Forest::new(1).unwrap(), Forest::new(chain_tip.as_usize() - 1).unwrap())
2480 .unwrap();
2481 let result = StateSync::advance_mmr(
2482 truncated_delta,
2483 &chain_tip_header,
2484 &mut genesis_partial_mmr(),
2485 &mut PartialBlockchainUpdates::default(),
2486 );
2487 assert!(matches!(result, Err(ClientError::ChainValidationError(_))));
2488 }
2489
2490 fn make_tx_record(account_id: AccountId, block_num: u32) -> RpcTransactionRecord {
2492 RpcTransactionRecord {
2493 block_num: BlockNumber::from(block_num),
2494 transaction_header: TransactionHeader::new(
2495 account_id,
2496 word(1),
2497 word(2),
2498 InputNotes::new_unchecked(vec![]),
2499 vec![],
2500 ),
2501 output_notes: vec![],
2502 erased_output_notes: vec![],
2503 }
2504 }
2505}