1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5
6use async_trait::async_trait;
7use miden_protocol::Word;
8use miden_protocol::account::{Account, AccountHeader, AccountId};
9use miden_protocol::block::{BlockHeader, BlockNumber};
10use miden_protocol::crypto::merkle::mmr::{MmrDelta, PartialMmr};
11use miden_protocol::note::{Note, NoteId, NoteTag, NoteType, Nullifier};
12use miden_protocol::transaction::InputNoteCommitment;
13use tracing::info;
14
15use super::state_sync_update::TransactionUpdateTracker;
16use super::{AccountUpdates, StateSyncUpdate};
17use crate::ClientError;
18use crate::note::NoteUpdateTracker;
19use crate::rpc::NodeRpcClient;
20use crate::rpc::domain::note::{CommittedNote, NoteSyncBlock};
21use crate::rpc::domain::transaction::{
22 TransactionInclusion,
23 TransactionRecord as RpcTransactionRecord,
24};
25use crate::store::{InputNoteRecord, OutputNoteRecord, StoreError};
26use crate::transaction::TransactionRecord;
27
28struct RawStateSyncData {
37 mmr_delta: MmrDelta,
39 chain_tip_header: BlockHeader,
41 note_blocks: Vec<NoteSyncBlock>,
43 public_notes: BTreeMap<NoteId, Note>,
45 account_commitment_updates: Vec<(AccountId, Word)>,
47 transactions: Vec<TransactionInclusion>,
49 nullifiers: Vec<Nullifier>,
51}
52
53pub struct StateSyncInput {
69 pub accounts: Vec<AccountHeader>,
71 pub note_tags: BTreeSet<NoteTag>,
73 pub input_notes: Vec<InputNoteRecord>,
75 pub output_notes: Vec<OutputNoteRecord>,
77 pub uncommitted_transactions: Vec<TransactionRecord>,
79}
80
81#[allow(clippy::large_enum_variant)]
86pub enum NoteUpdateAction {
87 Commit(CommittedNote),
90 Insert(InputNoteRecord),
92 Discard,
94}
95
96#[async_trait(?Send)]
97pub trait OnNoteReceived {
98 async fn on_note_received(
109 &self,
110 committed_note: CommittedNote,
111 public_note: Option<InputNoteRecord>,
112 ) -> Result<NoteUpdateAction, ClientError>;
113}
114#[derive(Clone)]
121pub struct StateSync {
122 rpc_api: Arc<dyn NodeRpcClient>,
124 note_screener: Arc<dyn OnNoteReceived>,
127 tx_discard_delta: Option<u32>,
130 sync_nullifiers: bool,
134}
135
136impl StateSync {
137 pub fn new(
148 rpc_api: Arc<dyn NodeRpcClient>,
149 note_screener: Arc<dyn OnNoteReceived>,
150 tx_discard_delta: Option<u32>,
151 ) -> Self {
152 Self {
153 rpc_api,
154 note_screener,
155 tx_discard_delta,
156 sync_nullifiers: true,
157 }
158 }
159
160 pub fn disable_nullifier_sync(&mut self) {
166 self.sync_nullifiers = false;
167 }
168
169 pub fn enable_nullifier_sync(&mut self) {
171 self.sync_nullifiers = true;
172 }
173
174 pub async fn sync_state(
197 &self,
198 current_partial_mmr: &mut PartialMmr,
199 input: StateSyncInput,
200 ) -> Result<StateSyncUpdate, ClientError> {
201 let StateSyncInput {
202 accounts,
203 note_tags,
204 input_notes,
205 output_notes,
206 uncommitted_transactions,
207 } = input;
208 let block_num = u32::try_from(current_partial_mmr.forest().num_leaves().saturating_sub(1))
209 .map_err(|_| ClientError::InvalidPartialMmrForest)?
210 .into();
211
212 let mut state_sync_update = StateSyncUpdate {
213 block_num,
214 note_updates: NoteUpdateTracker::new(input_notes, output_notes),
215 transaction_updates: TransactionUpdateTracker::new(uncommitted_transactions),
216 ..Default::default()
217 };
218
219 let note_tags = Arc::new(note_tags);
220 let account_ids: Vec<AccountId> = accounts.iter().map(AccountHeader::id).collect();
221 let Some(mut sync_data) = self
222 .fetch_sync_data(state_sync_update.block_num, &account_ids, ¬e_tags)
223 .await?
224 else {
225 return Ok(state_sync_update);
227 };
228
229 state_sync_update.block_num = sync_data.chain_tip_header.block_num();
230
231 let mut public_note_records: BTreeMap<NoteId, InputNoteRecord> = BTreeMap::new();
234 for (note_id, note) in core::mem::take(&mut sync_data.public_notes) {
235 let inclusion_proof = sync_data
236 .note_blocks
237 .iter()
238 .find_map(|b| b.notes.get(¬e_id))
239 .map(|committed| committed.inclusion_proof().clone());
240
241 if let Some(inclusion_proof) = inclusion_proof {
242 let state = crate::store::input_note_states::UnverifiedNoteState {
243 metadata: note.metadata().clone(),
244 inclusion_proof,
245 }
246 .into();
247 let record = InputNoteRecord::new(note.into(), None, state);
248 public_note_records.insert(record.id(), record);
249 }
250 }
251
252 self.account_state_sync(
253 &mut state_sync_update.account_updates,
254 &accounts,
255 &sync_data.account_commitment_updates,
256 )
257 .await?;
258
259 self.apply_sync_result(
261 sync_data,
262 &public_note_records,
263 &mut state_sync_update,
264 current_partial_mmr,
265 )
266 .await?;
267
268 if self.sync_nullifiers {
269 self.nullifiers_state_sync(&mut state_sync_update, block_num).await?;
270 }
271
272 Ok(state_sync_update)
273 }
274
275 async fn fetch_sync_data(
284 &self,
285 current_block_num: BlockNumber,
286 account_ids: &[AccountId],
287 note_tags: &Arc<BTreeSet<NoteTag>>,
288 ) -> Result<Option<RawStateSyncData>, ClientError> {
289 let chain_mmr_info = self.rpc_api.sync_chain_mmr(current_block_num, None).await?;
291 let chain_tip = chain_mmr_info.block_to;
292
293 if chain_tip == current_block_num {
295 info!(block_num = %current_block_num, "Already at chain tip, nothing to sync.");
296 return Ok(None);
297 }
298
299 info!(
300 block_from = %current_block_num,
301 block_to = %chain_tip,
302 "Syncing state.",
303 );
304
305 let sync_notes_result = self
308 .rpc_api
309 .sync_notes_with_details(current_block_num, Some(chain_tip), note_tags.as_ref())
310 .await?;
311
312 let note_count: usize = sync_notes_result.blocks.iter().map(|b| b.notes.len()).sum();
313 info!(
314 blocks_with_notes = sync_notes_result.blocks.len(),
315 notes = note_count,
316 public_notes = sync_notes_result.public_notes.len(),
317 "Fetched note sync data.",
318 );
319
320 let (account_commitment_updates, transactions, nullifiers) =
322 self.fetch_transaction_data(current_block_num, chain_tip, account_ids).await?;
323
324 Ok(Some(RawStateSyncData {
325 mmr_delta: chain_mmr_info.mmr_delta,
326 chain_tip_header: chain_mmr_info.block_header,
327 note_blocks: sync_notes_result.blocks,
328 public_notes: sync_notes_result.public_notes,
329 account_commitment_updates,
330 transactions,
331 nullifiers,
332 }))
333 }
334
335 async fn fetch_transaction_data(
337 &self,
338 block_from: BlockNumber,
339 block_to: BlockNumber,
340 account_ids: &[AccountId],
341 ) -> Result<(Vec<(AccountId, Word)>, Vec<TransactionInclusion>, Vec<Nullifier>), ClientError>
342 {
343 if account_ids.is_empty() {
344 return Ok((vec![], vec![], vec![]));
345 }
346
347 let tx_info = self
348 .rpc_api
349 .sync_transactions(block_from, Some(block_to), account_ids.to_vec())
350 .await?;
351
352 let transaction_records = tx_info.transaction_records;
353
354 let account_updates = derive_account_commitment_updates(&transaction_records);
355 let nullifiers = compute_ordered_nullifiers(&transaction_records);
356
357 let tx_inclusions = transaction_records
358 .into_iter()
359 .map(|r| {
360 let nullifiers = r
361 .transaction_header
362 .input_notes()
363 .iter()
364 .map(InputNoteCommitment::nullifier)
365 .collect();
366 TransactionInclusion {
367 transaction_id: r.transaction_header.id(),
368 block_num: r.block_num,
369 account_id: r.transaction_header.account_id(),
370 initial_state_commitment: r.transaction_header.initial_state_commitment(),
371 nullifiers,
372 output_notes: r.output_notes,
373 }
374 })
375 .collect();
376
377 Ok((account_updates, tx_inclusions, nullifiers))
378 }
379
380 async fn apply_sync_result(
390 &self,
391 sync_data: RawStateSyncData,
392 public_note_records: &BTreeMap<NoteId, InputNoteRecord>,
393 state_sync_update: &mut StateSyncUpdate,
394 current_partial_mmr: &mut PartialMmr,
395 ) -> Result<(), ClientError> {
396 let RawStateSyncData {
397 mmr_delta,
398 chain_tip_header,
399 note_blocks,
400 nullifiers,
401 transactions,
402 ..
403 } = sync_data;
404
405 let mut new_authentication_nodes =
409 current_partial_mmr.apply(mmr_delta).map_err(StoreError::MmrError)?;
410 let new_peaks = current_partial_mmr.peaks();
411 new_authentication_nodes
412 .append(&mut current_partial_mmr.add(chain_tip_header.commitment(), false));
413
414 state_sync_update.block_updates.insert(
415 chain_tip_header.clone(),
416 false,
417 new_peaks,
418 new_authentication_nodes,
419 );
420
421 for block in note_blocks {
424 let found_relevant_note = self
425 .note_state_sync(
426 &mut state_sync_update.note_updates,
427 block.notes,
428 &block.block_header,
429 public_note_records,
430 )
431 .await?;
432
433 if found_relevant_note {
434 let block_pos = block.block_header.block_num().as_usize();
435
436 let track_auth_nodes = if current_partial_mmr.is_tracked(block_pos) {
437 vec![]
438 } else {
439 let nodes_before: BTreeMap<_, _> =
440 current_partial_mmr.nodes().map(|(k, v)| (*k, *v)).collect();
441 current_partial_mmr
442 .track(block_pos, block.block_header.commitment(), &block.mmr_path)
443 .map_err(StoreError::MmrError)?;
444 current_partial_mmr
445 .nodes()
446 .filter(|(k, _)| !nodes_before.contains_key(k))
447 .map(|(k, v)| (*k, *v))
448 .collect()
449 };
450
451 state_sync_update.block_updates.insert(
452 block.block_header,
453 true,
454 current_partial_mmr.peaks(),
455 track_auth_nodes,
456 );
457 }
458 }
459
460 state_sync_update.note_updates.extend_nullifiers(nullifiers);
462 self.transaction_state_sync(
463 &mut state_sync_update.transaction_updates,
464 &chain_tip_header,
465 &transactions,
466 );
467
468 for transaction in &transactions {
472 state_sync_update
473 .note_updates
474 .apply_output_note_inclusion_proofs(&transaction.output_notes)?;
475 }
476
477 Ok(())
478 }
479
480 async fn account_state_sync(
491 &self,
492 account_updates: &mut AccountUpdates,
493 accounts: &[AccountHeader],
494 account_commitment_updates: &[(AccountId, Word)],
495 ) -> Result<(), ClientError> {
496 let (public_accounts, private_accounts): (Vec<_>, Vec<_>) =
497 accounts.iter().partition(|account_header| !account_header.id().is_private());
498
499 let updated_public_accounts = self
500 .get_updated_public_accounts(account_commitment_updates, &public_accounts)
501 .await?;
502
503 let mismatched_private_accounts = account_commitment_updates
504 .iter()
505 .filter(|(account_id, digest)| {
506 private_accounts.iter().any(|account| {
507 account.id() == *account_id && &account.to_commitment() != digest
508 })
509 })
510 .copied()
511 .collect::<Vec<_>>();
512
513 account_updates
514 .extend(AccountUpdates::new(updated_public_accounts, mismatched_private_accounts));
515
516 Ok(())
517 }
518
519 async fn get_updated_public_accounts(
522 &self,
523 account_updates: &[(AccountId, Word)],
524 current_public_accounts: &[&AccountHeader],
525 ) -> Result<Vec<Account>, ClientError> {
526 let mut mismatched_public_accounts = vec![];
527
528 for (id, commitment) in account_updates {
529 if let Some(account) = current_public_accounts
531 .iter()
532 .find(|acc| *id == acc.id() && *commitment != acc.to_commitment())
533 {
534 mismatched_public_accounts.push(*account);
535 }
536 }
537
538 self.rpc_api
539 .get_updated_public_accounts(&mismatched_public_accounts)
540 .await
541 .map_err(ClientError::RpcError)
542 }
543
544 async fn note_state_sync(
559 &self,
560 note_updates: &mut NoteUpdateTracker,
561 note_inclusions: BTreeMap<NoteId, CommittedNote>,
562 block_header: &BlockHeader,
563 public_notes: &BTreeMap<NoteId, InputNoteRecord>,
564 ) -> Result<bool, ClientError> {
565 let mut found_relevant_note = false;
567
568 for (_, committed_note) in note_inclusions {
569 let public_note = (committed_note.note_type() != NoteType::Private)
570 .then(|| public_notes.get(committed_note.note_id()))
571 .flatten()
572 .cloned();
573
574 match self.note_screener.on_note_received(committed_note, public_note).await? {
575 NoteUpdateAction::Commit(committed_note) => {
576 found_relevant_note |= note_updates
580 .apply_committed_note_state_transitions(&committed_note, block_header)?;
581 },
582 NoteUpdateAction::Insert(public_note) => {
583 found_relevant_note = true;
584
585 note_updates.apply_new_public_note(public_note, block_header)?;
586 },
587 NoteUpdateAction::Discard => {},
588 }
589 }
590
591 Ok(found_relevant_note)
592 }
593
594 async fn nullifiers_state_sync(
600 &self,
601 state_sync_update: &mut StateSyncUpdate,
602 current_block_num: BlockNumber,
603 ) -> Result<(), ClientError> {
604 let nullifiers_tags: Vec<u16> = state_sync_update
611 .note_updates
612 .unspent_nullifiers()
613 .map(|nullifier| nullifier.prefix())
614 .collect();
615
616 let mut new_nullifiers = self
617 .rpc_api
618 .sync_nullifiers(&nullifiers_tags, current_block_num, Some(state_sync_update.block_num))
619 .await?;
620
621 new_nullifiers.retain(|update| update.block_num <= state_sync_update.block_num);
624
625 for nullifier_update in new_nullifiers {
626 let external_consumer_account = state_sync_update
627 .transaction_updates
628 .external_nullifier_account(&nullifier_update.nullifier);
629
630 state_sync_update.note_updates.apply_nullifiers_state_transitions(
631 &nullifier_update,
632 state_sync_update.transaction_updates.committed_transactions(),
633 external_consumer_account,
634 )?;
635
636 state_sync_update
640 .transaction_updates
641 .apply_input_note_nullified(nullifier_update.nullifier);
642 }
643
644 Ok(())
645 }
646
647 fn transaction_state_sync(
654 &self,
655 transaction_updates: &mut TransactionUpdateTracker,
656 new_block_header: &BlockHeader,
657 transaction_inclusions: &[TransactionInclusion],
658 ) {
659 for transaction_inclusion in transaction_inclusions {
660 transaction_updates.apply_transaction_inclusion(
661 transaction_inclusion,
662 u64::from(new_block_header.timestamp()),
663 ); }
665
666 transaction_updates
667 .apply_sync_height_update(new_block_header.block_num(), self.tx_discard_delta);
668 }
669}
670
671fn derive_account_commitment_updates(
680 transaction_records: &[RpcTransactionRecord],
681) -> Vec<(AccountId, Word)> {
682 let mut latest_by_account: BTreeMap<AccountId, &RpcTransactionRecord> = BTreeMap::new();
683
684 for record in transaction_records {
685 let account_id = record.transaction_header.account_id();
686 latest_by_account
687 .entry(account_id)
688 .and_modify(|existing| {
689 if record.block_num > existing.block_num {
690 *existing = record;
691 }
692 })
693 .or_insert(record);
694 }
695
696 latest_by_account
697 .into_iter()
698 .map(|(account_id, record)| {
699 (account_id, record.transaction_header.final_state_commitment())
700 })
701 .collect()
702}
703
704fn compute_ordered_nullifiers(transaction_records: &[RpcTransactionRecord]) -> Vec<Nullifier> {
711 let mut groups: BTreeMap<(AccountId, BlockNumber), Vec<&RpcTransactionRecord>> =
713 BTreeMap::new();
714
715 for record in transaction_records {
716 let account_id = record.transaction_header.account_id();
717 groups.entry((account_id, record.block_num)).or_default().push(record);
718 }
719
720 let mut result = Vec::new();
721
722 for txs in groups.values() {
723 let mut init_to_tx: BTreeMap<Word, &RpcTransactionRecord> = txs
725 .iter()
726 .map(|tx| (tx.transaction_header.initial_state_commitment(), *tx))
727 .collect();
728
729 let final_states: BTreeSet<Word> =
731 txs.iter().map(|tx| tx.transaction_header.final_state_commitment()).collect();
732
733 let chain_start = txs
736 .iter()
737 .find(|tx| !final_states.contains(&tx.transaction_header.initial_state_commitment()));
738
739 let Some(start_tx) = chain_start else {
740 continue;
741 };
742
743 let mut current =
745 init_to_tx.remove(&start_tx.transaction_header.initial_state_commitment());
746
747 while let Some(tx) = current {
748 for commitment in tx.transaction_header.input_notes().iter() {
749 result.push(commitment.nullifier());
750 }
751 current = init_to_tx.remove(&tx.transaction_header.final_state_commitment());
752 }
753 }
754
755 result
756}
757
758#[cfg(test)]
759mod tests {
760 use alloc::collections::BTreeSet;
761 use alloc::sync::Arc;
762
763 use async_trait::async_trait;
764 use miden_protocol::assembly::DefaultSourceManager;
765 use miden_protocol::crypto::merkle::mmr::{Forest, InOrderIndex, PartialMmr};
766 use miden_protocol::note::{NoteTag, NoteType};
767 use miden_protocol::{Felt, Word};
768 use miden_standards::code_builder::CodeBuilder;
769 use miden_testing::MockChainBuilder;
770
771 use super::*;
772 use crate::testing::mock::MockRpcApi;
773
774 struct MockScreener;
776
777 #[async_trait(?Send)]
778 impl OnNoteReceived for MockScreener {
779 async fn on_note_received(
780 &self,
781 _committed_note: CommittedNote,
782 _public_note: Option<InputNoteRecord>,
783 ) -> Result<NoteUpdateAction, ClientError> {
784 Ok(NoteUpdateAction::Discard)
785 }
786 }
787
788 fn empty() -> StateSyncInput {
789 StateSyncInput {
790 accounts: vec![],
791 note_tags: BTreeSet::new(),
792 input_notes: vec![],
793 output_notes: vec![],
794 uncommitted_transactions: vec![],
795 }
796 }
797
798 mod compute_nullifiers_tests {
802 use alloc::vec;
803
804 use miden_protocol::asset::FungibleAsset;
805 use miden_protocol::block::BlockNumber;
806 use miden_protocol::note::Nullifier;
807 use miden_protocol::transaction::{InputNoteCommitment, InputNotes, TransactionHeader};
808 use miden_protocol::{Felt, ZERO};
809
810 use crate::rpc::domain::transaction::{
811 ACCOUNT_ID_NATIVE_ASSET_FAUCET,
812 TransactionRecord as RpcTransactionRecord,
813 };
814
815 fn word(n: u64) -> miden_protocol::Word {
816 [Felt::new(n), ZERO, ZERO, ZERO].into()
817 }
818
819 fn make_rpc_tx(
820 init_state: u64,
821 final_state: u64,
822 nullifier_vals: &[u64],
823 block_number: u32,
824 ) -> RpcTransactionRecord {
825 let account_id = miden_protocol::account::AccountId::try_from(
826 miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
827 )
828 .unwrap();
829
830 let input_notes = InputNotes::new_unchecked(
831 nullifier_vals
832 .iter()
833 .map(|v| InputNoteCommitment::from(Nullifier::from_raw(word(*v))))
834 .collect(),
835 );
836
837 let fee =
838 FungibleAsset::new(ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("valid"), 0u64)
839 .unwrap();
840
841 RpcTransactionRecord {
842 block_num: BlockNumber::from(block_number),
843 transaction_header: TransactionHeader::new(
844 account_id,
845 word(init_state),
846 word(final_state),
847 input_notes,
848 vec![],
849 fee,
850 ),
851 output_notes: vec![],
852 }
853 }
854
855 #[test]
856 fn chains_rpc_transactions_by_state_commitment() {
857 let tx_a = make_rpc_tx(1, 2, &[10], 5);
860 let tx_b = make_rpc_tx(2, 3, &[20], 5);
861 let tx_c = make_rpc_tx(3, 4, &[30], 5);
862
863 let result = super::super::compute_ordered_nullifiers(&[tx_c, tx_a, tx_b]);
864
865 assert_eq!(result[0], Nullifier::from_raw(word(10)));
866 assert_eq!(result[1], Nullifier::from_raw(word(20)));
867 assert_eq!(result[2], Nullifier::from_raw(word(30)));
868 }
869
870 #[test]
871 fn groups_independently_by_account_and_block() {
872 let tx_a1 = make_rpc_tx(1, 2, &[10], 5);
874 let tx_a2 = make_rpc_tx(2, 3, &[20], 5);
875
876 let tx_a3 = make_rpc_tx(3, 4, &[30], 6);
878
879 let account_b = miden_protocol::account::AccountId::try_from(
881 miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
882 )
883 .unwrap();
884
885 let fee =
886 FungibleAsset::new(ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("valid"), 0u64)
887 .unwrap();
888
889 let tx_b1 = RpcTransactionRecord {
890 block_num: BlockNumber::from(5u32),
891 transaction_header: TransactionHeader::new(
892 account_b,
893 word(100),
894 word(200),
895 InputNotes::new_unchecked(vec![InputNoteCommitment::from(
896 Nullifier::from_raw(word(40)),
897 )]),
898 vec![],
899 fee,
900 ),
901 output_notes: vec![],
902 };
903
904 let result = super::super::compute_ordered_nullifiers(&[tx_a2, tx_b1, tx_a3, tx_a1]);
905
906 let pos = |val: u64| -> usize {
909 result.iter().position(|n| *n == Nullifier::from_raw(word(val))).unwrap()
910 };
911
912 assert!(pos(10) < pos(20)); assert!(result.contains(&Nullifier::from_raw(word(30)))); assert!(result.contains(&Nullifier::from_raw(word(40)))); }
918
919 #[test]
920 fn multiple_nullifiers_per_transaction_are_consecutive() {
921 let tx = make_rpc_tx(1, 2, &[10, 20, 30], 5);
923
924 let result = super::super::compute_ordered_nullifiers(&[tx]);
925
926 assert_eq!(result.len(), 3);
927 assert!(result.contains(&Nullifier::from_raw(word(10))));
928 assert!(result.contains(&Nullifier::from_raw(word(20))));
929 assert!(result.contains(&Nullifier::from_raw(word(30))));
930 }
931
932 #[test]
933 fn empty_input_returns_empty_vec() {
934 let result = super::super::compute_ordered_nullifiers(&[]);
935 assert!(result.is_empty());
936 }
937 }
938
939 struct CommitAllScreener;
945
946 #[async_trait(?Send)]
947 impl OnNoteReceived for CommitAllScreener {
948 async fn on_note_received(
949 &self,
950 committed_note: CommittedNote,
951 _public_note: Option<InputNoteRecord>,
952 ) -> Result<NoteUpdateAction, ClientError> {
953 Ok(NoteUpdateAction::Commit(committed_note))
954 }
955 }
956
957 use miden_protocol::account::Account;
958 use miden_protocol::note::Note;
959
960 async fn build_chain_with_chained_consume_txs() -> (miden_testing::MockChain, Account, [Note; 3])
964 {
965 use miden_protocol::asset::{Asset, FungibleAsset};
966 use miden_protocol::note::NoteType;
967 use miden_protocol::testing::account_id::{
968 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
969 ACCOUNT_ID_SENDER,
970 };
971 use miden_testing::{MockChainBuilder, TxContextInput};
972
973 let sender_id: AccountId = ACCOUNT_ID_SENDER.try_into().unwrap();
974 let faucet_id: AccountId = ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap();
975
976 let mut builder = MockChainBuilder::new();
977 let account = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
978 let account_id = account.id();
979
980 let asset = Asset::Fungible(FungibleAsset::new(faucet_id, 100u64).unwrap());
981 let note1 = builder
982 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
983 .unwrap();
984 let note2 = builder
985 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
986 .unwrap();
987 let note3 = builder
988 .add_p2id_note(sender_id, account_id, &[asset], NoteType::Public)
989 .unwrap();
990
991 let mut chain = builder.build().unwrap();
992 chain.prove_next_block().unwrap(); let mut current_account = account.clone();
996 for note in [¬e1, ¬e2, ¬e3] {
997 let tx = Box::pin(
998 chain
999 .build_tx_context(
1000 TxContextInput::Account(current_account.clone()),
1001 &[],
1002 core::slice::from_ref(note),
1003 )
1004 .unwrap()
1005 .build()
1006 .unwrap()
1007 .execute(),
1008 )
1009 .await
1010 .unwrap();
1011 current_account.apply_delta(tx.account_delta()).unwrap();
1012 chain.add_pending_executed_transaction(&tx).unwrap();
1013 }
1014
1015 chain.prove_next_block().unwrap(); (chain, account, [note1, note2, note3])
1017 }
1018
1019 #[tokio::test]
1022 async fn sync_state_sets_consumed_tx_order_for_chained_transactions() {
1023 use miden_protocol::note::NoteMetadata;
1024
1025 let (chain, account, [note1, note2, note3]) = build_chain_with_chained_consume_txs().await;
1026
1027 let mock_rpc = MockRpcApi::new(chain);
1028 let state_sync =
1029 StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(CommitAllScreener), None);
1030
1031 let genesis_peaks = mock_rpc.get_mmr().peaks_at(Forest::new(1)).unwrap();
1032 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
1033
1034 let input_notes: Vec<InputNoteRecord> = [¬e1, ¬e2, ¬e3]
1035 .into_iter()
1036 .map(|n| InputNoteRecord::from(n.clone()))
1037 .collect();
1038
1039 let note_tags: BTreeSet<NoteTag> =
1040 input_notes.iter().filter_map(|n| n.metadata().map(NoteMetadata::tag)).collect();
1041
1042 let account_id = account.id();
1043 let sync_input = StateSyncInput {
1044 accounts: vec![account.into()],
1045 note_tags,
1046 input_notes,
1047 output_notes: vec![],
1048 uncommitted_transactions: vec![],
1049 };
1050
1051 let update = state_sync.sync_state(&mut partial_mmr, sync_input).await.unwrap();
1052
1053 let updated_notes: Vec<_> = update.note_updates.updated_input_notes().collect();
1054
1055 let find_order = |note_id: NoteId| -> Option<u32> {
1056 updated_notes
1057 .iter()
1058 .find(|n| n.id() == note_id)
1059 .and_then(|n| n.consumed_tx_order())
1060 };
1061
1062 assert_eq!(find_order(note1.id()), Some(0), "note1 should have tx_order 0");
1063 assert_eq!(find_order(note2.id()), Some(1), "note2 should have tx_order 1");
1064 assert_eq!(find_order(note3.id()), Some(2), "note3 should have tx_order 2");
1065
1066 for note in &updated_notes {
1069 let record = note.inner();
1070 assert!(record.is_consumed(), "note should be in a consumed state");
1071 assert_eq!(
1072 record.consumer_account(),
1073 Some(account_id),
1074 "externally-consumed notes by a tracked account should have consumer_account set",
1075 );
1076 }
1077 }
1078
1079 #[tokio::test]
1080 async fn sync_state_across_multiple_iterations_with_same_mmr() {
1081 let mock_rpc = MockRpcApi::default();
1083 mock_rpc.advance_blocks(3);
1084 let chain_tip_1 = mock_rpc.get_chain_tip_block_num();
1085
1086 let state_sync = StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(MockScreener), None);
1087
1088 let genesis_peaks = mock_rpc.get_mmr().peaks_at(Forest::new(1)).unwrap();
1090 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
1091 assert_eq!(partial_mmr.forest().num_leaves(), 1);
1092
1093 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1095
1096 assert_eq!(update.block_num, chain_tip_1);
1097 let forest_1 = partial_mmr.forest();
1098 assert_eq!(forest_1.num_leaves(), chain_tip_1.as_u32() as usize + 1);
1100
1101 mock_rpc.advance_blocks(2);
1103 let chain_tip_2 = mock_rpc.get_chain_tip_block_num();
1104
1105 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1106
1107 assert_eq!(update.block_num, chain_tip_2);
1108 let forest_2 = partial_mmr.forest();
1109 assert!(forest_2 > forest_1);
1110 assert_eq!(forest_2.num_leaves(), chain_tip_2.as_u32() as usize + 1);
1111
1112 let update = state_sync.sync_state(&mut partial_mmr, empty()).await.unwrap();
1114
1115 assert_eq!(update.block_num, chain_tip_2);
1116 assert_eq!(partial_mmr.forest(), forest_2);
1117 }
1118
1119 async fn build_chain_with_mint_notes(
1122 num_blocks: u64,
1123 ) -> (miden_testing::MockChain, BTreeSet<NoteTag>) {
1124 let mut builder = MockChainBuilder::new();
1125 let faucet = builder
1126 .add_existing_basic_faucet(
1127 miden_testing::Auth::BasicAuth {
1128 auth_scheme: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2,
1129 },
1130 "TST",
1131 10_000,
1132 None,
1133 )
1134 .unwrap();
1135 let _target = builder.add_existing_mock_account(miden_testing::Auth::IncrNonce).unwrap();
1136 let mut chain = builder.build().unwrap();
1137
1138 let recipient: Word = [0u32, 1, 2, 3].into();
1139 let tag = NoteTag::default();
1140 let mut faucet_account = faucet.clone();
1141 let mut note_tags = BTreeSet::new();
1142
1143 for i in 0..num_blocks {
1144 let amount = Felt::new(100 + i);
1145 let source_manager = Arc::new(DefaultSourceManager::default());
1146 let tx_script_code = format!(
1147 "
1148 begin
1149 padw padw push.0
1150 push.{r0}.{r1}.{r2}.{r3}
1151 push.{note_type}
1152 push.{tag}
1153 push.{amount}
1154 call.::miden::standards::faucets::basic_fungible::mint_and_send
1155 dropw dropw dropw dropw
1156 end
1157 ",
1158 r0 = recipient[0],
1159 r1 = recipient[1],
1160 r2 = recipient[2],
1161 r3 = recipient[3],
1162 note_type = NoteType::Private as u8,
1163 tag = u32::from(tag),
1164 amount = amount,
1165 );
1166 let tx_script = CodeBuilder::with_source_manager(source_manager.clone())
1167 .compile_tx_script(tx_script_code)
1168 .unwrap();
1169 let tx = Box::pin(
1170 chain
1171 .build_tx_context(
1172 miden_testing::TxContextInput::Account(faucet_account.clone()),
1173 &[],
1174 &[],
1175 )
1176 .unwrap()
1177 .tx_script(tx_script)
1178 .with_source_manager(source_manager)
1179 .build()
1180 .unwrap()
1181 .execute(),
1182 )
1183 .await
1184 .unwrap();
1185
1186 for output_note in tx.output_notes().iter() {
1187 note_tags.insert(output_note.metadata().tag());
1188 }
1189
1190 faucet_account.apply_delta(tx.account_delta()).unwrap();
1191 chain.add_pending_executed_transaction(&tx).unwrap();
1192 chain.prove_next_block().unwrap();
1193 }
1194
1195 (chain, note_tags)
1196 }
1197
1198 #[tokio::test]
1208 async fn sync_state_tracks_note_blocks_in_mmr() {
1209 let (chain, note_tags) = build_chain_with_mint_notes(3).await;
1210 let mock_rpc = MockRpcApi::new(chain);
1211 let chain_tip = mock_rpc.get_chain_tip_block_num();
1212
1213 let note_sync =
1215 mock_rpc.sync_notes(BlockNumber::from(0u32), None, ¬e_tags).await.unwrap();
1216 assert!(
1217 note_sync.blocks.len() >= 2,
1218 "expected notes in multiple blocks, got {}",
1219 note_sync.blocks.len()
1220 );
1221
1222 let note_block_nums: BTreeSet<BlockNumber> =
1224 note_sync.blocks.iter().map(|b| b.block_header.block_num()).collect();
1225
1226 let state_sync = StateSync::new(Arc::new(mock_rpc.clone()), Arc::new(MockScreener), None);
1229
1230 let genesis_peaks = mock_rpc.get_mmr().peaks_at(Forest::new(1)).unwrap();
1231 let mut partial_mmr = PartialMmr::from_peaks(genesis_peaks);
1232
1233 let sync_data = state_sync
1234 .fetch_sync_data(BlockNumber::GENESIS, &[], &Arc::new(note_tags.clone()))
1235 .await
1236 .unwrap()
1237 .expect("should have progressed past genesis");
1238
1239 assert_eq!(sync_data.chain_tip_header.block_num(), chain_tip);
1241 assert!(!sync_data.note_blocks.is_empty(), "should have note blocks");
1242
1243 let _auth_nodes: Vec<(InOrderIndex, Word)> =
1245 partial_mmr.apply(sync_data.mmr_delta).map_err(StoreError::MmrError).unwrap();
1246 partial_mmr.add(sync_data.chain_tip_header.commitment(), false);
1247
1248 assert_eq!(partial_mmr.forest().num_leaves(), chain_tip.as_u32() as usize + 1);
1249
1250 for block in &sync_data.note_blocks {
1252 let bn = block.block_header.block_num();
1253 partial_mmr
1254 .track(bn.as_usize(), block.block_header.commitment(), &block.mmr_path)
1255 .map_err(StoreError::MmrError)
1256 .unwrap();
1257
1258 assert!(
1259 partial_mmr.is_tracked(bn.as_usize()),
1260 "block {bn} should be tracked after calling track()"
1261 );
1262 }
1263
1264 for &bn in ¬e_block_nums {
1266 assert!(
1267 partial_mmr.is_tracked(bn.as_usize()),
1268 "block {bn} with notes should be tracked in partial MMR"
1269 );
1270 }
1271 }
1272}