1mod epoch_state;
16pub mod store;
17pub mod transaction_executor;
18
19use std::{
20 collections::HashMap,
21 num::NonZeroUsize,
22 path::PathBuf,
23 sync::{Arc, RwLock},
24};
25
26use anyhow::{Result, anyhow};
27use fastcrypto::traits::Signer;
28use iota_config::{
29 genesis, transaction_deny_config::TransactionDenyConfig,
30 verifier_signing_config::VerifierSigningConfig,
31};
32use iota_node_storage::{GrpcIndexes, GrpcStateReader};
33use iota_protocol_config::ProtocolVersion;
34use iota_sdk_types::{
35 Address, CheckpointContentsDigest, CheckpointDigest, ConsensusCommitDigest,
36 EndOfEpochTransactionKind, GasPayment, ObjectId, StructTag, SystemPackage, Transaction,
37 TransactionDigest, TransactionEffects, TransactionEvents, TransactionKind,
38 checkpoint::{CheckpointContents, EndOfEpochData},
39};
40use iota_storage::blob::{Blob, BlobEncoding};
41use iota_swarm_config::{
42 genesis_config::AccountConfig, network_config::NetworkConfig,
43 network_config_builder::ConfigBuilder,
44};
45use iota_types::{
46 base_types::{AuthorityName, EpochId, VersionNumber},
47 committee::Committee,
48 crypto::AuthoritySignature,
49 error::ExecutionError,
50 gas_coin::{GasCoin, NANOS_PER_IOTA},
51 inner_temporary_store::InnerTemporaryStore,
52 iota_system_state::{
53 IotaSystemState, IotaSystemStateTrait, epoch_start_iota_system_state::EpochStartSystemState,
54 },
55 messages_checkpoint::{CheckpointContentsExt, CheckpointSequenceNumber, VerifiedCheckpoint},
56 mock_checkpoint_builder::{MockCheckpointBuilder, ValidatorKeypairProvider},
57 object::Object,
58 programmable_transaction_builder::ProgrammableTransactionBuilder,
59 signature::VerifyParams,
60 storage::{EpochInfoV2, ObjectStore, ReadStore, TransactionInfo},
61 transaction::{TransactionAPI, TransactionEnvelope, VerifiedTransaction},
62};
63use rand::rngs::OsRng;
64
65pub use self::store::{SimulatorStore, in_mem_store::InMemoryStore};
66use self::{epoch_state::EpochState, store::in_mem_store::KeyStore};
67
68pub struct Simulacrum<R = OsRng, Store: SimulatorStore = InMemoryStore> {
78 inner: RwLock<SimulacrumInner<R, Store>>,
80 deny_config: TransactionDenyConfig,
82 verifier_signing_config: VerifierSigningConfig,
83}
84
85struct SimulacrumInner<R, Store: SimulatorStore> {
86 rng: R,
87 keystore: KeyStore,
88 #[expect(unused)]
89 genesis: genesis::Genesis,
90 store: Store,
91 checkpoint_builder: MockCheckpointBuilder,
92
93 epoch_state: EpochState,
95 data_ingestion_path: Option<PathBuf>,
96}
97
98impl Simulacrum {
99 #[expect(clippy::new_without_default)]
102 pub fn new() -> Self {
103 Self::new_with_rng(OsRng)
104 }
105}
106
107impl<R> Simulacrum<R>
108where
109 R: rand::RngCore + rand::CryptoRng,
110{
111 pub fn new_with_rng(mut rng: R) -> Self {
126 let config = ConfigBuilder::new_with_temp_dir()
127 .rng(&mut rng)
128 .with_chain_start_timestamp_ms(1)
129 .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
130 .build();
131 Self::new_with_network_config_in_mem(&config, rng)
132 }
133
134 pub fn new_with_protocol_version_and_accounts(
135 mut rng: R,
136 chain_start_timestamp_ms: u64,
137 protocol_version: ProtocolVersion,
138 account_configs: Vec<AccountConfig>,
139 ) -> Self {
140 let config = ConfigBuilder::new_with_temp_dir()
141 .rng(&mut rng)
142 .with_chain_start_timestamp_ms(chain_start_timestamp_ms)
143 .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
144 .with_protocol_version(protocol_version)
145 .with_accounts(account_configs)
146 .build();
147 Self::new_with_network_config_in_mem(&config, rng)
148 }
149
150 fn new_with_network_config_in_mem(config: &NetworkConfig, rng: R) -> Self {
151 let store = InMemoryStore::new(&config.genesis);
152 Self::new_with_network_config_store(config, rng, store)
153 }
154}
155
156impl<R, S: store::SimulatorStore> Simulacrum<R, S> {
157 pub fn new_with_network_config_store(config: &NetworkConfig, rng: R, store: S) -> Self {
158 let keystore = KeyStore::from_network_config(config);
159 let checkpoint_builder = MockCheckpointBuilder::new(config.genesis.checkpoint());
160
161 let genesis = &config.genesis;
162 let epoch_state = EpochState::new(genesis.iota_system_object());
163
164 Self {
165 deny_config: TransactionDenyConfig::default(),
166 verifier_signing_config: VerifierSigningConfig::default(),
167 inner: RwLock::new(SimulacrumInner {
168 rng,
169 keystore,
170 genesis: genesis.clone(),
171 store,
172 checkpoint_builder,
173 epoch_state,
174 data_ingestion_path: None,
175 }),
176 }
177 }
178
179 pub fn execute_transaction(
193 &self,
194 transaction: TransactionEnvelope,
195 ) -> anyhow::Result<(TransactionEffects, Option<ExecutionError>)> {
196 let mut inner = self.inner.write().unwrap();
197 let transaction = transaction.try_into_verified_for_testing(&VerifyParams::default())?;
198
199 let (inner_temporary_store, _, effects, execution_error_opt) =
200 inner.epoch_state.execute_transaction(
201 &inner.store,
202 &self.deny_config,
203 &self.verifier_signing_config,
204 &transaction,
205 )?;
206
207 let InnerTemporaryStore {
208 written, events, ..
209 } = inner_temporary_store;
210
211 inner.store.insert_executed_transaction(
212 transaction.clone(),
213 effects.clone(),
214 events,
215 written,
216 );
217
218 inner
220 .checkpoint_builder
221 .push_transaction(transaction, effects.clone());
222 Ok((effects, execution_error_opt.err()))
223 }
224
225 pub fn simulate_transaction(
228 &self,
229 transaction: Transaction,
230 checks: iota_types::transaction_executor::VmChecks,
231 ) -> iota_types::error::IotaResult<iota_types::transaction_executor::SimulateTransactionResult>
232 {
233 let inner = self.inner.read().unwrap();
234 inner.epoch_state.simulate_transaction(
235 &inner.store,
236 &self.deny_config,
237 &self.verifier_signing_config,
238 transaction,
239 checks,
240 )
241 }
242
243 pub fn create_checkpoint(&self) -> VerifiedCheckpoint {
246 let (checkpoint, contents) = {
247 let mut inner = self.inner.write().unwrap();
248 let committee = CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
249 let timestamp_ms = inner.store.get_clock().timestamp_ms();
250 let (checkpoint, contents, _) =
251 inner.checkpoint_builder.build(&committee, timestamp_ms);
252 inner.store.insert_checkpoint(checkpoint.clone());
253 inner.store.insert_checkpoint_contents(contents.clone());
254 (checkpoint, contents)
255 };
256 self.process_data_ingestion(checkpoint.clone(), contents)
258 .unwrap();
259 checkpoint
260 }
261
262 pub fn advance_clock(&self, duration: std::time::Duration) -> TransactionEffects {
267 let mut inner = self.inner.write().unwrap();
268 let epoch = inner.epoch_state.epoch();
269 let round = inner.epoch_state.next_consensus_round();
270 let timestamp_ms = inner.store.get_clock().timestamp_ms() + duration.as_millis() as u64;
271 drop(inner);
272
273 let consensus_commit_prologue_transaction =
274 VerifiedTransaction::new_consensus_commit_prologue_v1(
275 epoch,
276 round,
277 timestamp_ms,
278 ConsensusCommitDigest::default(),
279 Vec::new(),
280 );
281
282 self.execute_transaction(consensus_commit_prologue_transaction.into())
283 .expect("advancing the clock cannot fail")
284 .0
285 }
286
287 pub fn advance_epoch(&self, create_deny_rules_object: bool) -> TransactionEffects {
307 let inner = self.inner.read().unwrap();
308 let current_epoch = inner.epoch_state.epoch();
309 let next_epoch = current_epoch + 1;
310 let next_epoch_protocol_version = inner.epoch_state.protocol_version();
311 let gas_cost_summary = inner
312 .checkpoint_builder
313 .epoch_rolling_gas_cost_summary()
314 .clone();
315 let epoch_start_timestamp_ms = inner.store.get_clock().timestamp_ms();
316 let pass_validator_scores = inner
317 .epoch_state
318 .protocol_config()
319 .pass_validator_scores_to_advance_epoch();
320 let adjust_rewards_by_score = inner
321 .epoch_state
322 .protocol_config()
323 .adjust_rewards_by_score();
324 let committee_size = inner.epoch_state.committee().num_members();
325 drop(inner);
326
327 const MAX_SCORE: u64 = u16::MAX as u64 + 1;
331 let scores = vec![MAX_SCORE; committee_size];
332
333 let next_epoch_system_package_bytes: Vec<SystemPackage> = vec![];
334 let mut kinds = Vec::new();
335 if create_deny_rules_object {
336 kinds.push(EndOfEpochTransactionKind::TransactionDenyRulesCreate);
337 }
338 kinds.push(if pass_validator_scores {
341 EndOfEpochTransactionKind::new_change_epoch_v4(
342 next_epoch,
343 next_epoch_protocol_version.as_u64(),
344 gas_cost_summary.storage_cost,
345 gas_cost_summary.computation_cost,
346 gas_cost_summary.computation_cost_burned,
347 gas_cost_summary.storage_rebate,
348 gas_cost_summary.non_refundable_storage_fee,
349 epoch_start_timestamp_ms,
350 next_epoch_system_package_bytes,
351 vec![],
352 scores,
353 adjust_rewards_by_score,
354 )
355 } else {
356 EndOfEpochTransactionKind::new_change_epoch_v3(
357 next_epoch,
358 next_epoch_protocol_version.as_u64(),
359 gas_cost_summary.storage_cost,
360 gas_cost_summary.computation_cost,
361 gas_cost_summary.computation_cost_burned,
362 gas_cost_summary.storage_rebate,
363 gas_cost_summary.non_refundable_storage_fee,
364 epoch_start_timestamp_ms,
365 next_epoch_system_package_bytes,
366 vec![],
367 )
368 });
369
370 let tx = VerifiedTransaction::new_end_of_epoch_transaction(kinds);
371 let (effects, execution_error) = self
372 .execute_transaction(tx.into())
373 .expect("advancing the epoch cannot fail");
374 assert!(
375 execution_error.is_none(),
376 "the end-of-epoch transaction failed: {execution_error:?}, {effects:?}"
377 );
378
379 let (checkpoint, contents, new_epoch_state) = {
380 let mut inner = self.inner.write().unwrap();
381 let system_state = inner.store.get_system_state();
382 let prev_protocol_config = inner.epoch_state.protocol_config();
386 let new_epoch_state =
387 if system_state.protocol_version() == prev_protocol_config.version.as_u64() {
388 EpochState::new_with_config(prev_protocol_config.clone(), system_state)
389 } else {
390 EpochState::new(system_state)
391 };
392 let end_of_epoch_data = EndOfEpochData {
393 next_epoch_committee: new_epoch_state.committee().committee_members(),
394 next_epoch_protocol_version: next_epoch_protocol_version.as_u64(),
395 epoch_commitments: vec![],
396 epoch_supply_change: 0,
398 };
399 let (checkpoint, contents, _) = {
400 let committee =
401 CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
402 let timestamp_ms = inner.store.get_clock().timestamp_ms();
403 inner.checkpoint_builder.build_end_of_epoch(
404 &committee,
405 timestamp_ms,
406 next_epoch,
407 end_of_epoch_data,
408 )
409 };
410
411 inner.store.insert_checkpoint(checkpoint.clone());
412 inner.store.insert_checkpoint_contents(contents.clone());
413 inner
414 .store
415 .update_last_checkpoint_of_epoch(current_epoch, checkpoint.sequence_number());
416 (checkpoint, contents, new_epoch_state)
417 };
418
419 self.process_data_ingestion(checkpoint, contents).unwrap();
421
422 let mut inner = self.inner.write().unwrap();
424 inner.epoch_state = new_epoch_state;
425
426 effects
427 }
428
429 pub fn with_store<F, T>(&self, f: F) -> T
434 where
435 F: FnOnce(&S) -> T,
436 {
437 let inner = self.inner.read().unwrap();
438 f(&inner.store)
439 }
440
441 pub fn with_keystore<F, T>(&self, f: F) -> T
446 where
447 F: FnOnce(&KeyStore) -> T,
448 {
449 let inner = self.inner.read().unwrap();
450 f(&inner.keystore)
451 }
452
453 pub fn epoch_start_state(&self) -> EpochStartSystemState {
454 let inner = self.inner.read().unwrap();
455 inner.epoch_state.epoch_start_state()
456 }
457
458 pub fn with_rng<F, T>(&self, f: F) -> T
465 where
466 F: FnOnce(&mut R) -> T,
467 {
468 let mut inner = self.inner.write().unwrap();
469 f(&mut inner.rng)
470 }
471
472 pub fn reference_gas_price(&self) -> u64 {
474 self.inner.read().unwrap().epoch_state.reference_gas_price()
475 }
476
477 pub fn request_gas(&self, address: Address, amount: u64) -> Result<TransactionEffects> {
494 let (sender, key) = self.with_keystore(|keystore| -> Result<(Address, _)> {
498 let (s, k) = keystore
499 .accounts()
500 .next()
501 .ok_or_else(|| anyhow!("no accounts available in keystore"))?;
502 Ok((*s, k.clone()))
503 })?;
504
505 let object = self
506 .with_store(|store| {
507 store.owned_objects(sender).find(|object| {
508 object.is_gas_coin()
509 && object.get_coin_value_unchecked() > amount + NANOS_PER_IOTA
510 })
511 })
512 .ok_or_else(|| {
513 anyhow!("unable to find a coin with enough to satisfy request for {amount} Nanos")
514 })?;
515
516 let gas_data = GasPayment {
517 objects: vec![object.object_ref()],
518 owner: sender,
519 price: self.reference_gas_price(),
520 budget: NANOS_PER_IOTA,
521 };
522
523 let pt = {
524 let mut builder =
525 iota_types::programmable_transaction_builder::ProgrammableTransactionBuilder::new();
526 builder.transfer_iota(address, Some(amount));
527 builder.finish()
528 };
529
530 let kind = TransactionKind::Programmable(pt);
531 let tx = iota_sdk_types::Transaction::new_with_gas_data(kind, sender, gas_data);
532 let tx = TransactionEnvelope::from_data_and_signer(tx, vec![&key]);
533
534 self.execute_transaction(tx).map(|x| x.0)
535 }
536
537 pub fn set_data_ingestion_path(&self, data_ingestion_path: PathBuf) {
538 let checkpoint = {
539 let mut inner = self.inner.write().unwrap();
540 inner.data_ingestion_path = Some(data_ingestion_path);
541 let checkpoint = inner.store.get_checkpoint_by_sequence_number(0).unwrap();
542 let contents = inner
543 .store
544 .get_checkpoint_contents_by_digest(&checkpoint.contents_digest);
545 (checkpoint, contents)
546 };
547 if let (checkpoint, Some(contents)) = checkpoint {
549 self.process_data_ingestion(checkpoint, contents).unwrap();
550 }
551 }
552
553 pub fn override_next_checkpoint_number(&self, number: CheckpointSequenceNumber) {
559 let mut inner = self.inner.write().unwrap();
560 let committee = CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
561 inner
562 .checkpoint_builder
563 .override_next_checkpoint_number(number, &committee);
564 }
565
566 fn process_data_ingestion(
569 &self,
570 checkpoint: VerifiedCheckpoint,
571 checkpoint_contents: CheckpointContents,
572 ) -> anyhow::Result<()> {
573 let path = self.inner.read().unwrap().data_ingestion_path.clone();
574 if let Some(data_path) = path {
575 let file_name = format!("{}.chk", checkpoint.sequence_number);
576 let checkpoint_data = self.try_get_checkpoint_data(checkpoint, checkpoint_contents)?;
577 std::fs::create_dir_all(&data_path)?;
578 let blob = Blob::encode(&checkpoint_data, BlobEncoding::Bcs)?;
579 std::fs::write(data_path.join(file_name), blob.to_bytes())?;
580 }
581 Ok(())
582 }
583}
584
585pub struct CommitteeWithKeys {
586 keystore: KeyStore,
587 committee: Committee,
588}
589
590impl CommitteeWithKeys {
591 fn new(keystore: &KeyStore, committee: &Committee) -> Self {
592 Self {
593 keystore: keystore.clone(),
594 committee: committee.clone(),
595 }
596 }
597
598 pub fn keystore(&self) -> &KeyStore {
599 &self.keystore
600 }
601}
602
603impl ValidatorKeypairProvider for CommitteeWithKeys {
604 fn get_validator_key(&self, name: &AuthorityName) -> &dyn Signer<AuthoritySignature> {
605 self.keystore.validator(name).unwrap()
606 }
607
608 fn get_committee(&self) -> &Committee {
609 &self.committee
610 }
611}
612
613impl<T, V: store::SimulatorStore> ObjectStore for Simulacrum<T, V> {
614 fn try_get_object(
615 &self,
616 object_id: &ObjectId,
617 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
618 self.with_store(|store| store.try_get_object(object_id))
619 }
620
621 fn try_get_object_by_key(
622 &self,
623 object_id: &ObjectId,
624 version: VersionNumber,
625 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
626 self.with_store(|store| store.try_get_object_by_key(object_id, version))
627 }
628}
629
630impl<T, V: store::SimulatorStore> ReadStore for Simulacrum<T, V> {
631 fn try_get_committee(
632 &self,
633 epoch: iota_types::committee::EpochId,
634 ) -> iota_types::storage::error::Result<Option<std::sync::Arc<Committee>>> {
635 self.with_store(|store| store.try_get_committee(epoch))
636 }
637
638 fn try_get_latest_checkpoint(&self) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
639 Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
640 }
641
642 fn try_get_latest_epoch_id(&self) -> iota_types::storage::error::Result<EpochId> {
643 Ok(self.inner.read().unwrap().epoch_state.epoch())
644 }
645
646 fn try_get_highest_verified_checkpoint(
647 &self,
648 ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
649 Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
650 }
651
652 fn try_get_highest_synced_checkpoint(
653 &self,
654 ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
655 Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
656 }
657
658 fn try_get_lowest_available_checkpoint(
659 &self,
660 ) -> iota_types::storage::error::Result<iota_types::messages_checkpoint::CheckpointSequenceNumber>
661 {
662 Ok(0)
665 }
666
667 fn try_get_checkpoint_by_digest(
668 &self,
669 digest: &CheckpointDigest,
670 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
671 Ok(self.with_store(|store| store.get_checkpoint_by_digest(digest)))
672 }
673
674 fn try_get_checkpoint_by_sequence_number(
675 &self,
676 sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
677 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
678 Ok(self.with_store(|store| store.get_checkpoint_by_sequence_number(sequence_number)))
679 }
680
681 fn try_get_checkpoint_contents_by_digest(
682 &self,
683 digest: &CheckpointContentsDigest,
684 ) -> iota_types::storage::error::Result<Option<CheckpointContents>> {
685 Ok(self.with_store(|store| store.get_checkpoint_contents_by_digest(digest)))
686 }
687
688 fn try_get_checkpoint_contents_by_sequence_number(
689 &self,
690 sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
691 ) -> iota_types::storage::error::Result<Option<CheckpointContents>> {
692 Ok(self.with_store(|store| {
693 store
694 .get_checkpoint_by_sequence_number(sequence_number)
695 .and_then(|checkpoint| {
696 store.get_checkpoint_contents_by_digest(&checkpoint.contents_digest)
697 })
698 }))
699 }
700
701 fn try_get_transaction(
702 &self,
703 tx_digest: &TransactionDigest,
704 ) -> iota_types::storage::error::Result<Option<Arc<VerifiedTransaction>>> {
705 Ok(self.with_store(|store| store.get_transaction(tx_digest)))
706 }
707
708 fn try_get_transaction_effects(
709 &self,
710 tx_digest: &TransactionDigest,
711 ) -> iota_types::storage::error::Result<Option<TransactionEffects>> {
712 Ok(self.with_store(|store| store.get_transaction_effects(tx_digest)))
713 }
714
715 fn try_get_events(
716 &self,
717 digest: &TransactionDigest,
718 ) -> iota_types::storage::error::Result<Option<TransactionEvents>> {
719 Ok(self.with_store(|store| store.get_events(digest)))
720 }
721
722 fn try_get_full_checkpoint_contents_by_sequence_number(
723 &self,
724 sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
725 ) -> iota_types::storage::error::Result<
726 Option<iota_types::messages_checkpoint::FullCheckpointContents>,
727 > {
728 self.with_store(|store| {
729 store
730 .try_get_checkpoint_by_sequence_number(sequence_number)?
731 .and_then(|chk| store.get_checkpoint_contents_by_digest(&chk.contents_digest))
732 .map_or(Ok(None), |contents| {
733 iota_types::messages_checkpoint::FullCheckpointContents::try_from_checkpoint_contents(
734 store,
735 contents,
736 )
737 })
738 })
739 }
740
741 fn try_get_full_checkpoint_contents(
742 &self,
743 digest: &CheckpointContentsDigest,
744 ) -> iota_types::storage::error::Result<
745 Option<iota_types::messages_checkpoint::FullCheckpointContents>,
746 > {
747 self.with_store(|store| {
748 store.get_checkpoint_contents_by_digest(digest)
749 .map_or(Ok(None), |contents| {
750 iota_types::messages_checkpoint::FullCheckpointContents::try_from_checkpoint_contents(
751 store,
752 contents,
753 )
754 })
755 })
756 }
757}
758
759impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> GrpcStateReader for Simulacrum<T, V> {
760 fn get_lowest_available_checkpoint_objects(
761 &self,
762 ) -> iota_types::storage::error::Result<CheckpointSequenceNumber> {
763 Ok(0)
764 }
765
766 fn get_chain_identifier(
767 &self,
768 ) -> iota_types::storage::error::Result<iota_types::digests::ChainIdentifier> {
769 Ok(self
770 .with_store(|store| store.get_checkpoint_by_sequence_number(0))
771 .expect("lowest available checkpoint should exist")
772 .digest()
773 .to_owned()
774 .into())
775 }
776
777 fn get_epoch_last_checkpoint(
778 &self,
779 epoch_id: iota_types::committee::EpochId,
780 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
781 Ok(self.with_store(|store| {
782 store
783 .get_last_checkpoint_of_epoch(epoch_id)
784 .and_then(|seq| store.get_checkpoint_by_sequence_number(seq))
785 }))
786 }
787
788 fn get_epoch_info(
789 &self,
790 epoch: iota_types::committee::EpochId,
791 ) -> iota_types::storage::error::Result<Option<EpochInfoV2>> {
792 Ok(self.with_store(|store| {
793 let start_checkpoint_seq = if epoch != 0 {
794 store
795 .get_last_checkpoint_of_epoch(epoch - 1)
796 .map(|seq| Some(seq + 1))
797 .unwrap_or(None)?
798 } else {
799 0
800 };
801
802 let start_checkpoint = store.get_checkpoint_by_sequence_number(start_checkpoint_seq)?;
803
804 let system_state = self.get_system_state_for_epoch(epoch)?;
805
806 Some(EpochInfoV2 {
807 epoch,
808 start_checkpoint: start_checkpoint_seq,
809 start_timestamp_ms: start_checkpoint.data().timestamp_ms,
810 system_state,
811 epoch_close_proof: None,
814 })
815 }))
816 }
817
818 fn grpc_indexes(&self) -> Option<&dyn iota_node_storage::GrpcIndexes> {
819 Some(self)
820 }
821
822 fn get_struct_layout(
823 &self,
824 _: &StructTag,
825 ) -> iota_types::storage::error::Result<Option<move_core_types::annotated_value::MoveTypeLayout>>
826 {
827 Ok(None)
828 }
829}
830
831impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> Simulacrum<T, V> {
832 fn get_system_state_for_epoch(&self, epoch: u64) -> Option<IotaSystemState> {
833 self.with_store(|store| {
834 if let Some(historical_state) = store.get_system_state_by_epoch(epoch) {
835 return Some(historical_state.clone());
836 }
837 let current_system_state = store.get_system_state();
838 if epoch == current_system_state.epoch() {
839 return Some(current_system_state);
840 }
841 None
842 })
843 }
844}
845
846impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> GrpcIndexes for Simulacrum<T, V> {
847 fn get_transaction_info(
848 &self,
849 digest: &TransactionDigest,
850 ) -> iota_types::storage::error::Result<Option<TransactionInfo>> {
851 Ok(self.with_store(|store| {
852 let highest_seq = store
853 .get_highest_checkpoint()
854 .map(|cp| cp.sequence_number())?;
855
856 for seq in (0..=highest_seq).rev() {
857 if let Some(checkpoint) = store.get_checkpoint_by_sequence_number(seq) {
858 if let Some(contents) =
859 store.get_checkpoint_contents_by_digest(&checkpoint.contents_digest)
860 {
861 if contents
862 .iter()
863 .any(|exec_digests| exec_digests.transaction == *digest)
864 {
865 return Some(TransactionInfo {
869 checkpoint: checkpoint.sequence_number(),
870 object_types: HashMap::new(),
871 });
872 }
873 }
874 }
875 }
876 None
877 }))
878 }
879
880 fn account_owned_objects_info_iter(
881 &self,
882 _owner: Address,
883 _cursor: Option<&iota_types::storage::OwnedObjectCursor>,
884 _object_type: Option<StructTag>,
885 ) -> iota_types::storage::error::Result<
886 Box<dyn Iterator<Item = iota_types::storage::OwnedObjectIteratorItem> + '_>,
887 > {
888 Ok(Box::new(std::iter::empty()))
889 }
890
891 fn dynamic_field_iter(
892 &self,
893 _parent: iota_sdk_types::ObjectId,
894 _cursor: Option<iota_sdk_types::ObjectId>,
895 ) -> iota_types::storage::error::Result<
896 Box<
897 dyn Iterator<
898 Item = Result<
899 iota_types::storage::DynamicFieldKey,
900 typed_store_error::TypedStoreError,
901 >,
902 > + '_,
903 >,
904 > {
905 Ok(Box::new(std::iter::empty()))
906 }
907
908 fn get_coin_info(
909 &self,
910 _coin_type: &StructTag,
911 ) -> iota_types::storage::error::Result<Option<iota_types::storage::CoinInfo>> {
912 Ok(None)
913 }
914
915 fn package_versions_iter(
916 &self,
917 _original_package_id: iota_sdk_types::ObjectId,
918 _cursor: Option<u64>,
919 ) -> iota_types::storage::error::Result<
920 Box<dyn Iterator<Item = iota_types::storage::PackageVersionIteratorItem> + '_>,
921 > {
922 Ok(Box::new(std::iter::empty()))
923 }
924}
925
926impl Simulacrum {
927 pub fn transfer_txn(&self, recipient: Address) -> (TransactionEnvelope, u64) {
934 let (sender, key) = self.with_keystore(|keystore| {
935 let (s, k) = keystore.accounts().next().unwrap();
936 (*s, k.clone())
937 });
938
939 let (object, gas_coin_value) = self.with_store(|store| {
940 let object = store
941 .owned_objects(sender)
942 .find(|object| object.is_gas_coin())
943 .unwrap();
944 let gas_coin = GasCoin::try_from(object).unwrap();
945 (object.clone(), gas_coin.value())
946 });
947 let transfer_amount = gas_coin_value / 2;
948
949 let pt = {
950 let mut builder = ProgrammableTransactionBuilder::new();
951 builder.transfer_iota(recipient, Some(transfer_amount));
952 builder.finish()
953 };
954
955 let kind = TransactionKind::Programmable(pt);
956 let gas_data = GasPayment {
957 objects: vec![object.object_ref()],
958 owner: sender,
959 price: self.reference_gas_price(),
960 budget: 1_000_000_000,
961 };
962 let tx = Transaction::new_with_gas_data(kind, sender, gas_data);
963 let tx = TransactionEnvelope::from_data_and_signer(tx, vec![&key]);
964 (tx, transfer_amount)
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use std::time::Duration;
971
972 use iota_types::{
973 effects::TransactionEffectsAPI, gas_coin::GasCoin, transaction::TransactionAPI,
974 };
975 use rand::{SeedableRng, rngs::StdRng};
976
977 use super::*;
978
979 #[test]
980 fn deterministic_genesis() {
981 let rng = StdRng::from_seed([9; 32]);
982 let chain1 = Simulacrum::new_with_rng(rng);
983 let genesis_checkpoint_digest1 = chain1
984 .with_store(|store| *store.get_checkpoint_by_sequence_number(0).unwrap().digest());
985
986 let rng = StdRng::from_seed([9; 32]);
987 let chain2 = Simulacrum::new_with_rng(rng);
988 let genesis_checkpoint_digest2 = chain2
989 .with_store(|store| *store.get_checkpoint_by_sequence_number(0).unwrap().digest());
990
991 assert_eq!(genesis_checkpoint_digest1, genesis_checkpoint_digest2);
992
993 let rng = StdRng::from_seed([0; 32]);
995 let chain3 = Simulacrum::new_with_rng(rng);
996
997 let committee1 = chain1.with_store(|store| store.get_committee_by_epoch(0).cloned());
998 let committee3 = chain3.with_store(|store| store.get_committee_by_epoch(0).cloned());
999 assert_ne!(committee1, committee3);
1000 }
1001
1002 #[test]
1003 fn simple() {
1004 let steps = 10;
1005 let sim = Simulacrum::new();
1006
1007 let start_time_ms = sim.with_store(|store| {
1008 let clock = store.get_clock();
1009 println!("clock: {clock:#?}");
1010 clock.timestamp_ms()
1011 });
1012
1013 for _ in 0..steps {
1014 sim.advance_clock(Duration::from_millis(1));
1015 sim.create_checkpoint();
1016 sim.with_store(|store| {
1017 let clock = store.get_clock();
1018 println!("clock: {clock:#?}");
1019 });
1020 }
1021 let end_time_ms = sim.with_store(|store| store.get_clock().timestamp_ms());
1022 assert_eq!(end_time_ms - start_time_ms, steps);
1023 sim.with_store(|store| {
1024 dbg!(store.get_highest_checkpoint());
1025 });
1026 }
1027
1028 #[test]
1029 fn advance_epoch_creates_deny_rules_object() {
1030 let _guard =
1031 iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1032 config.set_deny_rule_governance_for_testing(true);
1033 config.set_deny_rule_governance_on_chain_for_testing(true);
1034 config.set_deny_rule_update_max_entries_per_tx_for_testing(1000);
1035 config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1036 config
1037 });
1038 let sim = Simulacrum::new();
1039
1040 assert!(sim.with_store(|store| {
1041 store
1042 .get_object(&iota_types::IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)
1043 .is_none()
1044 }));
1045
1046 sim.advance_epoch(true);
1047
1048 let object = sim
1049 .with_store(|store| {
1050 store
1051 .get_object(&iota_types::IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)
1052 .cloned()
1053 })
1054 .expect("the TransactionDenyRules object must exist after the create");
1055 assert!(object.owner().is_shared());
1056 }
1057
1058 #[test]
1061 fn walked_object_state_matches_applied_deltas() {
1062 use std::collections::BTreeSet;
1063
1064 use iota_sdk_types::TransactionDenyRulesUpdate;
1065 use iota_types::transaction_deny_rules::{
1066 get_transaction_deny_rules, get_transaction_deny_rules_obj_initial_shared_version,
1067 };
1068
1069 let _guard =
1070 iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1071 config.set_deny_rule_governance_for_testing(true);
1072 config.set_deny_rule_governance_on_chain_for_testing(true);
1073 config.set_deny_rule_update_max_entries_per_tx_for_testing(1000);
1074 config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1075 config
1076 });
1077 let sim = Simulacrum::new();
1078 sim.advance_epoch(true);
1079
1080 let initial_shared_version = sim
1081 .with_store(|store| get_transaction_deny_rules_obj_initial_shared_version(store))
1082 .unwrap()
1083 .expect("object must exist after the create");
1084
1085 let denied_address = Address::new([0xAA; 32]);
1086 let removed_address = Address::new([0xBB; 32]);
1087 let denied_package = ObjectId::new([0x2B; 32]);
1088 let update = |round, added_addresses: BTreeSet<Address>, removed_addresses| {
1089 VerifiedTransaction::new_transaction_deny_rules_update(TransactionDenyRulesUpdate {
1090 epoch: 1,
1091 round,
1092 added_addresses,
1093 removed_addresses,
1094 added_objects: BTreeSet::new(),
1095 removed_objects: BTreeSet::new(),
1096 added_packages: [denied_package].into(),
1097 removed_packages: BTreeSet::new(),
1098 package_publish_disabled: false,
1099 package_upgrade_disabled: false,
1100 shared_object_disabled: true,
1101 user_transaction_disabled: false,
1102 receiving_objects_disabled: false,
1103 move_authenticator_disabled: false,
1104 deny_rules_obj_initial_shared_version: initial_shared_version,
1105 })
1106 };
1107
1108 let (_, error) = sim
1111 .execute_transaction(
1112 update(0, [denied_address, removed_address].into(), BTreeSet::new()).into(),
1113 )
1114 .unwrap();
1115 assert!(error.is_none());
1116 let (_, error) = sim
1117 .execute_transaction(update(1, BTreeSet::new(), [removed_address].into()).into())
1118 .unwrap();
1119 assert!(error.is_none());
1120
1121 let walked = sim
1122 .with_store(|store| get_transaction_deny_rules(store))
1123 .unwrap()
1124 .expect("object must exist");
1125 assert_eq!(walked.denied_addresses, [denied_address].into());
1126 assert!(walked.denied_objects.is_empty());
1127 assert_eq!(walked.denied_packages, [denied_package].into());
1128 assert!(walked.shared_object_disabled);
1129 assert!(!walked.user_transaction_disabled);
1130 }
1131
1132 #[test]
1136 fn deny_rule_update_executes_at_the_chunk_ceiling() {
1137 use std::collections::BTreeSet;
1138
1139 use iota_sdk_types::TransactionDenyRulesUpdate;
1140 use iota_types::transaction_deny_rules::{
1141 get_transaction_deny_rules, get_transaction_deny_rules_obj_initial_shared_version,
1142 };
1143
1144 const CEILING: u64 = 2048;
1145
1146 let _guard =
1147 iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1148 config.set_deny_rule_governance_for_testing(true);
1149 config.set_deny_rule_governance_on_chain_for_testing(true);
1150 config.set_deny_rule_update_max_entries_per_tx_for_testing(CEILING);
1151 config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1152 config
1153 });
1154 let sim = Simulacrum::new();
1155 sim.advance_epoch(true);
1156
1157 let initial_shared_version = sim
1158 .with_store(|store| get_transaction_deny_rules_obj_initial_shared_version(store))
1159 .unwrap()
1160 .expect("object must exist after the create");
1161
1162 let addresses: BTreeSet<Address> = (0..CEILING)
1163 .map(|i| {
1164 let mut bytes = [0u8; 32];
1165 bytes[..8].copy_from_slice(&i.to_be_bytes());
1166 Address::new(bytes)
1167 })
1168 .collect();
1169 let update = |round, added_addresses, removed_addresses| {
1170 VerifiedTransaction::new_transaction_deny_rules_update(TransactionDenyRulesUpdate {
1171 epoch: 1,
1172 round,
1173 added_addresses,
1174 removed_addresses,
1175 added_objects: BTreeSet::new(),
1176 removed_objects: BTreeSet::new(),
1177 added_packages: BTreeSet::new(),
1178 removed_packages: BTreeSet::new(),
1179 package_publish_disabled: false,
1180 package_upgrade_disabled: false,
1181 shared_object_disabled: false,
1182 user_transaction_disabled: false,
1183 receiving_objects_disabled: false,
1184 move_authenticator_disabled: false,
1185 deny_rules_obj_initial_shared_version: initial_shared_version,
1186 })
1187 };
1188
1189 let (_, error) = sim
1190 .execute_transaction(update(0, addresses.clone(), BTreeSet::new()).into())
1191 .unwrap();
1192 assert!(error.is_none());
1193 let walked = sim
1194 .with_store(|store| get_transaction_deny_rules(store))
1195 .unwrap()
1196 .expect("object must exist");
1197 assert_eq!(walked.denied_addresses.len(), CEILING as usize);
1198
1199 let (_, error) = sim
1200 .execute_transaction(update(1, BTreeSet::new(), addresses).into())
1201 .unwrap();
1202 assert!(error.is_none());
1203 let walked = sim
1204 .with_store(|store| get_transaction_deny_rules(store))
1205 .unwrap()
1206 .expect("object must exist");
1207 assert!(walked.denied_addresses.is_empty());
1208 }
1209
1210 #[test]
1211 fn simple_epoch() {
1212 let steps = 10;
1213 let sim = Simulacrum::new();
1214
1215 let start_epoch = sim.with_store(|store| store.get_highest_checkpoint().unwrap().epoch);
1216 for i in 0..steps {
1217 sim.advance_epoch(false);
1218 sim.advance_clock(Duration::from_millis(1));
1219 sim.create_checkpoint();
1220 println!("{i}");
1221 }
1222 let end_epoch = sim.with_store(|store| store.get_highest_checkpoint().unwrap().epoch);
1223 assert_eq!(end_epoch - start_epoch, steps);
1224 sim.with_store(|store| {
1225 dbg!(store.get_highest_checkpoint());
1226 });
1227 }
1228
1229 #[test]
1230 fn transfer() {
1231 let sim = Simulacrum::new();
1232 let recipient = Address::random();
1233 let (tx, transfer_amount) = sim.transfer_txn(recipient);
1234
1235 let gas_id = tx.data().transaction().gas_data().objects[0].object_id;
1236 let effects = sim.execute_transaction(tx).unwrap().0;
1237 let gas_summary = effects.gas_cost_summary();
1238 let gas_paid = gas_summary.net_gas_usage();
1239
1240 sim.with_store(|store| {
1241 assert_eq!(
1242 (transfer_amount as i64 - gas_paid) as u64,
1243 store::SimulatorStore::get_object(store, &gas_id)
1244 .and_then(|object| GasCoin::try_from(&object).ok())
1245 .unwrap()
1246 .value()
1247 );
1248
1249 assert_eq!(
1250 transfer_amount,
1251 store
1252 .owned_objects(recipient)
1253 .next()
1254 .and_then(|object| GasCoin::try_from(object).ok())
1255 .unwrap()
1256 .value()
1257 );
1258 });
1259
1260 let checkpoint = sim.create_checkpoint();
1261
1262 assert_eq!(&checkpoint.epoch_rolling_gas_cost_summary, gas_summary);
1263 assert_eq!(checkpoint.network_total_transactions, 2); }
1265}