1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use anyhow::Context;
5use miden_block_prover::LocalBlockProver;
6use miden_processor::serde::DeserializationError;
7use miden_protocol::account::auth::{AuthSecretKey, PublicKey};
8use miden_protocol::account::{Account, AccountId, AccountUpdateDetails, PartialAccount};
9use miden_protocol::batch::{ProposedBatch, ProvenBatch};
10use miden_protocol::block::account_tree::{AccountTree, AccountWitness};
11use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness};
12use miden_protocol::block::{
13 BlockHeader,
14 BlockInputs,
15 BlockNumber,
16 BlockSignatures,
17 Blockchain,
18 ProposedBlock,
19 ProvenBlock,
20 ValidatorKeys,
21};
22use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
23use miden_protocol::note::{Note, NoteHeader, NoteId, NoteInclusionProof, Nullifier};
24use miden_protocol::transaction::{
25 ExecutedTransaction,
26 InputNote,
27 InputNotes,
28 OutputNote,
29 PartialBlockchain,
30 ProvenTransaction,
31 TransactionInputs,
32};
33use miden_protocol::{MIN_PROOF_SECURITY_LEVEL, Word};
34use miden_tx::LocalTransactionProver;
35use miden_tx::auth::BasicAuthenticator;
36use miden_tx::utils::serde::{ByteReader, ByteWriter, Deserializable, Serializable};
37use miden_tx_batch::LocalBatchProver;
38
39use super::note::MockChainNote;
40use crate::{MockChainBuilder, MockTransactionBuilder, TransactionContextBuilder};
41
42#[derive(Debug, Clone)]
178pub struct MockChain {
179 chain: Blockchain,
181
182 blocks: Vec<ProvenBlock>,
184
185 nullifier_tree: NullifierTree,
187
188 account_tree: AccountTree,
190
191 pending_transactions: Vec<ProvenTransaction>,
194
195 pending_batches: Vec<ProvenBatch>,
197
198 committed_notes: BTreeMap<NoteId, MockChainNote>,
200
201 committed_accounts: BTreeMap<AccountId, Account>,
208
209 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
212
213 validator_secret_keys: Vec<SigningKey>,
215}
216
217impl MockChain {
218 pub const TIMESTAMP_START_SECS: u32 = 1700000000;
223
224 pub const TIMESTAMP_STEP_SECS: u32 = 10;
227
228 pub fn new() -> Self {
233 Self::builder().build().expect("empty chain should be valid")
234 }
235
236 pub fn builder() -> MockChainBuilder {
238 MockChainBuilder::new()
239 }
240
241 pub(super) fn from_genesis_block(
243 genesis_block: ProvenBlock,
244 account_tree: AccountTree,
245 account_authenticators: BTreeMap<AccountId, AccountAuthenticator>,
246 secret_keys: Vec<SigningKey>,
247 genesis_notes: Vec<Note>,
248 ) -> anyhow::Result<Self> {
249 let mut chain = MockChain {
250 chain: Blockchain::default(),
251 blocks: vec![],
252 nullifier_tree: NullifierTree::default(),
253 account_tree,
254 pending_transactions: Vec::new(),
255 pending_batches: Vec::new(),
256 committed_notes: BTreeMap::new(),
257 committed_accounts: BTreeMap::new(),
258 account_authenticators,
259 validator_secret_keys: secret_keys,
260 };
261
262 chain
265 .apply_block(genesis_block)
266 .context("failed to build account from builder")?;
267
268 for note in genesis_notes {
272 if let Some(MockChainNote::Private(_, _, _, inclusion_proof)) =
273 chain.committed_notes.get(¬e.id())
274 {
275 chain.committed_notes.insert(
276 note.id(),
277 MockChainNote::Public(note.clone(), inclusion_proof.clone()),
278 );
279 }
280 }
281
282 debug_assert_eq!(chain.blocks.len(), 1);
283 debug_assert_eq!(chain.committed_accounts.len(), chain.account_tree.num_accounts());
284
285 Ok(chain)
286 }
287
288 pub fn blockchain(&self) -> &Blockchain {
293 &self.chain
294 }
295
296 pub fn latest_partial_blockchain(&self) -> PartialBlockchain {
299 let block_headers =
302 self.blocks.iter().map(|b| b.header()).take(self.blocks.len() - 1).cloned();
303
304 PartialBlockchain::from_blockchain(&self.chain, block_headers)
305 .expect("blockchain should be valid by construction")
306 }
307
308 pub fn latest_selective_partial_blockchain(
314 &self,
315 reference_blocks: impl IntoIterator<Item = BlockNumber>,
316 ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
317 let latest_block_header = self.latest_block_header();
318
319 self.selective_partial_blockchain(latest_block_header.block_num(), reference_blocks)
320 }
321
322 pub fn selective_partial_blockchain(
328 &self,
329 reference_block: BlockNumber,
330 reference_blocks: impl IntoIterator<Item = BlockNumber>,
331 ) -> anyhow::Result<(BlockHeader, PartialBlockchain)> {
332 let reference_block_header = self.block_header(reference_block.as_usize());
333 let reference_blocks: BTreeSet<_> = reference_blocks.into_iter().collect();
336
337 let mut block_headers = Vec::new();
339
340 for block_ref_num in &reference_blocks {
341 let block_index = block_ref_num.as_usize();
342 let block = self
343 .blocks
344 .get(block_index)
345 .ok_or_else(|| anyhow::anyhow!("block {} not found in chain", block_ref_num))?;
346 let block_header = block.header().clone();
347 if block_header.commitment() != reference_block_header.commitment() {
349 block_headers.push(block_header);
350 }
351 }
352
353 let partial_blockchain =
354 PartialBlockchain::from_blockchain_at(&self.chain, reference_block, block_headers)?;
355
356 Ok((reference_block_header, partial_blockchain))
357 }
358
359 pub fn account_witnesses(
362 &self,
363 account_ids: impl IntoIterator<Item = AccountId>,
364 ) -> BTreeMap<AccountId, AccountWitness> {
365 let mut account_witnesses = BTreeMap::new();
366
367 for account_id in account_ids {
368 let witness = self.account_tree.open(account_id);
369 account_witnesses.insert(account_id, witness);
370 }
371
372 account_witnesses
373 }
374
375 pub fn nullifier_witnesses(
378 &self,
379 nullifiers: impl IntoIterator<Item = Nullifier>,
380 ) -> BTreeMap<Nullifier, NullifierWitness> {
381 let mut nullifier_proofs = BTreeMap::new();
382
383 for nullifier in nullifiers {
384 let witness = self.nullifier_tree.open(&nullifier);
385 nullifier_proofs.insert(nullifier, witness);
386 }
387
388 nullifier_proofs
389 }
390
391 pub fn unauthenticated_note_proofs(
395 &self,
396 notes: impl IntoIterator<Item = NoteId>,
397 ) -> BTreeMap<NoteId, NoteInclusionProof> {
398 let mut proofs = BTreeMap::default();
399 for note in notes {
400 if let Some(input_note) = self.committed_notes.get(¬e) {
401 proofs.insert(note, input_note.inclusion_proof().clone());
402 }
403 }
404
405 proofs
406 }
407
408 pub fn genesis_block_header(&self) -> BlockHeader {
410 self.block_header(BlockNumber::GENESIS.as_usize())
411 }
412
413 pub fn latest_block_header(&self) -> BlockHeader {
415 let chain_tip =
416 self.chain.chain_tip().expect("chain should contain at least the genesis block");
417 self.blocks[chain_tip.as_usize()].header().clone()
418 }
419
420 pub fn validator_keys(&self) -> ValidatorKeys {
422 ValidatorKeys::new(self.validator_secret_keys.iter().map(|sk| sk.public_key()).collect())
423 .expect("the mock chain holds distinct validator keys")
424 }
425
426 fn sign_block(&self, commitment: Word) -> BlockSignatures {
429 let signatures = self
430 .validator_keys()
431 .as_keys()
432 .iter()
433 .map(|key| {
434 let signer = self
435 .validator_secret_keys
436 .iter()
437 .find(|sk| &sk.public_key() == key)
438 .expect("a signer should exist for every validator key");
439 signer.sign(commitment)
440 })
441 .collect();
442 BlockSignatures::new(signatures).expect("signature count same as validator key count")
443 }
444
445 pub fn latest_block(&self) -> ProvenBlock {
447 let chain_tip =
448 self.chain.chain_tip().expect("chain should contain at least the genesis block");
449 self.blocks[chain_tip.as_usize()].clone()
450 }
451
452 pub fn block_header(&self, block_number: usize) -> BlockHeader {
458 self.blocks[block_number].header().clone()
459 }
460
461 pub fn proven_blocks(&self) -> &[ProvenBlock] {
463 &self.blocks
464 }
465
466 pub fn fee_faucet_id(&self) -> AccountId {
472 self.genesis_block_header().fee_parameters().fee_faucet_id()
473 }
474
475 pub fn nullifier_tree(&self) -> &NullifierTree {
477 &self.nullifier_tree
478 }
479
480 pub fn committed_notes(&self) -> &BTreeMap<NoteId, MockChainNote> {
484 &self.committed_notes
485 }
486
487 pub fn is_note_committed(&self, note_id: &NoteId) -> bool {
489 self.committed_notes.contains_key(note_id)
490 }
491
492 pub fn is_note_consumed(&self, nullifier: &Nullifier) -> bool {
494 self.nullifier_tree.get_block_num(nullifier).is_some()
495 }
496
497 pub fn is_note_unspent(&self, nullifier: &Nullifier) -> bool {
502 !self.is_note_consumed(nullifier)
503 }
504
505 pub fn get_public_note(&self, note_id: &NoteId) -> Option<InputNote> {
508 let note = self.committed_notes.get(note_id)?;
509 note.clone().try_into().ok()
510 }
511
512 pub fn committed_account(&self, account_id: AccountId) -> anyhow::Result<&Account> {
516 self.committed_accounts
517 .get(&account_id)
518 .with_context(|| format!("account {account_id} not found in committed accounts"))
519 }
520
521 pub fn account_tree(&self) -> &AccountTree {
523 &self.account_tree
524 }
525
526 pub fn propose_transaction_batch<I>(
533 &self,
534 txs: impl IntoIterator<Item = ProvenTransaction, IntoIter = I>,
535 ) -> anyhow::Result<ProposedBatch>
536 where
537 I: Iterator<Item = ProvenTransaction> + Clone,
538 {
539 let transactions: Vec<_> = txs.into_iter().map(alloc::sync::Arc::new).collect();
540
541 let (batch_reference_block, partial_blockchain, unauthenticated_note_proofs) = self
542 .get_batch_inputs(
543 transactions.iter().map(|tx| tx.ref_block_num()),
544 transactions
545 .iter()
546 .flat_map(|tx| tx.unauthenticated_notes().map(NoteHeader::id)),
547 )?;
548
549 Ok(ProposedBatch::new_unverified(
550 transactions,
551 batch_reference_block,
552 partial_blockchain,
553 unauthenticated_note_proofs,
554 )?)
555 }
556
557 pub fn prove_transaction_batch(
561 &self,
562 proposed_batch: ProposedBatch,
563 ) -> anyhow::Result<ProvenBatch> {
564 let batch_prover = LocalBatchProver::new();
565 Ok(batch_prover.prove_dummy(proposed_batch)?)
566 }
567
568 pub fn propose_block_at<I>(
575 &self,
576 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
577 timestamp: u32,
578 ) -> anyhow::Result<ProposedBlock>
579 where
580 I: Iterator<Item = ProvenBatch> + Clone,
581 {
582 let batches: Vec<_> = batches.into_iter().collect();
583
584 let block_inputs = self
585 .get_block_inputs(batches.iter())
586 .context("could not retrieve block inputs")?;
587
588 let proposed_block = ProposedBlock::new_at(block_inputs, batches, timestamp)
589 .context("failed to create proposed block")?;
590
591 Ok(proposed_block)
592 }
593
594 pub fn propose_block<I>(
598 &self,
599 batches: impl IntoIterator<Item = ProvenBatch, IntoIter = I>,
600 ) -> anyhow::Result<ProposedBlock>
601 where
602 I: Iterator<Item = ProvenBatch> + Clone,
603 {
604 let timestamp = self.latest_block_header().timestamp() + 1;
607
608 self.propose_block_at(batches, timestamp)
609 }
610
611 pub fn build_tx_context_at(
629 &self,
630 reference_block: impl Into<BlockNumber>,
631 input: impl Into<TxContextInput>,
632 note_ids: &[NoteId],
633 unauthenticated_notes: &[Note],
634 ) -> anyhow::Result<TransactionContextBuilder> {
635 let input = input.into();
636 let reference_block = reference_block.into();
637
638 let authenticator = self.account_authenticator(input.id());
639
640 anyhow::ensure!(
641 reference_block.as_usize() < self.blocks.len(),
642 "reference block {reference_block} is out of range (latest {})",
643 self.latest_block_header().block_num()
644 );
645
646 let account = self.resolve_tx_account(input)?;
647
648 let tx_inputs = self
649 .get_transaction_inputs_at(reference_block, &account, note_ids, unauthenticated_notes)
650 .context("failed to gather transaction inputs")?;
651
652 let tx_context_builder = TransactionContextBuilder::new(account)
653 .authenticator(authenticator)
654 .tx_inputs(tx_inputs);
655
656 Ok(tx_context_builder)
657 }
658
659 pub fn build_transaction(
666 &self,
667 input: impl Into<TxContextInput>,
668 ) -> MockTransactionBuilder<'_> {
669 MockTransactionBuilder::new(self, input)
670 }
671
672 pub(crate) fn resolve_tx_account(&self, input: TxContextInput) -> anyhow::Result<Account> {
677 match input {
678 TxContextInput::AccountId(account_id) => {
679 anyhow::ensure!(
680 !account_id.is_private(),
681 "transaction contexts for private accounts should be created with TxContextInput::Account"
682 );
683
684 self.committed_account(account_id).cloned()
685 },
686 TxContextInput::Account(account) => Ok(account),
687 }
688 }
689
690 pub(crate) fn account_authenticator(
692 &self,
693 account_id: AccountId,
694 ) -> Option<BasicAuthenticator> {
695 self.account_authenticators
696 .get(&account_id)
697 .and_then(|authenticator| authenticator.authenticator().cloned())
698 }
699
700 pub fn build_tx_context(
705 &self,
706 input: impl Into<TxContextInput>,
707 note_ids: &[NoteId],
708 unauthenticated_notes: &[Note],
709 ) -> anyhow::Result<TransactionContextBuilder> {
710 let reference_block = self.latest_block_header().block_num();
711 self.build_tx_context_at(reference_block, input, note_ids, unauthenticated_notes)
712 }
713
714 pub fn get_transaction_inputs_at(
720 &self,
721 reference_block: BlockNumber,
722 account: impl Into<PartialAccount>,
723 notes: &[NoteId],
724 unauthenticated_notes: &[Note],
725 ) -> anyhow::Result<TransactionInputs> {
726 let ref_block = self.block_header(reference_block.as_usize());
727
728 let mut input_notes = vec![];
729 let mut block_headers_map: BTreeMap<BlockNumber, BlockHeader> = BTreeMap::new();
730 for note in notes {
731 let input_note: InputNote = self
732 .committed_notes
733 .get(note)
734 .with_context(|| format!("note with id {note} not found"))?
735 .clone()
736 .try_into()
737 .with_context(|| {
738 format!("failed to convert mock chain note with id {note} into input note")
739 })?;
740
741 let note_block_num = input_note
742 .location()
743 .with_context(|| format!("note location not available: {note}"))?
744 .block_num();
745
746 if note_block_num > ref_block.block_num() {
747 anyhow::bail!(
748 "note with ID {note} was created in block {note_block_num} which is larger than the reference block number {}",
749 ref_block.block_num()
750 )
751 }
752
753 if note_block_num != ref_block.block_num() {
754 let block_header = self
755 .blocks
756 .get(note_block_num.as_usize())
757 .with_context(|| format!("block {note_block_num} not found in chain"))?
758 .header()
759 .clone();
760 block_headers_map.insert(note_block_num, block_header);
761 }
762
763 input_notes.push(input_note);
764 }
765
766 for note in unauthenticated_notes {
767 input_notes.push(InputNote::Unauthenticated { note: note.clone() })
768 }
769
770 let block_headers = block_headers_map.values();
771 let (_, partial_blockchain) = self.selective_partial_blockchain(
772 reference_block,
773 block_headers.map(BlockHeader::block_num),
774 )?;
775
776 let input_notes = InputNotes::new(input_notes)?;
777
778 Ok(TransactionInputs::new(
779 account.into(),
780 ref_block.clone(),
781 partial_blockchain,
782 input_notes,
783 )?)
784 }
785
786 pub fn get_transaction_inputs(
788 &self,
789 account: impl Into<PartialAccount>,
790 notes: &[NoteId],
791 unauthenticated_notes: &[Note],
792 ) -> anyhow::Result<TransactionInputs> {
793 let latest_block_num = self.latest_block_header().block_num();
794 self.get_transaction_inputs_at(latest_block_num, account, notes, unauthenticated_notes)
795 }
796
797 pub fn get_batch_inputs(
800 &self,
801 tx_reference_blocks: impl IntoIterator<Item = BlockNumber>,
802 unauthenticated_notes: impl Iterator<Item = NoteId>,
803 ) -> anyhow::Result<(BlockHeader, PartialBlockchain, BTreeMap<NoteId, NoteInclusionProof>)>
804 {
805 let unauthenticated_note_proofs = self.unauthenticated_note_proofs(unauthenticated_notes);
807
808 let required_blocks = tx_reference_blocks.into_iter().chain(
811 unauthenticated_note_proofs
812 .values()
813 .map(|note_proof| note_proof.location().block_num()),
814 );
815
816 let (batch_reference_block, partial_block_chain) =
817 self.latest_selective_partial_blockchain(required_blocks)?;
818
819 Ok((batch_reference_block, partial_block_chain, unauthenticated_note_proofs))
820 }
821
822 pub fn get_foreign_account_inputs(
826 &self,
827 account_id: AccountId,
828 ) -> anyhow::Result<(Account, AccountWitness)> {
829 let account = self.committed_account(account_id)?.clone();
830
831 let account_witness = self.account_tree().open(account_id);
832 assert_eq!(account_witness.state_commitment(), account.to_commitment());
833
834 Ok((account, account_witness))
835 }
836
837 pub fn get_block_inputs<'batch, I>(
839 &self,
840 batch_iter: impl IntoIterator<Item = &'batch ProvenBatch, IntoIter = I>,
841 ) -> anyhow::Result<BlockInputs>
842 where
843 I: Iterator<Item = &'batch ProvenBatch> + Clone,
844 {
845 let batch_iterator = batch_iter.into_iter();
846
847 let unauthenticated_note_proofs =
848 self.unauthenticated_note_proofs(batch_iterator.clone().flat_map(|batch| {
849 batch.input_notes().iter().filter_map(|note| note.header().map(NoteHeader::id))
850 }));
851
852 let (block_reference_block, partial_blockchain) = self
853 .latest_selective_partial_blockchain(
854 batch_iterator.clone().map(ProvenBatch::reference_block_num).chain(
855 unauthenticated_note_proofs.values().map(|proof| proof.location().block_num()),
856 ),
857 )?;
858
859 let account_witnesses =
860 self.account_witnesses(batch_iterator.clone().flat_map(ProvenBatch::updated_accounts));
861
862 let nullifier_proofs =
863 self.nullifier_witnesses(batch_iterator.flat_map(ProvenBatch::created_nullifiers));
864
865 Ok(BlockInputs::new(
866 block_reference_block,
867 partial_blockchain,
868 account_witnesses,
869 nullifier_proofs,
870 unauthenticated_note_proofs,
871 ))
872 }
873
874 pub fn prove_next_block(&mut self) -> anyhow::Result<ProvenBlock> {
881 self.prove_and_apply_block(None, None)
882 }
883
884 pub fn prove_next_block_with_validator_keys_rotation(
893 &mut self,
894 new_validator_keys: Vec<SigningKey>,
895 ) -> anyhow::Result<ProvenBlock> {
896 let next_keys =
897 ValidatorKeys::new(new_validator_keys.iter().map(|sk| sk.public_key()).collect())
898 .context("invalid rotated validator key set")?;
899 let block = self.prove_and_apply_block(None, Some(next_keys))?;
900 self.validator_secret_keys = new_validator_keys;
901 Ok(block)
902 }
903
904 pub fn prove_next_block_at(&mut self, timestamp: u32) -> anyhow::Result<ProvenBlock> {
908 self.prove_and_apply_block(Some(timestamp), None)
909 }
910
911 pub fn prove_until_block(
921 &mut self,
922 target_block_num: impl Into<BlockNumber>,
923 ) -> anyhow::Result<ProvenBlock> {
924 let target_block_num = target_block_num.into();
925 let latest_block_num = self.latest_block_header().block_num();
926 assert!(
927 target_block_num > latest_block_num,
928 "target block number must be greater than the number of the latest block in the chain"
929 );
930
931 let mut last_block = None;
932 for _ in latest_block_num.as_usize()..target_block_num.as_usize() {
933 last_block = Some(self.prove_next_block()?);
934 }
935
936 Ok(last_block.expect("at least one block should have been created"))
937 }
938
939 pub fn add_pending_executed_transaction(
947 &mut self,
948 transaction: &ExecutedTransaction,
949 ) -> anyhow::Result<()> {
950 let proven_tx = LocalTransactionProver::default()
952 .prove_dummy(transaction.clone())
953 .context("failed to dummy-prove executed transaction into proven transaction")?;
954
955 self.pending_transactions.push(proven_tx);
956
957 Ok(())
958 }
959
960 pub fn add_pending_proven_transaction(&mut self, transaction: ProvenTransaction) {
965 self.pending_transactions.push(transaction);
966 }
967
968 pub fn add_pending_batch(&mut self, batch: ProvenBatch) {
973 self.pending_batches.push(batch);
974 }
975
976 fn apply_block(&mut self, proven_block: ProvenBlock) -> anyhow::Result<()> {
987 if proven_block.header().block_num() != BlockNumber::GENESIS {
990 let parent = self.latest_block_header();
991 proven_block
992 .validate(Some(&parent))
993 .context("block failed validation against its parent")?;
994 }
995
996 for account_update in proven_block.body().updated_accounts() {
997 self.account_tree
998 .insert(account_update.account_id(), account_update.final_state_commitment())
999 .context("failed to insert account update into account tree")?;
1000 }
1001
1002 for nullifier in proven_block.body().created_nullifiers() {
1003 self.nullifier_tree
1004 .mark_spent(*nullifier, proven_block.header().block_num())
1005 .context("failed to mark block nullifier as spent")?;
1006
1007 }
1011
1012 for account_update in proven_block.body().updated_accounts() {
1013 match account_update.details() {
1014 AccountUpdateDetails::Public(account_patch) => {
1015 if account_patch.is_full_state() {
1016 let account = Account::try_from(account_patch)
1017 .context("failed to convert full state patch into full account")?;
1018 self.committed_accounts.insert(account.id(), account.clone());
1019 } else {
1020 let committed_account = self
1021 .committed_accounts
1022 .get_mut(&account_update.account_id())
1023 .ok_or_else(|| {
1024 anyhow::anyhow!("account patch in block for non-existent account")
1025 })?;
1026 committed_account
1027 .apply_patch(account_patch)
1028 .context("failed to apply account patch")?;
1029 }
1030 },
1031 AccountUpdateDetails::Private => {},
1034 }
1035 }
1036
1037 let notes_tree = proven_block.body().compute_block_note_tree();
1038 for (block_note_index, created_note) in proven_block.body().output_notes() {
1039 let note_path = notes_tree.open(block_note_index);
1040 let note_inclusion_proof = NoteInclusionProof::new(
1041 proven_block.header().block_num(),
1042 block_note_index.leaf_index_value(),
1043 note_path,
1044 )
1045 .context("failed to create inclusion proof for output note")?;
1046
1047 match created_note {
1048 OutputNote::Public(public_note) => {
1049 self.committed_notes.insert(
1050 public_note.id(),
1051 MockChainNote::Public(public_note.as_note().clone(), note_inclusion_proof),
1052 );
1053 },
1054 OutputNote::Private(private_note) => {
1055 self.committed_notes.insert(
1056 private_note.id(),
1057 MockChainNote::Private(
1058 private_note.id(),
1059 *private_note.metadata(),
1060 private_note.attachments().clone(),
1061 note_inclusion_proof,
1062 ),
1063 );
1064 },
1065 }
1066 }
1067
1068 debug_assert_eq!(
1069 self.chain.commitment(),
1070 proven_block.header().chain_commitment(),
1071 "current mock chain commitment and new block's chain commitment should match"
1072 );
1073 debug_assert_eq!(
1074 BlockNumber::from(self.chain.as_mmr().forest().num_leaves() as u32),
1075 proven_block.header().block_num(),
1076 "current mock chain length and new block's number should match"
1077 );
1078
1079 self.chain.push(proven_block.header().commitment());
1080 self.blocks.push(proven_block);
1081
1082 Ok(())
1083 }
1084
1085 fn pending_transactions_to_batches(&mut self) -> anyhow::Result<Vec<ProvenBatch>> {
1086 if self.pending_transactions.is_empty() {
1089 return Ok(vec![]);
1090 }
1091
1092 let pending_transactions = core::mem::take(&mut self.pending_transactions);
1093
1094 let proposed_batch = self.propose_transaction_batch(pending_transactions)?;
1097 let proven_batch = self.prove_transaction_batch(proposed_batch)?;
1098
1099 Ok(vec![proven_batch])
1100 }
1101
1102 fn prove_and_apply_block(
1112 &mut self,
1113 timestamp: Option<u32>,
1114 next_validator_keys: Option<ValidatorKeys>,
1115 ) -> anyhow::Result<ProvenBlock> {
1116 let mut batches = self.pending_transactions_to_batches()?;
1120 batches.extend(core::mem::take(&mut self.pending_batches));
1121
1122 let block_timestamp =
1126 timestamp.unwrap_or(self.latest_block_header().timestamp() + Self::TIMESTAMP_STEP_SECS);
1127
1128 let mut proposed_block = self
1129 .propose_block_at(batches.clone(), block_timestamp)
1130 .context("failed to create proposed block")?;
1131
1132 if let Some(next_validator_keys) = next_validator_keys {
1134 proposed_block = proposed_block.with_next_validator_keys(next_validator_keys);
1135 }
1136
1137 let proven_block = self.prove_block(proposed_block.clone())?;
1138
1139 self.apply_block(proven_block.clone()).context("failed to apply block")?;
1143
1144 Ok(proven_block)
1145 }
1146
1147 pub fn prove_block(&self, proposed_block: ProposedBlock) -> anyhow::Result<ProvenBlock> {
1149 let (header, body) = proposed_block.clone().into_header_and_body()?;
1150 let inputs = self.get_block_inputs(proposed_block.batches().as_slice())?;
1151 let block_proof = LocalBlockProver::new(MIN_PROOF_SECURITY_LEVEL).prove_dummy(
1152 proposed_block.batches().clone(),
1153 header.clone(),
1154 inputs,
1155 )?;
1156 let signatures = self.sign_block(header.commitment());
1157 Ok(ProvenBlock::new_unchecked(header, body, signatures, block_proof))
1158 }
1159}
1160
1161impl Default for MockChain {
1162 fn default() -> Self {
1163 MockChain::new()
1164 }
1165}
1166
1167impl Serializable for MockChain {
1171 fn write_into<W: ByteWriter>(&self, target: &mut W) {
1172 self.chain.write_into(target);
1173 self.blocks.write_into(target);
1174 self.nullifier_tree.write_into(target);
1175 self.account_tree.write_into(target);
1176 self.pending_transactions.write_into(target);
1177 self.committed_accounts.write_into(target);
1178 self.committed_notes.write_into(target);
1179 self.account_authenticators.write_into(target);
1180 self.validator_secret_keys.write_into(target);
1181 }
1182}
1183
1184impl Deserializable for MockChain {
1185 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1186 let chain = Blockchain::read_from(source)?;
1187 let blocks = Vec::<ProvenBlock>::read_from(source)?;
1188 let nullifier_tree = NullifierTree::read_from(source)?;
1189 let account_tree = AccountTree::read_from(source)?;
1190 let pending_transactions = Vec::<ProvenTransaction>::read_from(source)?;
1191 let committed_accounts = BTreeMap::<AccountId, Account>::read_from(source)?;
1192 let committed_notes = BTreeMap::<NoteId, MockChainNote>::read_from(source)?;
1193 let account_authenticators =
1194 BTreeMap::<AccountId, AccountAuthenticator>::read_from(source)?;
1195 let secret_keys = Vec::<SigningKey>::read_from(source)?;
1196
1197 Ok(Self {
1198 chain,
1199 blocks,
1200 nullifier_tree,
1201 account_tree,
1202 pending_transactions,
1203 pending_batches: Vec::new(),
1204 committed_notes,
1205 committed_accounts,
1206 account_authenticators,
1207 validator_secret_keys: secret_keys,
1208 })
1209 }
1210}
1211
1212pub enum AccountState {
1218 New,
1219 Exists,
1220}
1221
1222#[derive(Debug, Clone)]
1227pub(super) struct AccountAuthenticator {
1228 authenticator: Option<BasicAuthenticator>,
1229}
1230
1231impl AccountAuthenticator {
1232 pub fn new(authenticator: Option<BasicAuthenticator>) -> Self {
1233 Self { authenticator }
1234 }
1235
1236 pub fn authenticator(&self) -> Option<&BasicAuthenticator> {
1237 self.authenticator.as_ref()
1238 }
1239}
1240
1241impl PartialEq for AccountAuthenticator {
1242 fn eq(&self, other: &Self) -> bool {
1243 match (&self.authenticator, &other.authenticator) {
1244 (Some(a), Some(b)) => {
1245 a.keys().keys().zip(b.keys().keys()).all(|(a_key, b_key)| a_key == b_key)
1246 },
1247 (None, None) => true,
1248 _ => false,
1249 }
1250 }
1251}
1252
1253impl Serializable for AccountAuthenticator {
1257 fn write_into<W: ByteWriter>(&self, target: &mut W) {
1258 self.authenticator
1259 .as_ref()
1260 .map(|auth| {
1261 auth.keys()
1262 .values()
1263 .map(|(secret_key, public_key)| (secret_key, public_key.as_ref().clone()))
1264 .collect::<Vec<_>>()
1265 })
1266 .write_into(target);
1267 }
1268}
1269
1270impl Deserializable for AccountAuthenticator {
1271 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
1272 let authenticator = Option::<Vec<(AuthSecretKey, PublicKey)>>::read_from(source)?;
1273
1274 let authenticator = authenticator.map(|keys| BasicAuthenticator::from_key_pairs(&keys));
1275
1276 Ok(Self { authenticator })
1277 }
1278}
1279
1280#[allow(clippy::large_enum_variant)]
1286#[derive(Debug, Clone)]
1287pub enum TxContextInput {
1288 AccountId(AccountId),
1289 Account(Account),
1290}
1291
1292impl TxContextInput {
1293 pub(crate) fn id(&self) -> AccountId {
1295 match self {
1296 TxContextInput::AccountId(account_id) => *account_id,
1297 TxContextInput::Account(account) => account.id(),
1298 }
1299 }
1300}
1301
1302impl From<AccountId> for TxContextInput {
1303 fn from(account: AccountId) -> Self {
1304 Self::AccountId(account)
1305 }
1306}
1307
1308impl From<Account> for TxContextInput {
1309 fn from(account: Account) -> Self {
1310 Self::Account(account)
1311 }
1312}
1313
1314#[cfg(test)]
1318mod tests {
1319 use miden_protocol::account::auth::AuthScheme;
1320 use miden_protocol::account::{AccountBuilder, AccountType};
1321 use miden_protocol::asset::{Asset, FungibleAsset};
1322 use miden_protocol::note::NoteType;
1323 use miden_protocol::testing::account_id::{
1324 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
1325 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
1326 ACCOUNT_ID_SENDER,
1327 };
1328 use miden_protocol::testing::random_secret_key::random_secret_key;
1329 use miden_standards::account::wallets::BasicWallet;
1330
1331 use super::*;
1332 use crate::Auth;
1333
1334 #[test]
1335 fn prove_until_block() -> anyhow::Result<()> {
1336 let mut chain = MockChain::new();
1337 let block = chain.prove_until_block(5)?;
1338 assert_eq!(block.header().block_num(), 5u32.into());
1339 assert_eq!(chain.proven_blocks().len(), 6);
1340
1341 Ok(())
1342 }
1343
1344 #[test]
1345 fn validator_keys_rotation_across_blocks() -> anyhow::Result<()> {
1346 let mut chain = MockChain::new();
1347 let original_keys = chain.validator_keys();
1348
1349 chain.prove_next_block()?;
1352 chain.prove_next_block()?;
1353 assert_eq!(chain.validator_keys(), original_keys);
1354
1355 let new_signers: Vec<SigningKey> = (0..4).map(|_| random_secret_key()).collect();
1357 let new_keys =
1358 ValidatorKeys::new(new_signers.iter().map(|sk| sk.public_key()).collect()).unwrap();
1359 let rotation_block = chain.prove_next_block_with_validator_keys_rotation(new_signers)?;
1360
1361 assert_eq!(rotation_block.header().validator_keys(), &new_keys);
1364 assert_eq!(chain.validator_keys(), new_keys);
1365
1366 chain.prove_next_block()?;
1369 assert_eq!(chain.validator_keys(), new_keys);
1370
1371 Ok(())
1372 }
1373
1374 #[test]
1375 fn proposed_block_serialization_round_trip() -> anyhow::Result<()> {
1376 let chain = MockChain::new();
1377 let timestamp = chain.latest_block_header().timestamp() + 1;
1378 let next_keys = ValidatorKeys::new(alloc::vec![random_secret_key().public_key()]).unwrap();
1379 let proposed = chain
1380 .propose_block_at(Vec::<ProvenBatch>::new(), timestamp)?
1381 .with_next_validator_keys(next_keys.clone());
1382
1383 let bytes = proposed.to_bytes();
1384 let deserialized = ProposedBlock::read_from_bytes(&bytes).unwrap();
1385
1386 assert_eq!(deserialized.to_bytes(), bytes);
1389 assert_eq!(deserialized.next_validator_keys(), &next_keys);
1390
1391 Ok(())
1392 }
1393
1394 #[tokio::test]
1395 async fn private_account_state_update() -> anyhow::Result<()> {
1396 let faucet_id = ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into()?;
1397 let account_builder = AccountBuilder::new([4; 32])
1398 .account_type(AccountType::Private)
1399 .with_component(BasicWallet);
1400
1401 let mut builder = MockChain::builder();
1402 let auth_scheme = AuthScheme::EcdsaK256Keccak;
1403 let account = builder.add_account_from_builder(
1404 Auth::BasicAuth { auth_scheme },
1405 account_builder,
1406 AccountState::New,
1407 )?;
1408
1409 let account_id = account.id();
1410 assert_eq!(account.nonce().as_canonical_u64(), 0);
1411
1412 let note_1 = builder.add_p2id_note(
1413 ACCOUNT_ID_SENDER.try_into().unwrap(),
1414 account.id(),
1415 &[Asset::Fungible(FungibleAsset::new(faucet_id, 1000u64).unwrap())],
1416 NoteType::Private,
1417 )?;
1418
1419 let mut mock_chain = builder.build()?;
1420 mock_chain.prove_next_block()?;
1421
1422 let tx = mock_chain
1423 .build_tx_context(TxContextInput::Account(account), &[], &[note_1])?
1424 .build()?
1425 .execute()
1426 .await?;
1427
1428 mock_chain.add_pending_executed_transaction(&tx)?;
1429 mock_chain.prove_next_block()?;
1430
1431 assert!(tx.final_account().nonce().as_canonical_u64() > 0);
1432 assert_eq!(
1433 tx.final_account().to_commitment(),
1434 mock_chain.account_tree.open(account_id).state_commitment()
1435 );
1436
1437 Ok(())
1438 }
1439
1440 #[tokio::test]
1441 async fn mock_chain_serialization() {
1442 let mut builder = MockChain::builder();
1443
1444 let mut notes = vec![];
1445 for i in 0..10 {
1446 let account = builder
1447 .add_account_from_builder(
1448 Auth::BasicAuth {
1449 auth_scheme: AuthScheme::Falcon512Poseidon2,
1450 },
1451 AccountBuilder::new([i; 32]).with_component(BasicWallet),
1452 AccountState::New,
1453 )
1454 .unwrap();
1455 let note = builder
1456 .add_p2id_note(
1457 ACCOUNT_ID_SENDER.try_into().unwrap(),
1458 account.id(),
1459 &[Asset::Fungible(
1460 FungibleAsset::new(
1461 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into().unwrap(),
1462 1000u64,
1463 )
1464 .unwrap(),
1465 )],
1466 NoteType::Private,
1467 )
1468 .unwrap();
1469 notes.push((account, note));
1470 }
1471
1472 let mut chain = builder.build().unwrap();
1473 for (account, note) in notes {
1474 let tx = chain
1475 .build_tx_context(TxContextInput::Account(account), &[], &[note])
1476 .unwrap()
1477 .build()
1478 .unwrap()
1479 .execute()
1480 .await
1481 .unwrap();
1482 chain.add_pending_executed_transaction(&tx).unwrap();
1483 chain.prove_next_block().unwrap();
1484 }
1485
1486 let bytes = chain.to_bytes();
1487
1488 let deserialized = MockChain::read_from_bytes(&bytes).unwrap();
1489
1490 assert_eq!(chain.chain.as_mmr().peaks(), deserialized.chain.as_mmr().peaks());
1491 assert_eq!(chain.blocks, deserialized.blocks);
1492 assert_eq!(chain.nullifier_tree, deserialized.nullifier_tree);
1493 assert_eq!(chain.account_tree, deserialized.account_tree);
1494 assert_eq!(chain.pending_transactions, deserialized.pending_transactions);
1495 assert_eq!(chain.committed_accounts, deserialized.committed_accounts);
1496 assert_eq!(chain.committed_notes, deserialized.committed_notes);
1497 assert_eq!(chain.account_authenticators, deserialized.account_authenticators);
1498 }
1499
1500 #[test]
1501 fn mock_chain_block_signatures() -> anyhow::Result<()> {
1502 let mut builder = MockChain::builder();
1503 builder.add_existing_mock_account(Auth::IncrNonce)?;
1504 let mut chain = builder.build()?;
1505
1506 let genesis_block = chain.latest_block();
1509 let genesis_validator_keys = genesis_block.header().validator_keys().clone();
1510 genesis_block
1511 .signatures()
1512 .verify_against(genesis_block.header().commitment(), &genesis_validator_keys)
1513 .unwrap();
1514
1515 chain.prove_next_block()?;
1517
1518 let next_block = chain.latest_block();
1521 next_block
1522 .signatures()
1523 .verify_against(next_block.header().commitment(), &genesis_validator_keys)
1524 .unwrap();
1525
1526 assert_eq!(next_block.header().validator_keys(), &genesis_validator_keys);
1529
1530 Ok(())
1531 }
1532
1533 #[tokio::test]
1534 async fn add_pending_batch() -> anyhow::Result<()> {
1535 let mut builder = MockChain::builder();
1536 let account = builder.add_existing_mock_account(Auth::IncrNonce)?;
1537 let mut chain = builder.build()?;
1538
1539 let tx = chain.build_tx_context(account.id(), &[], &[])?.build()?.execute().await?;
1541 let proven_tx = LocalTransactionProver::default().prove_dummy(tx)?;
1542 let proposed_batch = chain.propose_transaction_batch(vec![proven_tx])?;
1543 let proven_batch = chain.prove_transaction_batch(proposed_batch)?;
1544
1545 let num_blocks_before = chain.proven_blocks().len();
1547 chain.add_pending_batch(proven_batch);
1548 chain.prove_next_block()?;
1549
1550 assert_eq!(chain.proven_blocks().len(), num_blocks_before + 1);
1551
1552 Ok(())
1553 }
1554}