1use alloc::{
2 boxed::Box,
3 collections::{BTreeMap, BTreeSet},
4 string::ToString,
5 vec::Vec,
6};
7
8use anyhow::Context;
9use miden_block_prover::{LocalBlockProver, ProvenBlockError};
10use miden_lib::{
11 account::{faucets::BasicFungibleFaucet, wallets::BasicWallet},
12 note::{create_p2id_note, create_p2ide_note},
13 transaction::{TransactionKernel, memory},
14};
15use miden_objects::{
16 MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, NoteError,
17 account::{
18 Account, AccountBuilder, AccountId, AccountStorageMode, AccountType, StorageSlot,
19 delta::AccountUpdateDetails,
20 },
21 asset::{Asset, TokenSymbol},
22 batch::{ProposedBatch, ProvenBatch},
23 block::{
24 AccountTree, AccountWitness, BlockAccountUpdate, BlockHeader, BlockInputs, BlockNoteTree,
25 BlockNumber, Blockchain, NullifierTree, NullifierWitness, ProposedBlock, ProvenBlock,
26 },
27 crypto::merkle::SmtProof,
28 note::{Note, NoteHeader, NoteId, NoteInclusionProof, NoteType, Nullifier},
29 transaction::{
30 AccountInputs, ExecutedTransaction, InputNote, InputNotes, OrderedTransactionHeaders,
31 OutputNote, PartialBlockchain, ProvenTransaction, TransactionHeader, TransactionInputs,
32 },
33};
34use rand::{Rng, SeedableRng};
35use rand_chacha::ChaCha20Rng;
36use vm_processor::{Digest, Felt, Word, ZERO, crypto::RpoRandomCoin};
37
38use super::note::MockChainNote;
39use crate::{
40 Auth, MockFungibleFaucet, ProvenTransactionExt, TransactionContextBuilder,
41 mock_chain::account::MockAccount,
42};
43
44#[derive(Debug, Clone)]
158pub struct MockChain {
159 chain: Blockchain,
161
162 blocks: Vec<ProvenBlock>,
164
165 nullifier_tree: NullifierTree,
167
168 account_tree: AccountTree,
170
171 pending_objects: PendingObjects,
178
179 pending_transactions: Vec<ProvenTransaction>,
182
183 committed_notes: BTreeMap<NoteId, MockChainNote>,
185
186 committed_accounts: BTreeMap<AccountId, MockAccount>,
193
194 rng: ChaCha20Rng,
196}
197
198impl MockChain {
199 pub const TIMESTAMP_START_SECS: u32 = 1700000000;
204
205 pub const TIMESTAMP_STEP_SECS: u32 = 10;
208
209 pub fn new() -> Self {
214 Self::with_accounts(&[]).expect("empty mockchain is valid")
215 }
216
217 pub fn with_accounts(accounts: &[Account]) -> anyhow::Result<Self> {
219 let (genesis_block, account_tree) = create_genesis_state(accounts.iter().cloned())
220 .context("failed to build account from builder")?;
221
222 let mut chain = MockChain {
223 chain: Blockchain::default(),
224 blocks: vec![],
225 nullifier_tree: NullifierTree::default(),
226 account_tree,
227 pending_objects: PendingObjects::new(),
228 pending_transactions: Vec::new(),
229 committed_notes: BTreeMap::new(),
230 committed_accounts: BTreeMap::new(),
231 rng: ChaCha20Rng::from_seed(Default::default()),
233 };
234
235 chain
238 .apply_block(genesis_block)
239 .context("failed to build account from builder")?;
240
241 debug_assert_eq!(chain.blocks.len(), 1);
242 debug_assert_eq!(chain.account_tree.num_accounts(), accounts.len());
243 debug_assert_eq!(chain.committed_accounts.len(), accounts.len());
244 for added_account in accounts {
245 debug_assert_eq!(
246 chain.account_tree.get(added_account.id()),
247 added_account.commitment()
248 );
249 debug_assert_eq!(
250 chain.committed_account(added_account.id())?.commitment(),
251 added_account.commitment(),
252 );
253 }
254
255 Ok(chain)
256 }
257
258 pub fn blockchain(&self) -> &Blockchain {
263 &self.chain
264 }
265
266 pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
269 let block_headers =
272 self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
273
274 PartialBlockchain::from_blockchain(&self.chain, block_headers)
275 .expect("blockchain should be valid by construction")
276 }
277
278 pub fn latest_selective_partial_blockchain(
284 &self,
285 reference_blocks: impl IntoIterator<Item = BlockNumber>,
286 ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
287 let latest_block_header = self.latest_block_header();
288 let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
291
292 let mut block_headers = Vec::new();
294
295 for block_ref_num in &reference_blocks {
296 let block_index = block_ref_num.as_usize();
297 let block = self
298 .blocks
299 .get(block_index)
300 .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
301 let block_header = block.header().clone();
302 if block_header.commitment() != latest_block_header.commitment() {
304 block_headers.push(block_header);
305 }
306 }
307
308 let partial_blockchain = PartialBlockchain::from_blockchain(&self.chain, block_headers)?;
309
310 Ok((latest_block_header, partial_blockchain))
311 }
312
313 pub fn account_witnesses(
316 &self,
317 account_ids: impl IntoIterator<Item = AccountId>,
318 ) -> BTreeMap<AccountId, AccountWitness> {
319 let mut account_witnesses = BTreeMap::new();
320
321 for account_id in account_ids {
322 let witness = self.account_tree.open(account_id);
323 account_witnesses.insert(account_id, witness);
324 }
325
326 account_witnesses
327 }
328
329 pub fn nullifier_witnesses(
332 &self,
333 nullifiers: impl IntoIterator<Item = Nullifier>,
334 ) -> BTreeMap<Nullifier, NullifierWitness> {
335 let mut nullifier_proofs = BTreeMap::new();
336
337 for nullifier in nullifiers {
338 let witness = self.nullifier_tree.open(&nullifier);
339 nullifier_proofs.insert(nullifier, witness);
340 }
341
342 nullifier_proofs
343 }
344
345 pub fn unauthenticated_note_proofs(
349 &self,
350 notes: impl IntoIterator<Item = NoteId>,
351 ) -> BTreeMap<NoteId, NoteInclusionProof> {
352 let mut proofs = BTreeMap::default();
353 for note in notes {
354 if let Some(input_note) = self.committed_notes.get(¬e) {
355 proofs.insert(note, input_note.inclusion_proof().clone());
356 }
357 }
358
359 proofs
360 }
361
362 pub fn latest_block_header(&self) -> BlockHeader {
364 let chain_tip =
365 self.chain.chain_tip().expect("chain should contain at least the genesis block");
366 self.blocks[chain_tip.as_usize()].header().clone()
367 }
368
369 pub fn block_header(&self, block_number: usize) -> BlockHeader {
375 self.blocks[block_number].header().clone()
376 }
377
378 pub fn proven_blocks(&self) -> &[ProvenBlock] {
380 &self.blocks
381 }
382
383 pub fn nullifier_tree(&self) -> &NullifierTree {
385 &self.nullifier_tree
386 }
387
388 pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
392 &self.committed_notes
393 }
394
395 pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
398 let note = self.committed_notes.get(note_id)?;
399 note.clone().try_into().ok()
400 }
401
402 pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
406 self.committed_accounts
407 .get(&account_id)
408 .map(|mock_account| mock_account.account())
409 .with_context(|| format!("account {account_id} not found in committed accounts"))
410 }
411
412 pub fn account_tree(&self) -> &AccountTree {
414 &self.account_tree
415 }
416
417 pub fn propose_transaction_batch<I>(
424 &self,
425 txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
426 ) -> anyhow::Result<ProposedBatch>
427 where
428 I: Iterator<Item = ProvenTransaction> + Clone,
429 {
430 let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
431
432 let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
433 .get_batch_inputs(
434 transactions.iter().map(|tx| tx.ref_block_num()),
435 transactions
436 .iter()
437 .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
438 )?;
439
440 Ok(ProposedBatch::new(
441 transactions,
442 batch_reference_block,
443 partial_blockchain,
444 unauthenticated_note_proofs,
445 )?)
446 }
447
448 pub fn prove_transaction_batch(
452 &self,
453 proposed_batch: ProposedBatch,
454 ) -> anyhow::Result<ProvenBatch> {
455 let (
456 transactions,
457 block_header,
458 _partial_blockchain,
459 _unauthenticated_note_proofs,
460 id,
461 account_updates,
462 input_notes,
463 output_notes,
464 batch_expiration_block_num,
465 ) = proposed_batch.into_parts();
466
467 let tx_headers = OrderedTransactionHeaders::new_unchecked(
469 transactions
470 .iter()
471 .map(AsRef::as_ref)
472 .map(TransactionHeader::from)
473 .collect::<Vec<_>>(),
474 );
475
476 Ok(ProvenBatch::new(
477 id,
478 block_header.commitment(),
479 block_header.block_num(),
480 account_updates,
481 input_notes,
482 output_notes,
483 batch_expiration_block_num,
484 tx_headers,
485 )?)
486 }
487
488 pub fn propose_block_at<I>(
495 &self,
496 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
497 timestamp: u32,
498 ) -> anyhow::Result<ProposedBlock>
499 where
500 I: Iterator<Item = ProvenBatch> + Clone,
501 {
502 let batches: Vec<_> = batches.into_iter().collect();
503
504 let block_inputs = self
505 .get_block_inputs(batches.iter())
506 .context("could not retrieve block inputs")?;
507
508 let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
509 .context("failed to create proposed block")?;
510
511 Ok(proposed_block)
512 }
513
514 pub fn propose_block<I>(
518 &self,
519 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
520 ) -> anyhow::Result<ProposedBlock>
521 where
522 I: Iterator<Item = ProvenBatch> + Clone,
523 {
524 let timestamp = self.latest_block_header().timestamp() + 1;
527
528 self.propose_block_at(batches, timestamp)
529 }
530
531 pub fn prove_block(
535 &self,
536 proposed_block: ProposedBlock,
537 ) -> Result<ProvenBlock, ProvenBlockError> {
538 LocalBlockProver::new(0).prove_without_batch_verification(proposed_block)
539 }
540
541 pub fn build_tx_context(
563 &self,
564 input: impl Into<TxContextInput>,
565 note_ids: &[NoteId],
566 unauthenticated_notes: &[Note],
567 ) -> anyhow::Result<TransactionContextBuilder> {
568 let mock_account = match input.into() {
569 TxContextInput::AccountId(account_id) => {
570 if account_id.is_private() {
571 return Err(anyhow::anyhow!(
572 "transaction contexts for private accounts should be created with TxContextInput::Account"
573 ));
574 }
575
576 self.committed_accounts
577 .get(&account_id)
578 .with_context(|| {
579 format!("account {account_id} not found in committed accounts")
580 })?
581 .clone()
582 },
583 TxContextInput::Account(account) => {
584 let committed_account = self.committed_accounts.get(&account.id());
585 let authenticator = committed_account.and_then(|a| a.authenticator());
586 let seed = committed_account.and_then(|a| a.seed());
587 MockAccount::new(account, seed.cloned(), authenticator.cloned())
588 },
589 TxContextInput::ExecutedTransaction(executed_transaction) => {
590 let mut initial_account = executed_transaction.initial_account().clone();
591 initial_account
592 .apply_delta(executed_transaction.account_delta())
593 .context("could not apply delta from previous transaction")?;
594
595 let committed_account = self.committed_accounts.get(&initial_account.id());
596 let authenticator = committed_account.and_then(|a| a.authenticator());
597 let seed = committed_account.and_then(|a| a.seed());
598 MockAccount::new(initial_account, seed.cloned(), authenticator.cloned())
599 },
600 };
601
602 let tx_inputs = self
603 .get_transaction_inputs(
604 mock_account.account().clone(),
605 mock_account.seed().cloned(),
606 note_ids,
607 unauthenticated_notes,
608 )
609 .context("failed to gather transaction inputs")?;
610
611 let tx_context_builder = TransactionContextBuilder::new(mock_account.account().clone())
612 .authenticator(mock_account.authenticator().cloned())
613 .account_seed(mock_account.seed().cloned())
614 .tx_inputs(tx_inputs);
615
616 Ok(tx_context_builder)
617 }
618
619 pub fn get_transaction_inputs(
624 &self,
625 account: Account,
626 account_seed: Option<Word>,
627 notes: &[NoteId],
628 unauthenticated_notes: &[Note],
629 ) -> anyhow::Result<TransactionInputs> {
630 let block = self.blocks.last().expect("at least one block should have been created");
631
632 let mut input_notes = vec![];
633 let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
634 for note in notes {
635 let input_note: InputNote = self
636 .committed_notes
637 .get(note)
638 .with_context(|| format!("note with id {note} not found"))?
639 .clone()
640 .try_into()
641 .context("note error")?;
642
643 let note_block_num = input_note
644 .location()
645 .with_context(|| format!("note location not available: {note}"))?
646 .block_num();
647
648 if note_block_num != block.header().block_num() {
649 let block_header = self
650 .blocks
651 .get(note_block_num.as_usize())
652 .with_context(|| format!("block {note_block_num} not found in chain"))?
653 .header()
654 .clone();
655 block_headers_map.insert(note_block_num, block_header);
656 }
657
658 input_notes.push(input_note);
659 }
660
661 for note in unauthenticated_notes {
662 input_notes.push(InputNote::Unauthenticated { note: note.clone() })
663 }
664
665 let block_headers = block_headers_map.values().cloned();
666 let mmr = PartialBlockchain::from_blockchain(&self.chain, block_headers)?;
667
668 let input_notes = InputNotes::new(input_notes)?;
669
670 Ok(TransactionInputs::new(
671 account,
672 account_seed,
673 block.header().clone(),
674 mmr,
675 input_notes,
676 )?)
677 }
678
679 pub fn get_batch_inputs(
682 &self,
683 tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
684 unauthenticated_notes: impl Iterator<Item = NoteId>,
685 ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
686 {
687 let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
689
690 let required_blocks = tx_reference_blocks.into_iter().chain(
693 unauthenticated_note_proofs
694 .values()
695 .map(|note_proof| note_proof.location().block_num()),
696 );
697
698 let (batch_reference_block, partial_block_chain) =
699 self.latest_selective_partial_blockchain(required_blocks)?;
700
701 Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
702 }
703
704 pub fn get_foreign_account_inputs(
706 &self,
707 account_id: AccountId,
708 ) -> anyhow::Result<AccountInputs> {
709 let account = self.committed_account(account_id)?;
710
711 let account_witness = self.account_tree().open(account_id);
712 assert_eq!(account_witness.state_commitment(), account.commitment());
713
714 let mut storage_map_proofs = vec![];
715 for slot in account.storage().slots() {
716 if let StorageSlot::Map(map) = slot {
718 let proofs: Vec<SmtProof> = map.entries().map(|(key, _)| map.open(key)).collect();
719 storage_map_proofs.extend(proofs);
720 }
721 }
722
723 Ok(AccountInputs::new(account.into(), account_witness))
724 }
725
726 pub fn get_block_inputs<'batch, I>(
728 &self,
729 batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
730 ) -> anyhow::Result<BlockInputs>
731 where
732 I: Iterator<Item = &'batch ProvenBatch> + Clone,
733 {
734 let batch_iterator = batch_iter.into_iter();
735
736 let unauthenticated_note_proofs =
737 self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
738 batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
739 }));
740
741 let (block_reference_block, partial_blockchain) = self
742 .latest_selective_partial_blockchain(
743 batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
744 unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
745 ),
746 )?;
747
748 let account_witnesses =
749 self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
750
751 let nullifier_proofs =
752 self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
753
754 Ok(BlockInputs::new(
755 block_reference_block,
756 partial_blockchain,
757 account_witnesses,
758 nullifier_proofs,
759 unauthenticated_note_proofs,
760 ))
761 }
762
763 pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
770 self.prove_block_inner(None)
771 }
772
773 pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
775 self.prove_block_inner(Some(timestamp))
776 }
777
778 pub fn prove_until_block(
788 &mut self,
789 target_block_num: impl Into<BlockNumber>,
790 ) -> anyhow::Result<ProvenBlock> {
791 let target_block_num = target_block_num.into();
792 let latest_block_num = self.latest_block_header().block_num();
793 assert!(
794 target_block_num > latest_block_num,
795 "target block number must be greater than the number of the latest block in the chain"
796 );
797
798 let mut last_block = None;
799 for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
800 last_block = Some(self.prove_next_block()?);
801 }
802
803 Ok(last_block.expect("at least one block should have been created"))
804 }
805
806 pub fn set_rng_seed(&mut self, seed: [u8; 32]) {
808 self.rng = ChaCha20Rng::from_seed(seed);
809 }
810
811 pub fn add_pending_executed_transaction(
821 &mut self,
822 transaction: &ExecutedTransaction,
823 ) -> anyhow::Result<Account> {
824 let mut account = transaction.initial_account().clone();
825 account.apply_delta(transaction.account_delta())?;
826
827 let proven_tx = ProvenTransaction::from_executed_transaction_mocked(transaction.clone());
829
830 self.pending_transactions.push(proven_tx);
831
832 Ok(account)
833 }
834
835 pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
840 self.pending_transactions.push(transaction);
841 }
842
843 pub fn add_pending_note(&mut self, note: OutputNote) {
848 self.pending_objects.output_notes.push(note);
849 }
850
851 pub fn add_pending_p2id_note(
856 &mut self,
857 sender_account_id: AccountId,
858 target_account_id: AccountId,
859 asset: &[Asset],
860 note_type: NoteType,
861 ) -> Result<Note, NoteError> {
862 let mut rng = RpoRandomCoin::new(Word::default());
863
864 let note = create_p2id_note(
865 sender_account_id,
866 target_account_id,
867 asset.to_vec(),
868 note_type,
869 Default::default(),
870 &mut rng,
871 )?;
872
873 self.add_pending_note(OutputNote::Full(note.clone()));
874 Ok(note)
875 }
876
877 pub fn add_pending_p2ide_note(
883 &mut self,
884 sender_account_id: AccountId,
885 target_account_id: AccountId,
886 asset: &[Asset],
887 note_type: NoteType,
888 reclaim_height: Option<BlockNumber>,
889 timelock_height: Option<BlockNumber>,
890 ) -> Result<Note, NoteError> {
891 let mut rng = RpoRandomCoin::new(Word::default());
892
893 let note = create_p2ide_note(
894 sender_account_id,
895 target_account_id,
896 asset.to_vec(),
897 reclaim_height,
898 timelock_height,
899 note_type,
900 Default::default(),
901 &mut rng,
902 )?;
903
904 self.add_pending_note(OutputNote::Full(note.clone()));
905 Ok(note)
906 }
907
908 pub fn add_pending_nullifier(&mut self, nullifier: Nullifier) {
913 self.pending_objects.created_nullifiers.push(nullifier);
914 }
915
916 pub fn add_pending_new_wallet(&mut self, auth_method: Auth) -> Account {
921 let account_builder = AccountBuilder::new(self.rng.random())
922 .storage_mode(AccountStorageMode::Public)
923 .with_component(BasicWallet);
924
925 self.add_pending_account_from_builder(auth_method, account_builder, AccountState::New)
926 .expect("failed to add pending account from builder")
927 }
928
929 pub fn add_pending_existing_wallet(
935 &mut self,
936 auth_method: Auth,
937 assets: Vec<Asset>,
938 ) -> Account {
939 let account_builder = Account::builder(self.rng.random())
940 .storage_mode(AccountStorageMode::Public)
941 .with_component(BasicWallet)
942 .with_assets(assets);
943
944 self.add_pending_account_from_builder(auth_method, account_builder, AccountState::Exists)
945 .expect("failed to add pending account from builder")
946 }
947
948 pub fn add_pending_new_faucet(
954 &mut self,
955 auth_method: Auth,
956 token_symbol: &str,
957 max_supply: u64,
958 ) -> anyhow::Result<MockFungibleFaucet> {
959 let token_symbol = TokenSymbol::new(token_symbol)
960 .with_context(|| format!("invalid token symbol: {token_symbol}"))?;
961 let max_supply_felt = max_supply.try_into().map_err(|_| {
962 anyhow::anyhow!("max supply value cannot be converted to Felt: {max_supply}")
963 })?;
964 let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10, max_supply_felt)
965 .context("failed to create BasicFungibleFaucet")?;
966
967 let account_builder = AccountBuilder::new(self.rng.random())
968 .storage_mode(AccountStorageMode::Public)
969 .account_type(AccountType::FungibleFaucet)
970 .with_component(basic_faucet);
971
972 let account =
973 self.add_pending_account_from_builder(auth_method, account_builder, AccountState::New)?;
974
975 Ok(MockFungibleFaucet::new(account))
976 }
977
978 pub fn add_pending_existing_faucet(
984 &mut self,
985 auth_method: Auth,
986 token_symbol: &str,
987 max_supply: u64,
988 total_issuance: Option<u64>,
989 ) -> anyhow::Result<MockFungibleFaucet> {
990 let token_symbol = TokenSymbol::new(token_symbol).context("invalid argument")?;
991 let basic_faucet = BasicFungibleFaucet::new(token_symbol, 10u8, Felt::new(max_supply))
992 .context("invalid argument")?;
993
994 let mut account_builder = AccountBuilder::new(self.rng.random())
995 .storage_mode(AccountStorageMode::Public)
996 .with_component(basic_faucet)
997 .account_type(AccountType::FungibleFaucet);
998
999 let (auth_component, authenticator) = auth_method.build_component();
1000 account_builder = account_builder.with_auth_component(auth_component);
1001 let mut account = account_builder
1002 .build_existing()
1003 .context("failed to build account from builder")?;
1004
1005 if let Some(issuance) = total_issuance {
1008 account
1009 .storage_mut()
1010 .set_item(memory::FAUCET_STORAGE_DATA_SLOT, [ZERO, ZERO, ZERO, Felt::new(issuance)])
1011 .context("failed to set faucet storage")?;
1012 }
1013
1014 self.committed_accounts
1017 .insert(account.id(), MockAccount::new(account.clone(), None, authenticator));
1018 self.add_pending_account(account.clone());
1019
1020 Ok(MockFungibleFaucet::new(account))
1021 }
1022
1023 pub fn add_pending_account_from_builder(
1033 &mut self,
1034 auth_method: Auth,
1035 mut account_builder: AccountBuilder,
1036 account_state: AccountState,
1037 ) -> anyhow::Result<Account> {
1038 let (auth_component, authenticator) = auth_method.build_component();
1039 account_builder = account_builder.with_auth_component(auth_component);
1040
1041 let (account, seed) = if let AccountState::New = account_state {
1042 let (account, seed) =
1043 account_builder.build().context("failed to build account from builder")?;
1044 (account, Some(seed))
1045 } else {
1046 let account = account_builder
1047 .build_existing()
1048 .context("failed to build account from builder")?;
1049 (account, None)
1050 };
1051
1052 self.committed_accounts
1058 .insert(account.id(), MockAccount::new(account.clone(), seed, authenticator));
1059
1060 if let AccountState::Exists = account_state {
1068 self.add_pending_account(account.clone());
1069 }
1070
1071 Ok(account)
1072 }
1073
1074 pub fn add_pending_account(&mut self, account: Account) {
1078 let account_id = account.id();
1079 let account_commitment = account.commitment();
1080 let update_details = match account.is_private() {
1081 true => AccountUpdateDetails::Private,
1082 false => AccountUpdateDetails::New(account),
1083 };
1084
1085 self.pending_objects.updated_accounts.insert(
1086 account_id,
1087 BlockAccountUpdate::new(account_id, account_commitment, update_details),
1088 );
1089 }
1090
1091 fn apply_block_tree_updates(&mut self, proven_block: &ProvenBlock) -> anyhow::Result<()> {
1097 for account_update in proven_block.updated_accounts() {
1098 self.account_tree
1099 .insert(account_update.account_id(), account_update.final_state_commitment())
1100 .context("failed to insert account update into account tree")?;
1101 }
1102
1103 for nullifier in proven_block.created_nullifiers() {
1104 self.nullifier_tree
1105 .mark_spent(*nullifier, proven_block.header().block_num())
1106 .context("failed to mark block nullifier as spent")?;
1107
1108 }
1112
1113 Ok(())
1114 }
1115
1116 fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
1123 for account_update in proven_block.updated_accounts() {
1124 match account_update.details() {
1125 AccountUpdateDetails::New(account) => {
1126 let committed_account =
1127 self.committed_accounts.get(&account_update.account_id());
1128 let authenticator =
1129 committed_account.and_then(|account| account.authenticator());
1130 let seed = committed_account.and_then(|account| account.seed());
1131
1132 self.committed_accounts.insert(
1133 account.id(),
1134 MockAccount::new(account.clone(), seed.cloned(), authenticator.cloned()),
1135 );
1136 },
1137 AccountUpdateDetails::Delta(account_delta) => {
1138 let committed_account =
1139 self.committed_accounts.get_mut(&account_update.account_id()).ok_or_else(
1140 || anyhow::anyhow!("account delta in block for non-existent account"),
1141 )?;
1142 committed_account
1143 .apply_delta(account_delta)
1144 .context("failed to apply account delta")?;
1145 },
1146 AccountUpdateDetails::Private => {},
1149 }
1150 }
1151
1152 let notes_tree = proven_block.build_output_note_tree();
1153 for (block_note_index, created_note) in proven_block.output_notes() {
1154 let note_path = notes_tree.get_note_path(block_note_index);
1155 let note_inclusion_proof = NoteInclusionProof::new(
1156 proven_block.header().block_num(),
1157 block_note_index.leaf_index_value(),
1158 note_path,
1159 )
1160 .context("failed to construct note inclusion proof")?;
1161
1162 if let OutputNote::Full(note) = created_note {
1163 self.committed_notes
1164 .insert(note.id(), MockChainNote::Public(note.clone(), note_inclusion_proof));
1165 } else {
1166 self.committed_notes.insert(
1167 created_note.id(),
1168 MockChainNote::Private(
1169 created_note.id(),
1170 *created_note.metadata(),
1171 note_inclusion_proof,
1172 ),
1173 );
1174 }
1175 }
1176
1177 debug_assert_eq!(
1178 self.chain.commitment(),
1179 proven_block.header().chain_commitment(),
1180 "current mock chain commitment and new block's chain commitment should match"
1181 );
1182 debug_assert_eq!(
1183 BlockNumber::from(self.chain.as_mmr().forest() as u32),
1184 proven_block.header().block_num(),
1185 "current mock chain length and new block's number should match"
1186 );
1187
1188 self.chain.push(proven_block.header().commitment());
1189 self.blocks.push(proven_block);
1190
1191 Ok(())
1192 }
1193
1194 fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1195 if self.pending_transactions.is_empty() {
1198 return Ok(vec![]);
1199 }
1200
1201 let pending_transactions = core::mem::take(&mut self.pending_transactions);
1202
1203 let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1206 let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1207
1208 Ok(vec![proven_batch])
1209 }
1210
1211 fn apply_pending_objects_to_block(
1212 &mut self,
1213 proven_block: &mut ProvenBlock,
1214 ) -> anyhow::Result<()> {
1215 let pending_account_updates = core::mem::take(&mut self.pending_objects.updated_accounts);
1217
1218 let updated_accounts_block: BTreeSet<AccountId> = proven_block
1219 .updated_accounts()
1220 .iter()
1221 .map(|update| update.account_id())
1222 .collect();
1223
1224 for (id, account_update) in pending_account_updates {
1225 if updated_accounts_block.contains(&id) {
1226 return Err(anyhow::anyhow!(
1227 "account {id} is already modified in block through transactions",
1228 ));
1229 }
1230
1231 self.account_tree
1232 .insert(id, account_update.final_state_commitment())
1233 .context("failed to insert pending account into tree")?;
1234
1235 proven_block.updated_accounts_mut().push(account_update);
1236 }
1237
1238 let pending_created_nullifiers =
1240 core::mem::take(&mut self.pending_objects.created_nullifiers);
1241
1242 let created_nullifiers_block: BTreeSet<Nullifier> =
1243 proven_block.created_nullifiers().iter().copied().collect();
1244
1245 for nullifier in pending_created_nullifiers {
1246 if created_nullifiers_block.contains(&nullifier) {
1247 return Err(anyhow::anyhow!(
1248 "nullifier {nullifier} is already created in block through transactions",
1249 ));
1250 }
1251
1252 self.nullifier_tree
1253 .mark_spent(nullifier, proven_block.header().block_num())
1254 .context("failed to insert pending nullifier into tree")?;
1255
1256 proven_block.created_nullifiers_mut().push(nullifier);
1257 }
1258
1259 let output_notes_block: BTreeSet<NoteId> =
1261 proven_block.output_notes().map(|(_, output_note)| output_note.id()).collect();
1262
1263 if self.pending_objects.output_notes.len() > MAX_OUTPUT_NOTES_PER_BATCH {
1266 return Err(anyhow::anyhow!(
1267 "too many pending output notes: {}, max allowed: {MAX_OUTPUT_NOTES_PER_BATCH}",
1268 self.pending_objects.output_notes.len(),
1269 ));
1270 }
1271
1272 let mut pending_note_batch = Vec::with_capacity(self.pending_objects.output_notes.len());
1273 let pending_output_notes = core::mem::take(&mut self.pending_objects.output_notes);
1274 for (note_idx, output_note) in pending_output_notes.into_iter().enumerate() {
1275 if output_notes_block.contains(&output_note.id()) {
1276 return Err(anyhow::anyhow!(
1277 "output note {} is already created in block through transactions",
1278 output_note.id()
1279 ));
1280 }
1281
1282 pending_note_batch.push((note_idx, output_note));
1283 }
1284
1285 if (proven_block.output_note_batches().len() + 1) > MAX_BATCHES_PER_BLOCK {
1286 return Err(anyhow::anyhow!(
1287 "too many batches in block: cannot add more pending notes".to_string(),
1288 ));
1289 }
1290
1291 proven_block.output_note_batches_mut().push(pending_note_batch);
1292
1293 let updated_block_note_tree = proven_block.build_output_note_tree().root();
1294
1295 let block_header = proven_block.header();
1297 let updated_header = BlockHeader::new(
1298 block_header.version(),
1299 block_header.prev_block_commitment(),
1300 block_header.block_num(),
1301 block_header.chain_commitment(),
1302 self.account_tree.root(),
1303 self.nullifier_tree.root(),
1304 updated_block_note_tree,
1305 block_header.tx_commitment(),
1306 block_header.tx_kernel_commitment(),
1307 block_header.proof_commitment(),
1308 block_header.timestamp(),
1309 );
1310 proven_block.set_block_header(updated_header);
1311
1312 Ok(())
1313 }
1314
1315 fn prove_block_inner(&mut self, timestamp: Option<u32>) -> anyhow::Result<ProvenBlock> {
1336 let batches = self.pending_transactions_to_batches()?;
1340
1341 let block_timestamp =
1345 timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1346
1347 let proposed_block = self
1348 .propose_block_at(batches, block_timestamp)
1349 .context("failed to create proposed block")?;
1350 let mut proven_block = self.prove_block(proposed_block).context("failed to prove block")?;
1351
1352 self.apply_block_tree_updates(&proven_block)
1355 .context("failed to apply block tree updates")?;
1356
1357 if !self.pending_objects.is_empty() {
1358 self.apply_pending_objects_to_block(&mut proven_block)?;
1359 }
1360
1361 self.apply_block(proven_block.clone()).context("failed to apply block")?;
1362
1363 Ok(proven_block)
1364 }
1365}
1366
1367fn create_genesis_state(
1370 accounts: impl IntoIterator<Item = Account>,
1371) -> anyhow::Result<(ProvenBlock, AccountTree)> {
1372 let block_account_updates: Vec<BlockAccountUpdate> = accounts
1373 .into_iter()
1374 .map(|account| {
1375 BlockAccountUpdate::new(
1376 account.id(),
1377 account.commitment(),
1378 AccountUpdateDetails::New(account),
1379 )
1380 })
1381 .collect();
1382
1383 let account_tree = AccountTree::with_entries(
1384 block_account_updates
1385 .iter()
1386 .map(|account| (account.account_id(), account.final_state_commitment())),
1387 )
1388 .context("failed to create genesis account tree")?;
1389
1390 let output_note_batches = Vec::new();
1391 let created_nullifiers = Vec::new();
1392 let transactions = OrderedTransactionHeaders::new_unchecked(Vec::new());
1393
1394 let version = 0;
1395 let prev_block_commitment = Digest::default();
1396 let block_num = BlockNumber::from(0u32);
1397 let chain_commitment = Blockchain::new().commitment();
1398 let account_root = account_tree.root();
1399 let nullifier_root = NullifierTree::new().root();
1400 let note_root = BlockNoteTree::empty().root();
1401 let tx_commitment = transactions.commitment();
1402 let tx_kernel_commitment = TransactionKernel::kernel_commitment();
1403 let proof_commitment = Digest::default();
1404 let timestamp = MockChain::TIMESTAMP_START_SECS;
1405
1406 let header = BlockHeader::new(
1407 version,
1408 prev_block_commitment,
1409 block_num,
1410 chain_commitment,
1411 account_root,
1412 nullifier_root,
1413 note_root,
1414 tx_commitment,
1415 tx_kernel_commitment,
1416 proof_commitment,
1417 timestamp,
1418 );
1419
1420 Ok((
1421 ProvenBlock::new_unchecked(
1422 header,
1423 block_account_updates,
1424 output_note_batches,
1425 created_nullifiers,
1426 transactions,
1427 ),
1428 account_tree,
1429 ))
1430}
1431
1432impl Default for MockChain {
1433 fn default() -> Self {
1434 MockChain::new()
1435 }
1436}
1437
1438#[derive(Default, Debug, Clone)]
1443struct PendingObjects {
1444 updated_accounts: BTreeMap<AccountId, BlockAccountUpdate>,
1446
1447 output_notes: Vec<OutputNote>,
1449
1450 created_nullifiers: Vec<Nullifier>,
1452}
1453
1454impl PendingObjects {
1455 pub fn new() -> PendingObjects {
1456 PendingObjects {
1457 updated_accounts: BTreeMap::new(),
1458 output_notes: vec![],
1459 created_nullifiers: vec![],
1460 }
1461 }
1462
1463 pub fn is_empty(&self) -> bool {
1465 self.updated_accounts.is_empty()
1466 && self.output_notes.is_empty()
1467 && self.created_nullifiers.is_empty()
1468 }
1469}
1470
1471pub enum AccountState {
1477 New,
1478 Exists,
1479}
1480
1481#[derive(Debug, Clone)]
1487pub enum TxContextInput {
1488 AccountId(AccountId),
1489 Account(Account),
1490 ExecutedTransaction(Box<ExecutedTransaction>),
1491}
1492
1493impl From<AccountId> for TxContextInput {
1494 fn from(account: AccountId) -> Self {
1495 Self::AccountId(account)
1496 }
1497}
1498
1499impl From<Account> for TxContextInput {
1500 fn from(account: Account) -> Self {
1501 Self::Account(account)
1502 }
1503}
1504
1505impl From<ExecutedTransaction> for TxContextInput {
1506 fn from(tx: ExecutedTransaction) -> Self {
1507 Self::ExecutedTransaction(Box::new(tx))
1508 }
1509}
1510
1511#[cfg(test)]
1515mod tests {
1516 use miden_objects::{
1517 account::{AccountStorage, AccountStorageMode},
1518 asset::FungibleAsset,
1519 testing::{
1520 account_component::AccountMockComponent,
1521 account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_SENDER},
1522 },
1523 };
1524
1525 use super::*;
1526
1527 #[test]
1528 fn with_accounts() -> anyhow::Result<()> {
1529 let account = AccountBuilder::new([4; 32])
1530 .storage_mode(AccountStorageMode::Public)
1531 .with_auth_component(Auth::IncrNonce)
1532 .with_component(
1533 AccountMockComponent::new_with_slots(
1534 TransactionKernel::testing_assembler(),
1535 vec![AccountStorage::mock_item_2().slot],
1536 )
1537 .unwrap(),
1538 )
1539 .build_existing()?;
1540
1541 let mock_chain = MockChain::with_accounts(&[account.clone()])?;
1542
1543 assert_eq!(mock_chain.committed_account(account.id())?, &account);
1544
1545 let tx_context = mock_chain.build_tx_context(account.id(), &[], &[])?.build()?;
1548 assert_eq!(tx_context.tx_inputs().block_header().block_num(), BlockNumber::from(0u32));
1549 assert_eq!(
1550 tx_context.tx_inputs().block_header().account_root(),
1551 mock_chain.account_tree.root()
1552 );
1553
1554 Ok(())
1555 }
1556
1557 #[test]
1558 fn prove_until_block() -> anyhow::Result<()> {
1559 let mut chain = MockChain::new();
1560 let block = chain.prove_until_block(5)?;
1561 assert_eq!(block.header().block_num(), 5u32.into());
1562 assert_eq!(chain.proven_blocks().len(), 6);
1563
1564 Ok(())
1565 }
1566
1567 #[test]
1568 fn private_account_state_update() -> anyhow::Result<()> {
1569 let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1570 let account_builder = AccountBuilder::new([4; 32])
1571 .storage_mode(AccountStorageMode::Private)
1572 .with_component(BasicWallet);
1573
1574 let mut mock_chain = MockChain::new();
1575 let account = mock_chain.add_pending_account_from_builder(
1576 Auth::BasicAuth,
1577 account_builder,
1578 AccountState::New,
1579 )?;
1580 let account_id = account.id();
1581 assert_eq!(account.nonce().as_int(), 0);
1582
1583 let note_1 = mock_chain.add_pending_p2id_note(
1584 ACCOUNT_ID_SENDER.try_into().unwrap(),
1585 account.id(),
1586 &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1587 NoteType::Private,
1588 )?;
1589
1590 mock_chain.prove_next_block()?;
1591
1592 let tx = mock_chain
1593 .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1594 .build()?
1595 .execute()?;
1596
1597 mock_chain.add_pending_executed_transaction(&tx)?;
1598 mock_chain.prove_next_block()?;
1599
1600 assert!(tx.final_account().nonce().as_int() > 0);
1601 assert_eq!(
1602 tx.final_account().commitment(),
1603 mock_chain.account_tree.open(account_id).state_commitment()
1604 );
1605
1606 Ok(())
1607 }
1608}