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