1mod epoch_state;
16pub mod store;
17
18use std::{num::NonZeroUsize, path::PathBuf, sync::Arc};
19
20use anyhow::{Result, anyhow};
21use fastcrypto::traits::Signer;
22use iota_config::{
23 genesis, transaction_deny_config::TransactionDenyConfig,
24 verifier_signing_config::VerifierSigningConfig,
25};
26use iota_protocol_config::ProtocolVersion;
27use iota_storage::blob::{Blob, BlobEncoding};
28use iota_swarm_config::{
29 genesis_config::AccountConfig, network_config::NetworkConfig,
30 network_config_builder::ConfigBuilder,
31};
32use iota_types::{
33 base_types::{AuthorityName, IotaAddress, ObjectID, VersionNumber},
34 committee::Committee,
35 crypto::AuthoritySignature,
36 digests::ConsensusCommitDigest,
37 effects::TransactionEffects,
38 error::ExecutionError,
39 gas_coin::{GasCoin, NANOS_PER_IOTA},
40 inner_temporary_store::InnerTemporaryStore,
41 iota_system_state::epoch_start_iota_system_state::EpochStartSystemState,
42 messages_checkpoint::{
43 CheckpointContents, CheckpointSequenceNumber, EndOfEpochData, VerifiedCheckpoint,
44 },
45 mock_checkpoint_builder::{MockCheckpointBuilder, ValidatorKeypairProvider},
46 object::Object,
47 programmable_transaction_builder::ProgrammableTransactionBuilder,
48 signature::VerifyParams,
49 storage::{ObjectStore, ReadStore, RestStateReader},
50 transaction::{
51 EndOfEpochTransactionKind, GasData, Transaction, TransactionData, TransactionKind,
52 VerifiedTransaction,
53 },
54};
55use move_core_types::language_storage::StructTag;
56use rand::rngs::OsRng;
57
58pub use self::store::{SimulatorStore, in_mem_store::InMemoryStore};
59use self::{epoch_state::EpochState, store::in_mem_store::KeyStore};
60
61pub struct Simulacrum<R = OsRng, Store: SimulatorStore = InMemoryStore> {
71 rng: R,
72 keystore: KeyStore,
73 #[expect(unused)]
74 genesis: genesis::Genesis,
75 store: Store,
76 checkpoint_builder: MockCheckpointBuilder,
77
78 epoch_state: EpochState,
80
81 deny_config: TransactionDenyConfig,
83 data_ingestion_path: Option<PathBuf>,
84 verifier_signing_config: VerifierSigningConfig,
85}
86
87impl Simulacrum {
88 #[expect(clippy::new_without_default)]
91 pub fn new() -> Self {
92 Self::new_with_rng(OsRng)
93 }
94}
95
96impl<R> Simulacrum<R>
97where
98 R: rand::RngCore + rand::CryptoRng,
99{
100 pub fn new_with_rng(mut rng: R) -> Self {
115 let config = ConfigBuilder::new_with_temp_dir()
116 .rng(&mut rng)
117 .with_chain_start_timestamp_ms(1)
118 .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
119 .build();
120 Self::new_with_network_config_in_mem(&config, rng)
121 }
122
123 pub fn new_with_protocol_version_and_accounts(
124 mut rng: R,
125 chain_start_timestamp_ms: u64,
126 protocol_version: ProtocolVersion,
127 account_configs: Vec<AccountConfig>,
128 ) -> Self {
129 let config = ConfigBuilder::new_with_temp_dir()
130 .rng(&mut rng)
131 .with_chain_start_timestamp_ms(chain_start_timestamp_ms)
132 .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
133 .with_protocol_version(protocol_version)
134 .with_accounts(account_configs)
135 .build();
136 Self::new_with_network_config_in_mem(&config, rng)
137 }
138
139 fn new_with_network_config_in_mem(config: &NetworkConfig, rng: R) -> Self {
140 let store = InMemoryStore::new(&config.genesis);
141 Self::new_with_network_config_store(config, rng, store)
142 }
143}
144
145impl<R, S: store::SimulatorStore> Simulacrum<R, S> {
146 pub fn new_with_network_config_store(config: &NetworkConfig, rng: R, store: S) -> Self {
147 let keystore = KeyStore::from_network_config(config);
148 let checkpoint_builder = MockCheckpointBuilder::new(config.genesis.checkpoint());
149
150 let genesis = &config.genesis;
151 let epoch_state = EpochState::new(genesis.iota_system_object());
152
153 Self {
154 rng,
155 keystore,
156 genesis: genesis.clone(),
157 store,
158 checkpoint_builder,
159 epoch_state,
160 deny_config: TransactionDenyConfig::default(),
161 verifier_signing_config: VerifierSigningConfig::default(),
162 data_ingestion_path: None,
163 }
164 }
165
166 pub fn execute_transaction(
180 &mut self,
181 transaction: Transaction,
182 ) -> anyhow::Result<(TransactionEffects, Option<ExecutionError>)> {
183 let transaction = transaction
184 .try_into_verified_for_testing(self.epoch_state.epoch(), &VerifyParams::default())?;
185
186 let (inner_temporary_store, _, effects, execution_error_opt) =
187 self.epoch_state.execute_transaction(
188 &self.store,
189 &self.deny_config,
190 &self.verifier_signing_config,
191 &transaction,
192 )?;
193
194 let InnerTemporaryStore {
195 written, events, ..
196 } = inner_temporary_store;
197
198 self.store.insert_executed_transaction(
199 transaction.clone(),
200 effects.clone(),
201 events,
202 written,
203 );
204
205 self.checkpoint_builder
207 .push_transaction(transaction, effects.clone());
208 Ok((effects, execution_error_opt.err()))
209 }
210
211 pub fn create_checkpoint(&mut self) -> VerifiedCheckpoint {
214 let committee = CommitteeWithKeys::new(&self.keystore, self.epoch_state.committee());
215 let (checkpoint, contents, _) = self
216 .checkpoint_builder
217 .build(&committee, self.store.get_clock().timestamp_ms());
218 self.store.insert_checkpoint(checkpoint.clone());
219 self.store.insert_checkpoint_contents(contents.clone());
220 self.process_data_ingestion(checkpoint.clone(), contents)
221 .unwrap();
222 checkpoint
223 }
224
225 pub fn advance_clock(&mut self, duration: std::time::Duration) -> TransactionEffects {
230 let epoch = self.epoch_state.epoch();
231 let round = self.epoch_state.next_consensus_round();
232 let timestamp_ms = self.store.get_clock().timestamp_ms() + duration.as_millis() as u64;
233
234 let consensus_commit_prologue_transaction =
235 VerifiedTransaction::new_consensus_commit_prologue_v1(
236 epoch,
237 round,
238 timestamp_ms,
239 ConsensusCommitDigest::default(),
240 Vec::new(),
241 );
242
243 self.execute_transaction(consensus_commit_prologue_transaction.into())
244 .expect("advancing the clock cannot fail")
245 .0
246 }
247
248 pub fn advance_epoch(&mut self) {
258 let next_epoch = self.epoch_state.epoch() + 1;
259 let next_epoch_protocol_version = self.epoch_state.protocol_version();
260 let gas_cost_summary = self.checkpoint_builder.epoch_rolling_gas_cost_summary();
261 let epoch_start_timestamp_ms = self.store.get_clock().timestamp_ms();
262 let next_epoch_system_package_bytes = vec![];
263
264 let kinds = vec![EndOfEpochTransactionKind::new_change_epoch_v2(
265 next_epoch,
266 next_epoch_protocol_version,
267 gas_cost_summary.storage_cost,
268 gas_cost_summary.computation_cost,
269 gas_cost_summary.computation_cost_burned,
270 gas_cost_summary.storage_rebate,
271 gas_cost_summary.non_refundable_storage_fee,
272 epoch_start_timestamp_ms,
273 next_epoch_system_package_bytes,
274 )];
275
276 let tx = VerifiedTransaction::new_end_of_epoch_transaction(kinds);
277 self.execute_transaction(tx.into())
278 .expect("advancing the epoch cannot fail");
279
280 let new_epoch_state = EpochState::new(self.store.get_system_state());
281 let end_of_epoch_data = EndOfEpochData {
282 next_epoch_committee: new_epoch_state.committee().voting_rights.clone(),
283 next_epoch_protocol_version,
284 epoch_commitments: vec![],
285 epoch_supply_change: 0,
287 };
288 let committee = CommitteeWithKeys::new(&self.keystore, self.epoch_state.committee());
289 let (checkpoint, contents, _) = self.checkpoint_builder.build_end_of_epoch(
290 &committee,
291 self.store.get_clock().timestamp_ms(),
292 next_epoch,
293 end_of_epoch_data,
294 );
295
296 self.store.insert_checkpoint(checkpoint.clone());
297 self.store.insert_checkpoint_contents(contents.clone());
298 self.process_data_ingestion(checkpoint, contents).unwrap();
299 self.epoch_state = new_epoch_state;
300 }
301
302 pub fn store(&self) -> &dyn SimulatorStore {
303 &self.store
304 }
305
306 pub fn keystore(&self) -> &KeyStore {
307 &self.keystore
308 }
309
310 pub fn epoch_start_state(&self) -> &EpochStartSystemState {
311 self.epoch_state.epoch_start_state()
312 }
313
314 pub fn rng(&mut self) -> &mut R {
321 &mut self.rng
322 }
323
324 pub fn reference_gas_price(&self) -> u64 {
326 self.epoch_state.reference_gas_price()
327 }
328
329 pub fn request_gas(&mut self, address: IotaAddress, amount: u64) -> Result<TransactionEffects> {
345 let (sender, key) = self.keystore().accounts().next().unwrap();
349 let object = self
350 .store()
351 .owned_objects(*sender)
352 .find(|object| {
353 object.is_gas_coin() && object.get_coin_value_unsafe() > amount + NANOS_PER_IOTA
354 })
355 .ok_or_else(|| {
356 anyhow!("unable to find a coin with enough to satisfy request for {amount} Nanos")
357 })?;
358
359 let gas_data = iota_types::transaction::GasData {
360 payment: vec![object.compute_object_reference()],
361 owner: *sender,
362 price: self.reference_gas_price(),
363 budget: NANOS_PER_IOTA,
364 };
365
366 let pt = {
367 let mut builder =
368 iota_types::programmable_transaction_builder::ProgrammableTransactionBuilder::new();
369 builder.transfer_iota(address, Some(amount));
370 builder.finish()
371 };
372
373 let kind = iota_types::transaction::TransactionKind::ProgrammableTransaction(pt);
374 let tx_data =
375 iota_types::transaction::TransactionData::new_with_gas_data(kind, *sender, gas_data);
376 let tx = Transaction::from_data_and_signer(tx_data, vec![key]);
377
378 self.execute_transaction(tx).map(|x| x.0)
379 }
380
381 pub fn set_data_ingestion_path(&mut self, data_ingestion_path: PathBuf) {
382 self.data_ingestion_path = Some(data_ingestion_path);
383 let checkpoint = self.store.get_checkpoint_by_sequence_number(0).unwrap();
384 let contents = self
385 .store
386 .get_checkpoint_contents(&checkpoint.content_digest);
387 self.process_data_ingestion(checkpoint, contents.unwrap())
388 .unwrap();
389 }
390
391 fn process_data_ingestion(
392 &self,
393 checkpoint: VerifiedCheckpoint,
394 checkpoint_contents: CheckpointContents,
395 ) -> anyhow::Result<()> {
396 if let Some(path) = &self.data_ingestion_path {
397 let file_name = format!("{}.chk", checkpoint.sequence_number);
398 let checkpoint_data = self.get_checkpoint_data(checkpoint, checkpoint_contents)?;
399 std::fs::create_dir_all(path)?;
400 let blob = Blob::encode(&checkpoint_data, BlobEncoding::Bcs)?;
401 std::fs::write(path.join(file_name), blob.to_bytes())?;
402 }
403 Ok(())
404 }
405}
406
407pub struct CommitteeWithKeys<'a> {
408 keystore: &'a KeyStore,
409 committee: &'a Committee,
410}
411
412impl<'a> CommitteeWithKeys<'a> {
413 fn new(keystore: &'a KeyStore, committee: &'a Committee) -> Self {
414 Self {
415 keystore,
416 committee,
417 }
418 }
419
420 pub fn keystore(&self) -> &KeyStore {
421 self.keystore
422 }
423}
424
425impl ValidatorKeypairProvider for CommitteeWithKeys<'_> {
426 fn get_validator_key(&self, name: &AuthorityName) -> &dyn Signer<AuthoritySignature> {
427 self.keystore.validator(name).unwrap()
428 }
429
430 fn get_committee(&self) -> &Committee {
431 self.committee
432 }
433}
434
435impl<T, V: store::SimulatorStore> ObjectStore for Simulacrum<T, V> {
436 fn get_object(
437 &self,
438 object_id: &ObjectID,
439 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
440 Ok(store::SimulatorStore::get_object(&self.store, object_id))
441 }
442
443 fn get_object_by_key(
444 &self,
445 object_id: &ObjectID,
446 version: VersionNumber,
447 ) -> Result<Option<Object>, iota_types::storage::error::Error> {
448 self.store.get_object_by_key(object_id, version)
449 }
450}
451
452impl<T, V: store::SimulatorStore> ReadStore for Simulacrum<T, V> {
453 fn get_committee(
454 &self,
455 _epoch: iota_types::committee::EpochId,
456 ) -> iota_types::storage::error::Result<Option<std::sync::Arc<Committee>>> {
457 todo!()
458 }
459
460 fn get_latest_checkpoint(&self) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
461 Ok(self.store().get_highest_checkpoint().unwrap())
462 }
463
464 fn get_highest_verified_checkpoint(
465 &self,
466 ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
467 todo!()
468 }
469
470 fn get_highest_synced_checkpoint(
471 &self,
472 ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
473 todo!()
474 }
475
476 fn get_lowest_available_checkpoint(
477 &self,
478 ) -> iota_types::storage::error::Result<iota_types::messages_checkpoint::CheckpointSequenceNumber>
479 {
480 Ok(0)
483 }
484
485 fn get_checkpoint_by_digest(
486 &self,
487 digest: &iota_types::messages_checkpoint::CheckpointDigest,
488 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
489 Ok(self.store().get_checkpoint_by_digest(digest))
490 }
491
492 fn get_checkpoint_by_sequence_number(
493 &self,
494 sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
495 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
496 Ok(self
497 .store()
498 .get_checkpoint_by_sequence_number(sequence_number))
499 }
500
501 fn get_checkpoint_contents_by_digest(
502 &self,
503 digest: &iota_types::messages_checkpoint::CheckpointContentsDigest,
504 ) -> iota_types::storage::error::Result<
505 Option<iota_types::messages_checkpoint::CheckpointContents>,
506 > {
507 Ok(self.store().get_checkpoint_contents(digest))
508 }
509
510 fn get_checkpoint_contents_by_sequence_number(
511 &self,
512 _sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
513 ) -> iota_types::storage::error::Result<
514 Option<iota_types::messages_checkpoint::CheckpointContents>,
515 > {
516 todo!()
517 }
518
519 fn get_transaction(
520 &self,
521 tx_digest: &iota_types::digests::TransactionDigest,
522 ) -> iota_types::storage::error::Result<Option<Arc<VerifiedTransaction>>> {
523 Ok(self.store().get_transaction(tx_digest).map(Arc::new))
524 }
525
526 fn get_transaction_effects(
527 &self,
528 tx_digest: &iota_types::digests::TransactionDigest,
529 ) -> iota_types::storage::error::Result<Option<TransactionEffects>> {
530 Ok(self.store().get_transaction_effects(tx_digest))
531 }
532
533 fn get_events(
534 &self,
535 event_digest: &iota_types::digests::TransactionEventsDigest,
536 ) -> iota_types::storage::error::Result<Option<iota_types::effects::TransactionEvents>> {
537 Ok(self.store().get_transaction_events(event_digest))
538 }
539
540 fn get_full_checkpoint_contents_by_sequence_number(
541 &self,
542 _sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
543 ) -> iota_types::storage::error::Result<
544 Option<iota_types::messages_checkpoint::FullCheckpointContents>,
545 > {
546 todo!()
547 }
548
549 fn get_full_checkpoint_contents(
550 &self,
551 _digest: &iota_types::messages_checkpoint::CheckpointContentsDigest,
552 ) -> iota_types::storage::error::Result<
553 Option<iota_types::messages_checkpoint::FullCheckpointContents>,
554 > {
555 todo!()
556 }
557}
558
559impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> RestStateReader for Simulacrum<T, V> {
560 fn get_transaction_checkpoint(
561 &self,
562 _digest: &iota_types::digests::TransactionDigest,
563 ) -> iota_types::storage::error::Result<
564 Option<iota_types::messages_checkpoint::CheckpointSequenceNumber>,
565 > {
566 todo!()
567 }
568
569 fn get_lowest_available_checkpoint_objects(
570 &self,
571 ) -> iota_types::storage::error::Result<CheckpointSequenceNumber> {
572 Ok(0)
573 }
574
575 fn get_chain_identifier(
576 &self,
577 ) -> iota_types::storage::error::Result<iota_types::digests::ChainIdentifier> {
578 Ok(self
579 .store()
580 .get_checkpoint_by_sequence_number(0)
581 .unwrap()
582 .digest()
583 .to_owned()
584 .into())
585 }
586
587 fn account_owned_objects_info_iter(
588 &self,
589 _owner: IotaAddress,
590 _cursor: Option<ObjectID>,
591 ) -> iota_types::storage::error::Result<
592 Box<dyn Iterator<Item = iota_types::storage::AccountOwnedObjectInfo> + '_>,
593 > {
594 todo!()
595 }
596
597 fn dynamic_field_iter(
598 &self,
599 _parent: ObjectID,
600 _cursor: Option<ObjectID>,
601 ) -> iota_types::storage::error::Result<
602 Box<
603 dyn Iterator<
604 Item = (
605 iota_types::storage::DynamicFieldKey,
606 iota_types::storage::DynamicFieldIndexInfo,
607 ),
608 > + '_,
609 >,
610 > {
611 todo!()
612 }
613
614 fn get_coin_info(
615 &self,
616 _coin_type: &StructTag,
617 ) -> iota_types::storage::error::Result<Option<iota_types::storage::CoinInfo>> {
618 todo!()
619 }
620
621 fn get_epoch_last_checkpoint(
622 &self,
623 _epoch_id: iota_types::committee::EpochId,
624 ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
625 todo!()
626 }
627}
628
629impl Simulacrum {
630 pub fn transfer_txn(&mut self, recipient: IotaAddress) -> (Transaction, u64) {
637 let (sender, key) = self.keystore().accounts().next().unwrap();
638 let sender = *sender;
639
640 let object = self
641 .store()
642 .owned_objects(sender)
643 .find(|object| object.is_gas_coin())
644 .unwrap();
645 let gas_coin = GasCoin::try_from(&object).unwrap();
646 let transfer_amount = gas_coin.value() / 2;
647
648 let pt = {
649 let mut builder = ProgrammableTransactionBuilder::new();
650 builder.transfer_iota(recipient, Some(transfer_amount));
651 builder.finish()
652 };
653
654 let kind = TransactionKind::ProgrammableTransaction(pt);
655 let gas_data = GasData {
656 payment: vec![object.compute_object_reference()],
657 owner: sender,
658 price: self.reference_gas_price(),
659 budget: 1_000_000_000,
660 };
661 let tx_data = TransactionData::new_with_gas_data(kind, sender, gas_data);
662 let tx = Transaction::from_data_and_signer(tx_data, vec![key]);
663 (tx, transfer_amount)
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use std::time::Duration;
670
671 use iota_types::{
672 base_types::IotaAddress, effects::TransactionEffectsAPI, gas_coin::GasCoin,
673 transaction::TransactionDataAPI,
674 };
675 use rand::{SeedableRng, rngs::StdRng};
676
677 use super::*;
678
679 #[test]
680 fn deterministic_genesis() {
681 let rng = StdRng::from_seed([9; 32]);
682 let chain1 = Simulacrum::new_with_rng(rng);
683 let genesis_checkpoint_digest1 = *chain1
684 .store()
685 .get_checkpoint_by_sequence_number(0)
686 .unwrap()
687 .digest();
688
689 let rng = StdRng::from_seed([9; 32]);
690 let chain2 = Simulacrum::new_with_rng(rng);
691 let genesis_checkpoint_digest2 = *chain2
692 .store()
693 .get_checkpoint_by_sequence_number(0)
694 .unwrap()
695 .digest();
696
697 assert_eq!(genesis_checkpoint_digest1, genesis_checkpoint_digest2);
698
699 let rng = StdRng::from_seed([0; 32]);
701 let chain3 = Simulacrum::new_with_rng(rng);
702
703 assert_ne!(
704 chain1.store().get_committee_by_epoch(0),
705 chain3.store().get_committee_by_epoch(0),
706 );
707 }
708
709 #[test]
710 fn simple() {
711 let steps = 10;
712 let mut chain = Simulacrum::new();
713
714 let clock = chain.store().get_clock();
715 let start_time_ms = clock.timestamp_ms();
716 println!("clock: {:#?}", clock);
717 for _ in 0..steps {
718 chain.advance_clock(Duration::from_millis(1));
719 chain.create_checkpoint();
720 let clock = chain.store().get_clock();
721 println!("clock: {:#?}", clock);
722 }
723 let end_time_ms = chain.store().get_clock().timestamp_ms();
724 assert_eq!(end_time_ms - start_time_ms, steps);
725 dbg!(chain.store().get_highest_checkpoint());
726 }
727
728 #[test]
729 fn simple_epoch() {
730 let steps = 10;
731 let mut chain = Simulacrum::new();
732
733 let start_epoch = chain.store.get_highest_checkpoint().unwrap().epoch;
734 for i in 0..steps {
735 chain.advance_epoch();
736 chain.advance_clock(Duration::from_millis(1));
737 chain.create_checkpoint();
738 println!("{i}");
739 }
740 let end_epoch = chain.store.get_highest_checkpoint().unwrap().epoch;
741 assert_eq!(end_epoch - start_epoch, steps);
742 dbg!(chain.store().get_highest_checkpoint());
743 }
744
745 #[test]
746 fn transfer() {
747 let mut sim = Simulacrum::new();
748 let recipient = IotaAddress::random_for_testing_only();
749 let (tx, transfer_amount) = sim.transfer_txn(recipient);
750
751 let gas_id = tx.data().transaction_data().gas_data().payment[0].0;
752 let effects = sim.execute_transaction(tx).unwrap().0;
753 let gas_summary = effects.gas_cost_summary();
754 let gas_paid = gas_summary.net_gas_usage();
755
756 assert_eq!(
757 (transfer_amount as i64 - gas_paid) as u64,
758 store::SimulatorStore::get_object(sim.store(), &gas_id)
759 .and_then(|object| GasCoin::try_from(&object).ok())
760 .unwrap()
761 .value()
762 );
763
764 assert_eq!(
765 transfer_amount,
766 sim.store()
767 .owned_objects(recipient)
768 .next()
769 .and_then(|object| GasCoin::try_from(&object).ok())
770 .unwrap()
771 .value()
772 );
773
774 let checkpoint = sim.create_checkpoint();
775
776 assert_eq!(&checkpoint.epoch_rolling_gas_cost_summary, gas_summary);
777 assert_eq!(checkpoint.network_total_transactions, 2); }
779}