1use alloc::boxed::Box;
2use alloc::collections::{BTreeMap, BTreeSet};
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use anyhow::Context;
7use miden_block_prover::{LocalBlockProver, ProvenBlockError};
8use miden_lib::note::{create_p2id_note, create_p2ide_note};
9use miden_objects::account::delta::AccountUpdateDetails;
10use miden_objects::account::{Account, AccountId, AuthSecretKey, StorageSlot};
11use miden_objects::asset::Asset;
12use miden_objects::batch::{ProposedBatch, ProvenBatch};
13use miden_objects::block::{
14 AccountTree,
15 AccountWitness,
16 BlockHeader,
17 BlockInputs,
18 BlockNumber,
19 Blockchain,
20 NullifierTree,
21 NullifierWitness,
22 ProposedBlock,
23 ProvenBlock,
24};
25use miden_objects::crypto::merkle::SmtProof;
26use miden_objects::note::{Note, NoteHeader, NoteId, NoteInclusionProof, NoteType, Nullifier};
27use miden_objects::transaction::{
28 AccountInputs,
29 ExecutedTransaction,
30 InputNote,
31 InputNotes,
32 OrderedTransactionHeaders,
33 OutputNote,
34 PartialBlockchain,
35 ProvenTransaction,
36 TransactionHeader,
37 TransactionInputs,
38};
39use miden_objects::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, NoteError};
40use miden_processor::crypto::RpoRandomCoin;
41use miden_processor::{DeserializationError, Word};
42use miden_tx::auth::BasicAuthenticator;
43use miden_tx::utils::{ByteReader, Deserializable, Serializable};
44use rand::SeedableRng;
45use rand_chacha::ChaCha20Rng;
46use winterfell::ByteWriter;
47
48use super::note::MockChainNote;
49use crate::{MockChainBuilder, ProvenTransactionExt, TransactionContextBuilder};
50
51#[derive(Debug, Clone)]
162pub struct MockChain {
163 chain: Blockchain,
165
166 blocks: Vec<ProvenBlock>,
168
169 nullifier_tree: NullifierTree,
171
172 account_tree: AccountTree,
174
175 pending_output_notes: Vec<OutputNote>,
179
180 pending_transactions: Vec<ProvenTransaction>,
183
184 committed_notes: BTreeMap<NoteId, MockChainNote>,
186
187 committed_accounts: BTreeMap<AccountId, Account>,
194
195 account_credentials: BTreeMap<AccountId, AccountCredentials>,
198
199 rng: ChaCha20Rng,
201}
202
203impl MockChain {
204 pub const TIMESTAMP_START_SECS: u32 = 1700000000;
209
210 pub const TIMESTAMP_STEP_SECS: u32 = 10;
213
214 pub fn new() -> Self {
219 Self::builder().build().expect("empty chain should be valid")
220 }
221
222 pub fn builder() -> MockChainBuilder {
224 MockChainBuilder::new()
225 }
226
227 pub(super) fn from_genesis_block(
229 genesis_block: ProvenBlock,
230 account_tree: AccountTree,
231 account_credentials: BTreeMap<AccountId, AccountCredentials>,
232 ) -> anyhow::Result<Self> {
233 let mut chain = MockChain {
234 chain: Blockchain::default(),
235 blocks: vec![],
236 nullifier_tree: NullifierTree::default(),
237 account_tree,
238 pending_output_notes: Vec::new(),
239 pending_transactions: Vec::new(),
240 committed_notes: BTreeMap::new(),
241 committed_accounts: BTreeMap::new(),
242 account_credentials,
243 rng: ChaCha20Rng::from_seed(Default::default()),
245 };
246
247 chain
250 .apply_block(genesis_block)
251 .context("failed to build account from builder")?;
252
253 debug_assert_eq!(chain.blocks.len(), 1);
254 debug_assert_eq!(chain.committed_accounts.len(), chain.account_tree.num_accounts());
255
256 Ok(chain)
257 }
258
259 pub fn blockchain(&self) -> &Blockchain {
264 &self.chain
265 }
266
267 pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
270 let block_headers =
273 self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
274
275 PartialBlockchain::from_blockchain(&self.chain, block_headers)
276 .expect("blockchain should be valid by construction")
277 }
278
279 pub fn latest_selective_partial_blockchain(
285 &self,
286 reference_blocks: impl IntoIterator<Item = BlockNumber>,
287 ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
288 let latest_block_header = self.latest_block_header();
289
290 self.selective_partial_blockchain(latest_block_header.block_num(), reference_blocks)
291 }
292
293 pub fn selective_partial_blockchain(
299 &self,
300 reference_block: BlockNumber,
301 reference_blocks: impl IntoIterator<Item = BlockNumber>,
302 ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
303 let reference_block_header = self.block_header(reference_block.as_usize());
304 let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
307
308 let mut block_headers = Vec::new();
310
311 for block_ref_num in &reference_blocks {
312 let block_index = block_ref_num.as_usize();
313 let block = self
314 .blocks
315 .get(block_index)
316 .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
317 let block_header = block.header().clone();
318 if block_header.commitment() != reference_block_header.commitment() {
320 block_headers.push(block_header);
321 }
322 }
323
324 let partial_blockchain =
325 PartialBlockchain::from_blockchain_at(&self.chain, reference_block, block_headers)?;
326
327 Ok((reference_block_header, partial_blockchain))
328 }
329
330 pub fn account_witnesses(
333 &self,
334 account_ids: impl IntoIterator<Item = AccountId>,
335 ) -> BTreeMap<AccountId, AccountWitness> {
336 let mut account_witnesses = BTreeMap::new();
337
338 for account_id in account_ids {
339 let witness = self.account_tree.open(account_id);
340 account_witnesses.insert(account_id, witness);
341 }
342
343 account_witnesses
344 }
345
346 pub fn nullifier_witnesses(
349 &self,
350 nullifiers: impl IntoIterator<Item = Nullifier>,
351 ) -> BTreeMap<Nullifier, NullifierWitness> {
352 let mut nullifier_proofs = BTreeMap::new();
353
354 for nullifier in nullifiers {
355 let witness = self.nullifier_tree.open(&nullifier);
356 nullifier_proofs.insert(nullifier, witness);
357 }
358
359 nullifier_proofs
360 }
361
362 pub fn unauthenticated_note_proofs(
366 &self,
367 notes: impl IntoIterator<Item = NoteId>,
368 ) -> BTreeMap<NoteId, NoteInclusionProof> {
369 let mut proofs = BTreeMap::default();
370 for note in notes {
371 if let Some(input_note) = self.committed_notes.get(¬e) {
372 proofs.insert(note, input_note.inclusion_proof().clone());
373 }
374 }
375
376 proofs
377 }
378
379 pub fn genesis_block_header(&self) -> BlockHeader {
381 self.block_header(BlockNumber::GENESIS.as_usize())
382 }
383
384 pub fn latest_block_header(&self) -> BlockHeader {
386 let chain_tip =
387 self.chain.chain_tip().expect("chain should contain at least the genesis block");
388 self.blocks[chain_tip.as_usize()].header().clone()
389 }
390
391 pub fn block_header(&self, block_number: usize) -> BlockHeader {
397 self.blocks[block_number].header().clone()
398 }
399
400 pub fn proven_blocks(&self) -> &[ProvenBlock] {
402 &self.blocks
403 }
404
405 pub fn native_asset_id(&self) -> AccountId {
411 self.genesis_block_header().fee_parameters().native_asset_id()
412 }
413
414 pub fn nullifier_tree(&self) -> &NullifierTree {
416 &self.nullifier_tree
417 }
418
419 pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
423 &self.committed_notes
424 }
425
426 pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
429 let note = self.committed_notes.get(note_id)?;
430 note.clone().try_into().ok()
431 }
432
433 pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
437 self.committed_accounts
438 .get(&account_id)
439 .with_context(|| format!("account {account_id} not found in committed accounts"))
440 }
441
442 pub fn account_tree(&self) -> &AccountTree {
444 &self.account_tree
445 }
446
447 pub fn propose_transaction_batch<I>(
454 &self,
455 txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
456 ) -> anyhow::Result<ProposedBatch>
457 where
458 I: Iterator<Item = ProvenTransaction> + Clone,
459 {
460 let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
461
462 let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
463 .get_batch_inputs(
464 transactions.iter().map(|tx| tx.ref_block_num()),
465 transactions
466 .iter()
467 .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
468 )?;
469
470 Ok(ProposedBatch::new(
471 transactions,
472 batch_reference_block,
473 partial_blockchain,
474 unauthenticated_note_proofs,
475 )?)
476 }
477
478 pub fn prove_transaction_batch(
482 &self,
483 proposed_batch: ProposedBatch,
484 ) -> anyhow::Result<ProvenBatch> {
485 let (
486 transactions,
487 block_header,
488 _partial_blockchain,
489 _unauthenticated_note_proofs,
490 id,
491 account_updates,
492 input_notes,
493 output_notes,
494 batch_expiration_block_num,
495 ) = proposed_batch.into_parts();
496
497 let tx_headers = OrderedTransactionHeaders::new_unchecked(
499 transactions
500 .iter()
501 .map(AsRef::as_ref)
502 .map(TransactionHeader::from)
503 .collect::<Vec<_>>(),
504 );
505
506 Ok(ProvenBatch::new(
507 id,
508 block_header.commitment(),
509 block_header.block_num(),
510 account_updates,
511 input_notes,
512 output_notes,
513 batch_expiration_block_num,
514 tx_headers,
515 )?)
516 }
517
518 pub fn propose_block_at<I>(
525 &self,
526 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
527 timestamp: u32,
528 ) -> anyhow::Result<ProposedBlock>
529 where
530 I: Iterator<Item = ProvenBatch> + Clone,
531 {
532 let batches: Vec<_> = batches.into_iter().collect();
533
534 let block_inputs = self
535 .get_block_inputs(batches.iter())
536 .context("could not retrieve block inputs")?;
537
538 let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
539 .context("failed to create proposed block")?;
540
541 Ok(proposed_block)
542 }
543
544 pub fn propose_block<I>(
548 &self,
549 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
550 ) -> anyhow::Result<ProposedBlock>
551 where
552 I: Iterator<Item = ProvenBatch> + Clone,
553 {
554 let timestamp = self.latest_block_header().timestamp() + 1;
557
558 self.propose_block_at(batches, timestamp)
559 }
560
561 pub fn prove_block(
565 &self,
566 proposed_block: ProposedBlock,
567 ) -> Result<ProvenBlock, ProvenBlockError> {
568 LocalBlockProver::new(0).prove_without_batch_verification(proposed_block)
569 }
570
571 pub fn build_tx_context_at(
593 &self,
594 reference_block: impl Into<BlockNumber>,
595 input: impl Into<TxContextInput>,
596 note_ids: &[NoteId],
597 unauthenticated_notes: &[Note],
598 ) -> anyhow::Result<TransactionContextBuilder> {
599 let input = input.into();
600 let reference_block = reference_block.into();
601
602 let credentials = self.account_credentials.get(&input.id());
603 let authenticator =
604 credentials.and_then(|credentials| credentials.authenticator().cloned());
605 let seed = credentials.and_then(|credentials| credentials.seed());
606
607 anyhow::ensure!(
608 reference_block.as_usize() < self.blocks.len(),
609 "reference block {reference_block} is out of range (latest {})",
610 self.latest_block_header().block_num()
611 );
612
613 let account = match input {
614 TxContextInput::AccountId(account_id) => {
615 if account_id.is_private() {
616 return Err(anyhow::anyhow!(
617 "transaction contexts for private accounts should be created with TxContextInput::Account"
618 ));
619 }
620
621 self.committed_accounts
622 .get(&account_id)
623 .with_context(|| {
624 format!("account {account_id} not found in committed accounts")
625 })?
626 .clone()
627 },
628 TxContextInput::Account(account) => account,
629 TxContextInput::ExecutedTransaction(executed_transaction) => {
630 let mut initial_account = executed_transaction.initial_account().clone();
631 initial_account
632 .apply_delta(executed_transaction.account_delta())
633 .context("could not apply delta from previous transaction")?;
634
635 initial_account
636 },
637 };
638
639 let tx_inputs = self
640 .get_transaction_inputs_at(
641 reference_block,
642 account.clone(),
643 seed,
644 note_ids,
645 unauthenticated_notes,
646 )
647 .context("failed to gather transaction inputs")?;
648
649 let tx_context_builder = TransactionContextBuilder::new(account)
650 .authenticator(authenticator)
651 .account_seed(seed)
652 .tx_inputs(tx_inputs);
653
654 Ok(tx_context_builder)
655 }
656
657 pub fn build_tx_context(
662 &self,
663 input: impl Into<TxContextInput>,
664 note_ids: &[NoteId],
665 unauthenticated_notes: &[Note],
666 ) -> anyhow::Result<TransactionContextBuilder> {
667 let reference_block = self.latest_block_header().block_num();
668 self.build_tx_context_at(reference_block, input, note_ids, unauthenticated_notes)
669 }
670
671 pub fn get_transaction_inputs_at(
677 &self,
678 reference_block: BlockNumber,
679 account: Account,
680 account_seed: Option<Word>,
681 notes: &[NoteId],
682 unauthenticated_notes: &[Note],
683 ) -> anyhow::Result<TransactionInputs> {
684 let ref_block = self.block_header(reference_block.as_usize());
685
686 let mut input_notes = vec![];
687 let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
688 for note in notes {
689 let input_note: InputNote = self
690 .committed_notes
691 .get(note)
692 .with_context(|| format!("note with id {note} not found"))?
693 .clone()
694 .try_into()
695 .with_context(|| {
696 format!("failed to convert mock chain note with id {note} into input note")
697 })?;
698
699 let note_block_num = input_note
700 .location()
701 .with_context(|| format!("note location not available: {note}"))?
702 .block_num();
703
704 if note_block_num > ref_block.block_num() {
705 anyhow::bail!(
706 "note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
707 ref_block.block_num()
708 )
709 }
710
711 if note_block_num != ref_block.block_num() {
712 let block_header = self
713 .blocks
714 .get(note_block_num.as_usize())
715 .with_context(|| format!("block {note_block_num} not found in chain"))?
716 .header()
717 .clone();
718 block_headers_map.insert(note_block_num, block_header);
719 }
720
721 input_notes.push(input_note);
722 }
723
724 for note in unauthenticated_notes {
725 input_notes.push(InputNote::Unauthenticated { note: note.clone() })
726 }
727
728 let block_headers = block_headers_map.values();
729 let (_, partial_blockchain) = self.selective_partial_blockchain(
730 reference_block,
731 block_headers.map(BlockHeader::block_num),
732 )?;
733
734 let input_notes = InputNotes::new(input_notes)?;
735
736 Ok(TransactionInputs::new(
737 account,
738 account_seed,
739 ref_block.clone(),
740 partial_blockchain,
741 input_notes,
742 )?)
743 }
744
745 pub fn get_transaction_inputs(
747 &self,
748 account: Account,
749 account_seed: Option<Word>,
750 notes: &[NoteId],
751 unauthenticated_notes: &[Note],
752 ) -> anyhow::Result<TransactionInputs> {
753 let latest_block_num = self.latest_block_header().block_num();
754 self.get_transaction_inputs_at(
755 latest_block_num,
756 account,
757 account_seed,
758 notes,
759 unauthenticated_notes,
760 )
761 }
762
763 pub fn get_batch_inputs(
766 &self,
767 tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
768 unauthenticated_notes: impl Iterator<Item = NoteId>,
769 ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
770 {
771 let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
773
774 let required_blocks = tx_reference_blocks.into_iter().chain(
777 unauthenticated_note_proofs
778 .values()
779 .map(|note_proof| note_proof.location().block_num()),
780 );
781
782 let (batch_reference_block, partial_block_chain) =
783 self.latest_selective_partial_blockchain(required_blocks)?;
784
785 Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
786 }
787
788 pub fn get_foreign_account_inputs(
790 &self,
791 account_id: AccountId,
792 ) -> anyhow::Result<AccountInputs> {
793 let account = self.committed_account(account_id)?;
794
795 let account_witness = self.account_tree().open(account_id);
796 assert_eq!(account_witness.state_commitment(), account.commitment());
797
798 let mut storage_map_proofs = vec![];
799 for slot in account.storage().slots() {
800 if let StorageSlot::Map(map) = slot {
802 let proofs: Vec<SmtProof> = map.entries().map(|(key, _)| map.open(key)).collect();
803 storage_map_proofs.extend(proofs);
804 }
805 }
806
807 Ok(AccountInputs::new(account.into(), account_witness))
808 }
809
810 pub fn get_block_inputs<'batch, I>(
812 &self,
813 batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
814 ) -> anyhow::Result<BlockInputs>
815 where
816 I: Iterator<Item = &'batch ProvenBatch> + Clone,
817 {
818 let batch_iterator = batch_iter.into_iter();
819
820 let unauthenticated_note_proofs =
821 self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
822 batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
823 }));
824
825 let (block_reference_block, partial_blockchain) = self
826 .latest_selective_partial_blockchain(
827 batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
828 unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
829 ),
830 )?;
831
832 let account_witnesses =
833 self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
834
835 let nullifier_proofs =
836 self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
837
838 Ok(BlockInputs::new(
839 block_reference_block,
840 partial_blockchain,
841 account_witnesses,
842 nullifier_proofs,
843 unauthenticated_note_proofs,
844 ))
845 }
846
847 pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
854 self.prove_block_inner(None)
855 }
856
857 pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
859 self.prove_block_inner(Some(timestamp))
860 }
861
862 pub fn prove_until_block(
872 &mut self,
873 target_block_num: impl Into<BlockNumber>,
874 ) -> anyhow::Result<ProvenBlock> {
875 let target_block_num = target_block_num.into();
876 let latest_block_num = self.latest_block_header().block_num();
877 assert!(
878 target_block_num > latest_block_num,
879 "target block number must be greater than the number of the latest block in the chain"
880 );
881
882 let mut last_block = None;
883 for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
884 last_block = Some(self.prove_next_block()?);
885 }
886
887 Ok(last_block.expect("at least one block should have been created"))
888 }
889
890 pub fn set_rng_seed(&mut self, seed: [u8; 32]) {
892 self.rng = ChaCha20Rng::from_seed(seed);
893 }
894
895 pub fn add_pending_executed_transaction(
905 &mut self,
906 transaction: &ExecutedTransaction,
907 ) -> anyhow::Result<Account> {
908 let mut account = transaction.initial_account().clone();
909 account.apply_delta(transaction.account_delta())?;
910
911 let proven_tx = ProvenTransaction::from_executed_transaction_mocked(transaction.clone());
913
914 self.pending_transactions.push(proven_tx);
915
916 Ok(account)
917 }
918
919 pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
924 self.pending_transactions.push(transaction);
925 }
926
927 pub fn add_pending_note(&mut self, note: OutputNote) {
932 self.pending_output_notes.push(note);
933 }
934
935 pub fn add_pending_p2id_note(
940 &mut self,
941 sender_account_id: AccountId,
942 target_account_id: AccountId,
943 asset: &[Asset],
944 note_type: NoteType,
945 ) -> Result<Note, NoteError> {
946 let mut rng = RpoRandomCoin::new(Word::empty());
947
948 let note = create_p2id_note(
949 sender_account_id,
950 target_account_id,
951 asset.to_vec(),
952 note_type,
953 Default::default(),
954 &mut rng,
955 )?;
956
957 self.add_pending_note(OutputNote::Full(note.clone()));
958 Ok(note)
959 }
960
961 pub fn add_pending_p2ide_note(
967 &mut self,
968 sender_account_id: AccountId,
969 target_account_id: AccountId,
970 asset: &[Asset],
971 note_type: NoteType,
972 reclaim_height: Option<BlockNumber>,
973 timelock_height: Option<BlockNumber>,
974 ) -> Result<Note, NoteError> {
975 let mut rng = RpoRandomCoin::new(Word::empty());
976
977 let note = create_p2ide_note(
978 sender_account_id,
979 target_account_id,
980 asset.to_vec(),
981 reclaim_height,
982 timelock_height,
983 note_type,
984 Default::default(),
985 &mut rng,
986 )?;
987
988 self.add_pending_note(OutputNote::Full(note.clone()));
989 Ok(note)
990 }
991
992 fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
1003 for account_update in proven_block.updated_accounts() {
1004 self.account_tree
1005 .insert(account_update.account_id(), account_update.final_state_commitment())
1006 .context("failed to insert account update into account tree")?;
1007 }
1008
1009 for nullifier in proven_block.created_nullifiers() {
1010 self.nullifier_tree
1011 .mark_spent(*nullifier, proven_block.header().block_num())
1012 .context("failed to mark block nullifier as spent")?;
1013
1014 }
1018
1019 for account_update in proven_block.updated_accounts() {
1020 match account_update.details() {
1021 AccountUpdateDetails::New(account) => {
1022 self.committed_accounts.insert(account.id(), account.clone());
1023 },
1024 AccountUpdateDetails::Delta(account_delta) => {
1025 let committed_account =
1026 self.committed_accounts.get_mut(&account_update.account_id()).ok_or_else(
1027 || anyhow::anyhow!("account delta in block for non-existent account"),
1028 )?;
1029 committed_account
1030 .apply_delta(account_delta)
1031 .context("failed to apply account delta")?;
1032 },
1033 AccountUpdateDetails::Private => {},
1036 }
1037 }
1038
1039 let notes_tree = proven_block.build_output_note_tree();
1040 for (block_note_index, created_note) in proven_block.output_notes() {
1041 let note_path = notes_tree.open(block_note_index);
1042 let note_inclusion_proof = NoteInclusionProof::new(
1043 proven_block.header().block_num(),
1044 block_note_index.leaf_index_value(),
1045 note_path,
1046 )
1047 .context("failed to construct note inclusion proof")?;
1048
1049 if let OutputNote::Full(note) = created_note {
1050 self.committed_notes
1051 .insert(note.id(), MockChainNote::Public(note.clone(), note_inclusion_proof));
1052 } else {
1053 self.committed_notes.insert(
1054 created_note.id(),
1055 MockChainNote::Private(
1056 created_note.id(),
1057 *created_note.metadata(),
1058 note_inclusion_proof,
1059 ),
1060 );
1061 }
1062 }
1063
1064 debug_assert_eq!(
1065 self.chain.commitment(),
1066 proven_block.header().chain_commitment(),
1067 "current mock chain commitment and new block's chain commitment should match"
1068 );
1069 debug_assert_eq!(
1070 BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
1071 proven_block.header().block_num(),
1072 "current mock chain length and new block's number should match"
1073 );
1074
1075 self.chain.push(proven_block.header().commitment());
1076 self.blocks.push(proven_block);
1077
1078 Ok(())
1079 }
1080
1081 fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1082 if self.pending_transactions.is_empty() {
1085 return Ok(vec![]);
1086 }
1087
1088 let pending_transactions = core::mem::take(&mut self.pending_transactions);
1089
1090 let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1093 let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1094
1095 Ok(vec![proven_batch])
1096 }
1097
1098 fn apply_pending_notes_to_block(
1099 &mut self,
1100 proven_block: &mut ProvenBlock,
1101 ) -> anyhow::Result<()> {
1102 let output_notes_block: BTreeSet<NoteId> =
1104 proven_block.output_notes().map(|(_, output_note)| output_note.id()).collect();
1105
1106 if self.pending_output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
1109 return Err(anyhow::anyhow!(
1110 "too many pending output notes: {}, max allowed: {MAX_OUTPUT_NOTES_PER_BATCH}",
1111 self.pending_output_notes.len(),
1112 ));
1113 }
1114
1115 let mut pending_note_batch = Vec::with_capacity(self.pending_output_notes.len());
1116 let pending_output_notes = core::mem::take(&mut self.pending_output_notes);
1117 for (note_idx, output_note) in pending_output_notes.into_iter().enumerate() {
1118 if output_notes_block.contains(&output_note.id()) {
1119 return Err(anyhow::anyhow!(
1120 "output note {} is already created in block through transactions",
1121 output_note.id()
1122 ));
1123 }
1124
1125 pending_note_batch.push((note_idx, output_note));
1126 }
1127
1128 if (proven_block.output_note_batches().len() + 1) > MAX_BATCHES_PER_BLOCK {
1129 return Err(anyhow::anyhow!(
1130 "too many batches in block: cannot add more pending notes".to_string(),
1131 ));
1132 }
1133
1134 proven_block.output_note_batches_mut().push(pending_note_batch);
1135
1136 let updated_block_note_tree = proven_block.build_output_note_tree().root();
1137
1138 let block_header = proven_block.header();
1140 let updated_header = BlockHeader::new(
1141 block_header.version(),
1142 block_header.prev_block_commitment(),
1143 block_header.block_num(),
1144 block_header.chain_commitment(),
1145 block_header.account_root(),
1146 block_header.nullifier_root(),
1147 updated_block_note_tree,
1148 block_header.tx_commitment(),
1149 block_header.tx_kernel_commitment(),
1150 block_header.proof_commitment(),
1151 block_header.fee_parameters().clone(),
1152 block_header.timestamp(),
1153 );
1154 proven_block.set_block_header(updated_header);
1155
1156 Ok(())
1157 }
1158
1159 fn prove_block_inner(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
1175 let batches = self.pending_transactions_to_batches()?;
1179
1180 let block_timestamp =
1184 timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1185
1186 let proposed_block = self
1187 .propose_block_at(batches, block_timestamp)
1188 .context("failed to create proposed block")?;
1189 let mut proven_block = self.prove_block(proposed_block).context("failed to prove block")?;
1190
1191 if !self.pending_output_notes.is_empty() {
1192 self.apply_pending_notes_to_block(&mut proven_block)?;
1193 }
1194
1195 self.apply_block(proven_block.clone()).context("failed to apply block")?;
1196
1197 Ok(proven_block)
1198 }
1199}
1200
1201impl Default for MockChain {
1202 fn default() -> Self {
1203 MockChain::new()
1204 }
1205}
1206
1207impl Serializable for MockChain {
1211 fn write_into<W: ByteWriter>(&self, target: &mut W) {
1212 self.chain.write_into(target);
1213 self.blocks.write_into(target);
1214 self.nullifier_tree.write_into(target);
1215 self.account_tree.write_into(target);
1216 self.pending_output_notes.write_into(target);
1217 self.pending_transactions.write_into(target);
1218 self.committed_accounts.write_into(target);
1219 self.committed_notes.write_into(target);
1220 self.account_credentials.write_into(target);
1221 }
1222}
1223
1224impl Deserializable for MockChain {
1225 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1226 let chain = Blockchain::read_from(source)?;
1227 let blocks = Vec::<ProvenBlock>::read_from(source)?;
1228 let nullifier_tree = NullifierTree::read_from(source)?;
1229 let account_tree = AccountTree::read_from(source)?;
1230 let pending_output_notes = Vec::<OutputNote>::read_from(source)?;
1231 let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
1232 let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
1233 let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
1234 let account_credentials = BTreeMap::<AccountId, AccountCredentials>::read_from(source)?;
1235
1236 Ok(Self {
1237 chain,
1238 blocks,
1239 nullifier_tree,
1240 account_tree,
1241 pending_output_notes,
1242 pending_transactions,
1243 committed_notes,
1244 committed_accounts,
1245 account_credentials,
1246 rng: ChaCha20Rng::from_os_rng(),
1247 })
1248 }
1249}
1250
1251pub enum AccountState {
1257 New,
1258 Exists,
1259}
1260
1261#[derive(Debug, Clone)]
1266pub(super) struct AccountCredentials {
1267 seed: Option<Word>,
1268 authenticator: Option<BasicAuthenticator<ChaCha20Rng>>,
1269}
1270
1271impl AccountCredentials {
1272 pub fn new(seed: Option<Word>, authenticator: Option<BasicAuthenticator<ChaCha20Rng>>) -> Self {
1273 Self { seed, authenticator }
1274 }
1275
1276 pub fn authenticator(&self) -> Option<&BasicAuthenticator<ChaCha20Rng>> {
1277 self.authenticator.as_ref()
1278 }
1279
1280 pub fn seed(&self) -> Option<Word> {
1281 self.seed
1282 }
1283}
1284
1285impl PartialEq for AccountCredentials {
1286 fn eq(&self, other: &Self) -> bool {
1287 let authenticator_eq = match (&self.authenticator, &other.authenticator) {
1288 (Some(a), Some(b)) => {
1289 a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
1290 },
1291 (None, None) => true,
1292 _ => false,
1293 };
1294 authenticator_eq && self.seed == other.seed
1295 }
1296}
1297
1298impl Serializable for AccountCredentials {
1302 fn write_into<W: ByteWriter>(&self, target: &mut W) {
1303 self.seed.write_into(target);
1304 self.authenticator
1305 .as_ref()
1306 .map(|auth| auth.keys().iter().collect::<Vec<_>>())
1307 .write_into(target);
1308 }
1309}
1310
1311impl Deserializable for AccountCredentials {
1312 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1313 let seed = Option::<Word>::read_from(source)?;
1314 let authenticator = Option::<Vec<(Word, AuthSecretKey)>>::read_from(source)?;
1315
1316 let authenticator = authenticator
1317 .map(|keys| BasicAuthenticator::new_with_rng(&keys, ChaCha20Rng::from_os_rng()));
1318
1319 Ok(Self { seed, authenticator })
1320 }
1321}
1322
1323#[derive(Debug, Clone)]
1329pub enum TxContextInput {
1330 AccountId(AccountId),
1331 Account(Account),
1332 ExecutedTransaction(Box<ExecutedTransaction>),
1333}
1334
1335impl TxContextInput {
1336 fn id(&self) -> AccountId {
1338 match self {
1339 TxContextInput::AccountId(account_id) => *account_id,
1340 TxContextInput::Account(account) => account.id(),
1341 TxContextInput::ExecutedTransaction(executed_transaction) => {
1342 executed_transaction.account_id()
1343 },
1344 }
1345 }
1346}
1347
1348impl From<AccountId> for TxContextInput {
1349 fn from(account: AccountId) -> Self {
1350 Self::AccountId(account)
1351 }
1352}
1353
1354impl From<Account> for TxContextInput {
1355 fn from(account: Account) -> Self {
1356 Self::Account(account)
1357 }
1358}
1359
1360impl From<ExecutedTransaction> for TxContextInput {
1361 fn from(tx: ExecutedTransaction) -> Self {
1362 Self::ExecutedTransaction(Box::new(tx))
1363 }
1364}
1365
1366#[cfg(test)]
1370mod tests {
1371 use miden_lib::account::wallets::BasicWallet;
1372 use miden_objects::account::{AccountBuilder, AccountStorageMode};
1373 use miden_objects::asset::FungibleAsset;
1374 use miden_objects::testing::account_id::{
1375 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1376 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1377 ACCOUNT_ID_SENDER,
1378 };
1379
1380 use super::*;
1381 use crate::Auth;
1382
1383 #[test]
1384 fn prove_until_block() -> anyhow::Result<()> {
1385 let mut chain = MockChain::new();
1386 let block = chain.prove_until_block(5)?;
1387 assert_eq!(block.header().block_num(), 5u32.into());
1388 assert_eq!(chain.proven_blocks().len(), 6);
1389
1390 Ok(())
1391 }
1392
1393 #[test]
1394 fn private_account_state_update() -> anyhow::Result<()> {
1395 let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1396 let account_builder = AccountBuilder::new([4; 32])
1397 .storage_mode(AccountStorageMode::Private)
1398 .with_component(BasicWallet);
1399
1400 let mut builder = MockChain::builder();
1401 let account = builder.add_account_from_builder(
1402 Auth::BasicAuth,
1403 account_builder,
1404 AccountState::New,
1405 )?;
1406
1407 let account_id = account.id();
1408 assert_eq!(account.nonce().as_int(), 0);
1409
1410 let note_1 = builder.add_p2id_note(
1411 ACCOUNT_ID_SENDER.try_into().unwrap(),
1412 account.id(),
1413 &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1414 NoteType::Private,
1415 )?;
1416
1417 let mut mock_chain = builder.build()?;
1418 mock_chain.prove_next_block()?;
1419
1420 let tx = mock_chain
1421 .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1422 .build()?
1423 .execute_blocking()?;
1424
1425 mock_chain.add_pending_executed_transaction(&tx)?;
1426 mock_chain.prove_next_block()?;
1427
1428 assert!(tx.final_account().nonce().as_int() > 0);
1429 assert_eq!(
1430 tx.final_account().commitment(),
1431 mock_chain.account_tree.open(account_id).state_commitment()
1432 );
1433
1434 Ok(())
1435 }
1436
1437 #[test]
1438 fn mock_chain_serialization() {
1439 let mut builder = MockChain::builder();
1440
1441 let mut notes = vec![];
1442 for i in 0..10 {
1443 let account = builder
1444 .add_account_from_builder(
1445 Auth::BasicAuth,
1446 AccountBuilder::new([i; 32]).with_component(BasicWallet),
1447 AccountState::New,
1448 )
1449 .unwrap();
1450 let note = builder
1451 .add_p2id_note(
1452 ACCOUNT_ID_SENDER.try_into().unwrap(),
1453 account.id(),
1454 &[Asset::Fungible(
1455 FungibleAsset::new(
1456 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1457 1000u64,
1458 )
1459 .unwrap(),
1460 )],
1461 NoteType::Private,
1462 )
1463 .unwrap();
1464 notes.push((account, note));
1465 }
1466
1467 let mut chain = builder.build().unwrap();
1468 for (account, note) in notes {
1469 let tx = chain
1470 .build_tx_context(TxContextInput::Account(account), &[], &[note])
1471 .unwrap()
1472 .build()
1473 .unwrap()
1474 .execute_blocking()
1475 .unwrap();
1476 chain.add_pending_executed_transaction(&tx).unwrap();
1477 chain.prove_next_block().unwrap();
1478 }
1479
1480 let bytes = chain.to_bytes();
1481
1482 let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
1483
1484 assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
1485 assert_eq!(chain.blocks, deserialized.blocks);
1486 assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
1487 assert_eq!(chain.account_tree, deserialized.account_tree);
1488 assert_eq!(chain.pending_output_notes, deserialized.pending_output_notes);
1489 assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
1490 assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
1491 assert_eq!(chain.committed_notes, deserialized.committed_notes);
1492 assert_eq!(chain.account_credentials, deserialized.account_credentials);
1493 }
1494}