Skip to main content

simulacrum/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! A `Simulacrum` of IOTA.
6//!
7//! The word simulacrum is latin for "likeness, semblance", it is also a spell
8//! in D&D which creates a copy of a creature which then follows the player's
9//! commands and wishes. As such this crate provides the [`Simulacrum`] type
10//! which is a implementation or instantiation of a iota blockchain, one which
11//! doesn't do anything unless acted upon.
12//!
13//! [`Simulacrum`]: crate::Simulacrum
14
15mod 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, CheckpointContents, CheckpointContentsDigest, CheckpointDigest, ConsensusCommitDigest,
36    EndOfEpochData, EndOfEpochTransactionKind, GasPayment, ObjectId, StructTag, SystemPackage,
37    Transaction, TransactionDigest, TransactionEffects, TransactionEvents, TransactionKind,
38};
39use iota_storage::blob::{Blob, BlobEncoding};
40use iota_swarm_config::{
41    genesis_config::AccountConfig, network_config::NetworkConfig,
42    network_config_builder::ConfigBuilder,
43};
44use iota_types::{
45    base_types::{AuthorityName, EpochId, VersionNumber},
46    committee::Committee,
47    crypto::AuthoritySignature,
48    error::ExecutionError,
49    gas_coin::{GasCoin, NANOS_PER_IOTA},
50    inner_temporary_store::InnerTemporaryStore,
51    iota_system_state::{
52        IotaSystemState, IotaSystemStateTrait, epoch_start_iota_system_state::EpochStartSystemState,
53    },
54    messages_checkpoint::{CheckpointContentsExt, CheckpointSequenceNumber, VerifiedCheckpoint},
55    mock_checkpoint_builder::{MockCheckpointBuilder, ValidatorKeypairProvider},
56    object::Object,
57    programmable_transaction_builder::ProgrammableTransactionBuilder,
58    signature::VerifyParams,
59    storage::{EpochInfoV2, ObjectStore, ReadStore, TransactionInfo},
60    transaction::{TransactionAPI, TransactionEnvelope, VerifiedTransaction},
61};
62use rand::rngs::OsRng;
63
64pub use self::store::{SimulatorStore, in_mem_store::InMemoryStore};
65use self::{epoch_state::EpochState, store::in_mem_store::KeyStore};
66
67/// A `Simulacrum` of IOTA.
68///
69/// This type represents a simulated instantiation of an IOTA blockchain that
70/// needs to be driven manually, that is time doesn't advance and checkpoints
71/// are not formed unless explicitly requested.
72///
73/// See [module level][mod] documentation for more details.
74///
75/// [mod]: index.html
76pub struct Simulacrum<R = OsRng, Store: SimulatorStore = InMemoryStore> {
77    // Mutable state protected by RwLock for thread-safe interior mutability
78    inner: RwLock<SimulacrumInner<R, Store>>,
79    // Immutable config - can be accessed directly
80    deny_config: TransactionDenyConfig,
81    verifier_signing_config: VerifierSigningConfig,
82}
83
84struct SimulacrumInner<R, Store: SimulatorStore> {
85    rng: R,
86    keystore: KeyStore,
87    #[expect(unused)]
88    genesis: genesis::Genesis,
89    store: Store,
90    checkpoint_builder: MockCheckpointBuilder,
91
92    // Epoch specific data
93    epoch_state: EpochState,
94    data_ingestion_path: Option<PathBuf>,
95}
96
97impl Simulacrum {
98    /// Create a new, random Simulacrum instance using an `OsRng` as the source
99    /// of randomness.
100    #[expect(clippy::new_without_default)]
101    pub fn new() -> Self {
102        Self::new_with_rng(OsRng)
103    }
104}
105
106impl<R> Simulacrum<R>
107where
108    R: rand::RngCore + rand::CryptoRng,
109{
110    /// Create a new Simulacrum instance using the provided `rng`.
111    ///
112    /// This allows you to create a fully deterministic initial chainstate when
113    /// a seeded rng is used.
114    ///
115    /// ```
116    /// use rand::{SeedableRng, rngs::StdRng};
117    /// use simulacrum::Simulacrum;
118    ///
119    /// # fn main() {
120    /// let mut rng = StdRng::seed_from_u64(1);
121    /// let simulacrum = Simulacrum::new_with_rng(rng);
122    /// # }
123    /// ```
124    pub fn new_with_rng(mut rng: R) -> Self {
125        let config = ConfigBuilder::new_with_temp_dir()
126            .rng(&mut rng)
127            .with_chain_start_timestamp_ms(1)
128            .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
129            .build();
130        Self::new_with_network_config_in_mem(&config, rng)
131    }
132
133    pub fn new_with_protocol_version_and_accounts(
134        mut rng: R,
135        chain_start_timestamp_ms: u64,
136        protocol_version: ProtocolVersion,
137        account_configs: Vec<AccountConfig>,
138    ) -> Self {
139        let config = ConfigBuilder::new_with_temp_dir()
140            .rng(&mut rng)
141            .with_chain_start_timestamp_ms(chain_start_timestamp_ms)
142            .deterministic_committee_size(NonZeroUsize::new(1).unwrap())
143            .with_protocol_version(protocol_version)
144            .with_accounts(account_configs)
145            .build();
146        Self::new_with_network_config_in_mem(&config, rng)
147    }
148
149    fn new_with_network_config_in_mem(config: &NetworkConfig, rng: R) -> Self {
150        let store = InMemoryStore::new(&config.genesis);
151        Self::new_with_network_config_store(config, rng, store)
152    }
153}
154
155impl<R, S: store::SimulatorStore> Simulacrum<R, S> {
156    pub fn new_with_network_config_store(config: &NetworkConfig, rng: R, store: S) -> Self {
157        let keystore = KeyStore::from_network_config(config);
158        let checkpoint_builder = MockCheckpointBuilder::new(config.genesis.checkpoint());
159
160        let genesis = &config.genesis;
161        let epoch_state = EpochState::new(genesis.iota_system_object());
162
163        Self {
164            deny_config: TransactionDenyConfig::default(),
165            verifier_signing_config: VerifierSigningConfig::default(),
166            inner: RwLock::new(SimulacrumInner {
167                rng,
168                keystore,
169                genesis: genesis.clone(),
170                store,
171                checkpoint_builder,
172                epoch_state,
173                data_ingestion_path: None,
174            }),
175        }
176    }
177
178    /// Attempts to execute the provided transaction.
179    ///
180    /// The provided transaction undergoes the same types of checks that a
181    /// Validator does prior to signing and executing in the production
182    /// system. Some of these checks are as follows:
183    /// - User signature is valid
184    /// - Sender owns all OwnedObject inputs
185    /// - etc
186    ///
187    /// If the above checks are successful then the transaction is immediately
188    /// executed, enqueued to be included in the next checkpoint (the next
189    /// time `create_checkpoint` is called) and the corresponding
190    /// TransactionEffects are returned.
191    pub fn execute_transaction(
192        &self,
193        transaction: TransactionEnvelope,
194    ) -> anyhow::Result<(TransactionEffects, Option<ExecutionError>)> {
195        let mut inner = self.inner.write().unwrap();
196        let transaction = transaction.try_into_verified_for_testing(&VerifyParams::default())?;
197
198        let (inner_temporary_store, _, effects, execution_error_opt) =
199            inner.epoch_state.execute_transaction(
200                &inner.store,
201                &self.deny_config,
202                &self.verifier_signing_config,
203                &transaction,
204            )?;
205
206        let InnerTemporaryStore {
207            written, events, ..
208        } = inner_temporary_store;
209
210        inner.store.insert_executed_transaction(
211            transaction.clone(),
212            effects.clone(),
213            events,
214            written,
215        );
216
217        // Insert into checkpoint builder
218        inner
219            .checkpoint_builder
220            .push_transaction(transaction, effects.clone());
221        Ok((effects, execution_error_opt.err()))
222    }
223
224    /// Simulate a transaction without committing changes.
225    /// This is useful for testing transaction behavior without modifying state.
226    pub fn simulate_transaction(
227        &self,
228        transaction: Transaction,
229        checks: iota_types::transaction_executor::VmChecks,
230    ) -> iota_types::error::IotaResult<iota_types::transaction_executor::SimulateTransactionResult>
231    {
232        let inner = self.inner.read().unwrap();
233        inner.epoch_state.simulate_transaction(
234            &inner.store,
235            &self.deny_config,
236            &self.verifier_signing_config,
237            transaction,
238            checks,
239        )
240    }
241
242    /// Creates the next Checkpoint using the Transactions enqueued since the
243    /// last checkpoint was created.
244    pub fn create_checkpoint(&self) -> VerifiedCheckpoint {
245        let (checkpoint, contents) = {
246            let mut inner = self.inner.write().unwrap();
247            let committee = CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
248            let timestamp_ms = inner.store.get_clock().timestamp_ms();
249            let (checkpoint, contents, _) =
250                inner.checkpoint_builder.build(&committee, timestamp_ms);
251            inner.store.insert_checkpoint(checkpoint.clone());
252            inner.store.insert_checkpoint_contents(contents.clone());
253            (checkpoint, contents)
254        };
255        // Release lock before expensive data ingestion operation
256        self.process_data_ingestion(checkpoint.clone(), contents)
257            .unwrap();
258        checkpoint
259    }
260
261    /// Advances the clock by `duration`.
262    ///
263    /// This creates and executes a ConsensusCommitPrologue transaction which
264    /// advances the chain Clock by the provided duration.
265    pub fn advance_clock(&self, duration: std::time::Duration) -> TransactionEffects {
266        let mut inner = self.inner.write().unwrap();
267        let epoch = inner.epoch_state.epoch();
268        let round = inner.epoch_state.next_consensus_round();
269        let timestamp_ms = inner.store.get_clock().timestamp_ms() + duration.as_millis() as u64;
270        drop(inner);
271
272        let consensus_commit_prologue_transaction =
273            VerifiedTransaction::new_consensus_commit_prologue_v1(
274                epoch,
275                round,
276                timestamp_ms,
277                ConsensusCommitDigest::default(),
278                Vec::new(),
279            );
280
281        self.execute_transaction(consensus_commit_prologue_transaction.into())
282            .expect("advancing the clock cannot fail")
283            .0
284    }
285
286    /// Advances the epoch.
287    ///
288    /// This creates and executes an EndOfEpoch transaction which advances the
289    /// chain into the next epoch. Since it is required to be the final
290    /// transaction in an epoch, the final checkpoint in the epoch is also
291    /// created.
292    ///
293    /// NOTE: This function does not currently support updating the protocol
294    /// version or the system packages.
295    ///
296    /// With `create_deny_rules_object`, the end-of-epoch transaction also
297    /// creates the `TransactionDenyRules` object (requires the
298    /// `deny_rule_governance_on_chain` feature flag).
299    ///
300    /// Returns the effects of the end-of-epoch transaction.
301    ///
302    /// # Panics
303    ///
304    /// Panics if the end-of-epoch transaction cannot be executed or fails.
305    pub fn advance_epoch(&self, create_deny_rules_object: bool) -> TransactionEffects {
306        let inner = self.inner.read().unwrap();
307        let current_epoch = inner.epoch_state.epoch();
308        let next_epoch = current_epoch + 1;
309        let next_epoch_protocol_version = inner.epoch_state.protocol_version();
310        let gas_cost_summary = inner
311            .checkpoint_builder
312            .epoch_rolling_gas_cost_summary()
313            .clone();
314        let epoch_start_timestamp_ms = inner.store.get_clock().timestamp_ms();
315        let pass_validator_scores = inner
316            .epoch_state
317            .protocol_config()
318            .pass_validator_scores_to_advance_epoch();
319        let adjust_rewards_by_score = inner
320            .epoch_state
321            .protocol_config()
322            .adjust_rewards_by_score();
323        let committee_size = inner.epoch_state.committee().num_members();
324        drop(inner);
325
326        // One full score per validator, so rewards stay unadjusted. This
327        // mirrors the node's default when locally calculated scores are not
328        // passed. Must match MAX_SCORE in validator_set.move.
329        const MAX_SCORE: u64 = u16::MAX as u64 + 1;
330        let scores = vec![MAX_SCORE; committee_size];
331
332        let next_epoch_system_package_bytes: Vec<SystemPackage> = vec![];
333        let mut kinds = Vec::new();
334        if create_deny_rules_object {
335            kinds.push(EndOfEpochTransactionKind::TransactionDenyRulesCreate);
336        }
337        // Mirror the node's kind selection: the framework's `advance_epoch`
338        // expects the V4 argument shape when the flag is enabled.
339        kinds.push(if pass_validator_scores {
340            EndOfEpochTransactionKind::new_change_epoch_v4(
341                next_epoch,
342                next_epoch_protocol_version.as_u64(),
343                gas_cost_summary.storage_cost,
344                gas_cost_summary.computation_cost,
345                gas_cost_summary.computation_cost_burned,
346                gas_cost_summary.storage_rebate,
347                gas_cost_summary.non_refundable_storage_fee,
348                epoch_start_timestamp_ms,
349                next_epoch_system_package_bytes,
350                vec![],
351                scores,
352                adjust_rewards_by_score,
353            )
354        } else {
355            EndOfEpochTransactionKind::new_change_epoch_v3(
356                next_epoch,
357                next_epoch_protocol_version.as_u64(),
358                gas_cost_summary.storage_cost,
359                gas_cost_summary.computation_cost,
360                gas_cost_summary.computation_cost_burned,
361                gas_cost_summary.storage_rebate,
362                gas_cost_summary.non_refundable_storage_fee,
363                epoch_start_timestamp_ms,
364                next_epoch_system_package_bytes,
365                vec![],
366            )
367        });
368
369        let tx = VerifiedTransaction::new_end_of_epoch_transaction(kinds);
370        let (effects, execution_error) = self
371            .execute_transaction(tx.into())
372            .expect("advancing the epoch cannot fail");
373        assert!(
374            execution_error.is_none(),
375            "the end-of-epoch transaction failed: {execution_error:?}, {effects:?}"
376        );
377
378        let (checkpoint, contents, new_epoch_state) = {
379            let mut inner = self.inner.write().unwrap();
380            let system_state = inner.store.get_system_state();
381            // On same-version epoch changes, carry the current protocol config
382            // over instead of re-resolving it from the version, so feature
383            // flags customized for testing survive the epoch change.
384            let prev_protocol_config = inner.epoch_state.protocol_config();
385            let new_epoch_state =
386                if system_state.protocol_version() == prev_protocol_config.version.as_u64() {
387                    EpochState::new_with_config(prev_protocol_config.clone(), system_state)
388                } else {
389                    EpochState::new(system_state)
390                };
391            let end_of_epoch_data = EndOfEpochData {
392                next_epoch_committee: new_epoch_state.committee().committee_members(),
393                next_epoch_protocol_version: next_epoch_protocol_version.as_u64(),
394                epoch_commitments: vec![],
395                // Do not simulate supply changes for now.
396                epoch_supply_change: 0,
397            };
398            let (checkpoint, contents, _) = {
399                let committee =
400                    CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
401                let timestamp_ms = inner.store.get_clock().timestamp_ms();
402                inner.checkpoint_builder.build_end_of_epoch(
403                    &committee,
404                    timestamp_ms,
405                    next_epoch,
406                    end_of_epoch_data,
407                )
408            };
409
410            inner.store.insert_checkpoint(checkpoint.clone());
411            inner.store.insert_checkpoint_contents(contents.clone());
412            inner
413                .store
414                .update_last_checkpoint_of_epoch(current_epoch, checkpoint.sequence_number());
415            (checkpoint, contents, new_epoch_state)
416        };
417
418        // Process data ingestion without holding the lock
419        self.process_data_ingestion(checkpoint, contents).unwrap();
420
421        // Finally, update the epoch state
422        let mut inner = self.inner.write().unwrap();
423        inner.epoch_state = new_epoch_state;
424
425        effects
426    }
427
428    /// Execute a function with read access to the store.
429    ///
430    /// This provides thread-safe access to the underlying store by locking it
431    /// for the duration of the closure execution.
432    pub fn with_store<F, T>(&self, f: F) -> T
433    where
434        F: FnOnce(&S) -> T,
435    {
436        let inner = self.inner.read().unwrap();
437        f(&inner.store)
438    }
439
440    /// Execute a function with read access to the keystore.
441    ///
442    /// This provides thread-safe access to the keystore by locking it
443    /// for the duration of the closure execution.
444    pub fn with_keystore<F, T>(&self, f: F) -> T
445    where
446        F: FnOnce(&KeyStore) -> T,
447    {
448        let inner = self.inner.read().unwrap();
449        f(&inner.keystore)
450    }
451
452    pub fn epoch_start_state(&self) -> EpochStartSystemState {
453        let inner = self.inner.read().unwrap();
454        inner.epoch_state.epoch_start_state()
455    }
456
457    /// Execute a function with mutable access to the internally held RNG.
458    ///
459    /// Provides mutable access to the RNG used to create this Simulacrum for
460    /// use as a source of randomness. Using a seeded RNG to build a
461    /// Simulacrum and then utilizing the stored RNG as a source of
462    /// randomness can lead to a fully deterministic chain evolution.
463    pub fn with_rng<F, T>(&self, f: F) -> T
464    where
465        F: FnOnce(&mut R) -> T,
466    {
467        let mut inner = self.inner.write().unwrap();
468        f(&mut inner.rng)
469    }
470
471    /// Return the reference gas price for the current epoch
472    pub fn reference_gas_price(&self) -> u64 {
473        self.inner.read().unwrap().epoch_state.reference_gas_price()
474    }
475
476    /// Request that `amount` Nanos be sent to `address` from a faucet account.
477    ///
478    /// ```
479    /// use iota_sdk_types::Address;
480    /// use iota_types::gas_coin::NANOS_PER_IOTA;
481    /// use simulacrum::Simulacrum;
482    ///
483    /// # fn main() {
484    /// let mut simulacrum = Simulacrum::new();
485    /// let address = simulacrum.with_rng(|rng| Address::random_with(rng));
486    /// simulacrum.request_gas(address, NANOS_PER_IOTA).unwrap();
487    ///
488    /// // `account` now has a Coin<IOTA> object with single IOTA in it.
489    /// // ...
490    /// # }
491    /// ```
492    pub fn request_gas(&self, address: Address, amount: u64) -> Result<TransactionEffects> {
493        // For right now we'll just use the first account as the `faucet` account. We
494        // may want to explicitly cordon off the faucet account from the rest of
495        // the accounts though.
496        let (sender, key) = self.with_keystore(|keystore| -> Result<(Address, _)> {
497            let (s, k) = keystore
498                .accounts()
499                .next()
500                .ok_or_else(|| anyhow!("no accounts available in keystore"))?;
501            Ok((*s, k.clone()))
502        })?;
503
504        let object = self
505            .with_store(|store| {
506                store.owned_objects(sender).find(|object| {
507                    object.is_gas_coin()
508                        && object.get_coin_value_unchecked() > amount + NANOS_PER_IOTA
509                })
510            })
511            .ok_or_else(|| {
512                anyhow!("unable to find a coin with enough to satisfy request for {amount} Nanos")
513            })?;
514
515        let gas_data = GasPayment {
516            objects: vec![object.object_ref()],
517            owner: sender,
518            price: self.reference_gas_price(),
519            budget: NANOS_PER_IOTA,
520        };
521
522        let pt = {
523            let mut builder =
524                iota_types::programmable_transaction_builder::ProgrammableTransactionBuilder::new();
525            builder.transfer_iota(address, Some(amount));
526            builder.finish()
527        };
528
529        let kind = TransactionKind::Programmable(pt);
530        let tx = iota_sdk_types::Transaction::new_with_gas_data(kind, sender, gas_data);
531        let tx = TransactionEnvelope::from_data_and_signer(tx, vec![&key]);
532
533        self.execute_transaction(tx).map(|x| x.0)
534    }
535
536    pub fn set_data_ingestion_path(&self, data_ingestion_path: PathBuf) {
537        let checkpoint = {
538            let mut inner = self.inner.write().unwrap();
539            inner.data_ingestion_path = Some(data_ingestion_path);
540            let checkpoint = inner.store.get_checkpoint_by_sequence_number(0).unwrap();
541            let contents = inner
542                .store
543                .get_checkpoint_contents_by_digest(&checkpoint.contents_digest);
544            (checkpoint, contents)
545        };
546        // Release lock before expensive data ingestion operation
547        if let (checkpoint, Some(contents)) = checkpoint {
548            self.process_data_ingestion(checkpoint, contents).unwrap();
549        }
550    }
551
552    /// Overrides the next checkpoint number indirectly by setting the previous
553    /// checkpoint's number to checkpoint_number - 1. This ensures the next
554    /// generated checkpoint has the exact sequence number provided. This
555    /// can be useful to generate checkpoints with specific sequence
556    /// numbers. Monotonicity of checkpoint numbers is enforced strictly.
557    pub fn override_next_checkpoint_number(&self, number: CheckpointSequenceNumber) {
558        let mut inner = self.inner.write().unwrap();
559        let committee = CommitteeWithKeys::new(&inner.keystore, inner.epoch_state.committee());
560        inner
561            .checkpoint_builder
562            .override_next_checkpoint_number(number, &committee);
563    }
564
565    /// Process data ingestion without holding the inner lock.
566    /// This version should be used when you don't already hold the lock.
567    fn process_data_ingestion(
568        &self,
569        checkpoint: VerifiedCheckpoint,
570        checkpoint_contents: CheckpointContents,
571    ) -> anyhow::Result<()> {
572        let path = self.inner.read().unwrap().data_ingestion_path.clone();
573        if let Some(data_path) = path {
574            let file_name = format!("{}.chk", checkpoint.sequence_number);
575            let checkpoint_data = self.try_get_checkpoint_data(checkpoint, checkpoint_contents)?;
576            std::fs::create_dir_all(&data_path)?;
577            let blob = Blob::encode(&checkpoint_data, BlobEncoding::Bcs)?;
578            std::fs::write(data_path.join(file_name), blob.to_bytes())?;
579        }
580        Ok(())
581    }
582}
583
584pub struct CommitteeWithKeys {
585    keystore: KeyStore,
586    committee: Committee,
587}
588
589impl CommitteeWithKeys {
590    fn new(keystore: &KeyStore, committee: &Committee) -> Self {
591        Self {
592            keystore: keystore.clone(),
593            committee: committee.clone(),
594        }
595    }
596
597    pub fn keystore(&self) -> &KeyStore {
598        &self.keystore
599    }
600}
601
602impl ValidatorKeypairProvider for CommitteeWithKeys {
603    fn get_validator_key(&self, name: &AuthorityName) -> &dyn Signer<AuthoritySignature> {
604        self.keystore.validator(name).unwrap()
605    }
606
607    fn get_committee(&self) -> &Committee {
608        &self.committee
609    }
610}
611
612impl<T, V: store::SimulatorStore> ObjectStore for Simulacrum<T, V> {
613    fn try_get_object(
614        &self,
615        object_id: &ObjectId,
616    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
617        self.with_store(|store| store.try_get_object(object_id))
618    }
619
620    fn try_get_object_by_key(
621        &self,
622        object_id: &ObjectId,
623        version: VersionNumber,
624    ) -> Result<Option<Object>, iota_types::storage::error::Error> {
625        self.with_store(|store| store.try_get_object_by_key(object_id, version))
626    }
627}
628
629impl<T, V: store::SimulatorStore> ReadStore for Simulacrum<T, V> {
630    fn try_get_committee(
631        &self,
632        epoch: iota_types::committee::EpochId,
633    ) -> iota_types::storage::error::Result<Option<std::sync::Arc<Committee>>> {
634        self.with_store(|store| store.try_get_committee(epoch))
635    }
636
637    fn try_get_latest_checkpoint(&self) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
638        Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
639    }
640
641    fn try_get_latest_epoch_id(&self) -> iota_types::storage::error::Result<EpochId> {
642        Ok(self.inner.read().unwrap().epoch_state.epoch())
643    }
644
645    fn try_get_highest_verified_checkpoint(
646        &self,
647    ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
648        Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
649    }
650
651    fn try_get_highest_synced_checkpoint(
652        &self,
653    ) -> iota_types::storage::error::Result<VerifiedCheckpoint> {
654        Ok(self.with_store(|store| store.get_highest_checkpoint().unwrap()))
655    }
656
657    fn try_get_lowest_available_checkpoint(
658        &self,
659    ) -> iota_types::storage::error::Result<iota_types::messages_checkpoint::CheckpointSequenceNumber>
660    {
661        // TODO wire this up to the underlying sim store, for now this will work since
662        // we never prune the sim store
663        Ok(0)
664    }
665
666    fn try_get_checkpoint_by_digest(
667        &self,
668        digest: &CheckpointDigest,
669    ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
670        Ok(self.with_store(|store| store.get_checkpoint_by_digest(digest)))
671    }
672
673    fn try_get_checkpoint_by_sequence_number(
674        &self,
675        sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
676    ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
677        Ok(self.with_store(|store| store.get_checkpoint_by_sequence_number(sequence_number)))
678    }
679
680    fn try_get_checkpoint_contents_by_digest(
681        &self,
682        digest: &CheckpointContentsDigest,
683    ) -> iota_types::storage::error::Result<Option<CheckpointContents>> {
684        Ok(self.with_store(|store| store.get_checkpoint_contents_by_digest(digest)))
685    }
686
687    fn try_get_checkpoint_contents_by_sequence_number(
688        &self,
689        sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
690    ) -> iota_types::storage::error::Result<Option<CheckpointContents>> {
691        Ok(self.with_store(|store| {
692            store
693                .get_checkpoint_by_sequence_number(sequence_number)
694                .and_then(|checkpoint| {
695                    store.get_checkpoint_contents_by_digest(&checkpoint.contents_digest)
696                })
697        }))
698    }
699
700    fn try_get_transaction(
701        &self,
702        tx_digest: &TransactionDigest,
703    ) -> iota_types::storage::error::Result<Option<Arc<VerifiedTransaction>>> {
704        Ok(self.with_store(|store| store.get_transaction(tx_digest)))
705    }
706
707    fn try_get_transaction_effects(
708        &self,
709        tx_digest: &TransactionDigest,
710    ) -> iota_types::storage::error::Result<Option<TransactionEffects>> {
711        Ok(self.with_store(|store| store.get_transaction_effects(tx_digest)))
712    }
713
714    fn try_get_events(
715        &self,
716        digest: &TransactionDigest,
717    ) -> iota_types::storage::error::Result<Option<TransactionEvents>> {
718        Ok(self.with_store(|store| store.get_events(digest)))
719    }
720
721    fn try_get_full_checkpoint_contents_by_sequence_number(
722        &self,
723        sequence_number: iota_types::messages_checkpoint::CheckpointSequenceNumber,
724    ) -> iota_types::storage::error::Result<
725        Option<iota_types::messages_checkpoint::FullCheckpointContents>,
726    > {
727        self.with_store(|store| {
728            store
729                .try_get_checkpoint_by_sequence_number(sequence_number)?
730                .and_then(|chk| store.get_checkpoint_contents_by_digest(&chk.contents_digest))
731                .map_or(Ok(None), |contents| {
732                    iota_types::messages_checkpoint::FullCheckpointContents::try_from_checkpoint_contents(
733                        store,
734                        contents,
735                    )
736                })
737        })
738    }
739
740    fn try_get_full_checkpoint_contents(
741        &self,
742        digest: &CheckpointContentsDigest,
743    ) -> iota_types::storage::error::Result<
744        Option<iota_types::messages_checkpoint::FullCheckpointContents>,
745    > {
746        self.with_store(|store| {
747            store.get_checkpoint_contents_by_digest(digest)
748            .map_or(Ok(None), |contents| {
749                iota_types::messages_checkpoint::FullCheckpointContents::try_from_checkpoint_contents(
750                    store,
751                    contents,
752                )
753            })
754        })
755    }
756}
757
758impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> GrpcStateReader for Simulacrum<T, V> {
759    fn get_lowest_available_checkpoint_objects(
760        &self,
761    ) -> iota_types::storage::error::Result<CheckpointSequenceNumber> {
762        Ok(0)
763    }
764
765    fn get_chain_identifier(
766        &self,
767    ) -> iota_types::storage::error::Result<iota_types::digests::ChainIdentifier> {
768        Ok(self
769            .with_store(|store| store.get_checkpoint_by_sequence_number(0))
770            .expect("lowest available checkpoint should exist")
771            .digest()
772            .to_owned()
773            .into())
774    }
775
776    fn get_epoch_last_checkpoint(
777        &self,
778        epoch_id: iota_types::committee::EpochId,
779    ) -> iota_types::storage::error::Result<Option<VerifiedCheckpoint>> {
780        Ok(self.with_store(|store| {
781            store
782                .get_last_checkpoint_of_epoch(epoch_id)
783                .and_then(|seq| store.get_checkpoint_by_sequence_number(seq))
784        }))
785    }
786
787    fn get_epoch_info(
788        &self,
789        epoch: iota_types::committee::EpochId,
790    ) -> iota_types::storage::error::Result<Option<EpochInfoV2>> {
791        Ok(self.with_store(|store| {
792            let start_checkpoint_seq = if epoch != 0 {
793                store
794                    .get_last_checkpoint_of_epoch(epoch - 1)
795                    .map(|seq| Some(seq + 1))
796                    .unwrap_or(None)?
797            } else {
798                0
799            };
800
801            let start_checkpoint = store.get_checkpoint_by_sequence_number(start_checkpoint_seq)?;
802
803            let system_state = self.get_system_state_for_epoch(epoch)?;
804
805            Some(EpochInfoV2 {
806                epoch,
807                start_checkpoint: start_checkpoint_seq,
808                start_timestamp_ms: start_checkpoint.data().timestamp_ms,
809                system_state,
810                // Simulacrum doesn't build the close-of-epoch proof, so the
811                // derived `end_*` fields report `None`.
812                epoch_close_proof: None,
813            })
814        }))
815    }
816
817    fn grpc_indexes(&self) -> Option<&dyn iota_node_storage::GrpcIndexes> {
818        Some(self)
819    }
820
821    fn get_struct_layout(
822        &self,
823        _: &StructTag,
824    ) -> iota_types::storage::error::Result<Option<move_core_types::annotated_value::MoveTypeLayout>>
825    {
826        Ok(None)
827    }
828}
829
830impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> Simulacrum<T, V> {
831    fn get_system_state_for_epoch(&self, epoch: u64) -> Option<IotaSystemState> {
832        self.with_store(|store| {
833            if let Some(historical_state) = store.get_system_state_by_epoch(epoch) {
834                return Some(historical_state.clone());
835            }
836            let current_system_state = store.get_system_state();
837            if epoch == current_system_state.epoch() {
838                return Some(current_system_state);
839            }
840            None
841        })
842    }
843}
844
845impl<T: Send + Sync, V: store::SimulatorStore + Send + Sync> GrpcIndexes for Simulacrum<T, V> {
846    fn get_transaction_info(
847        &self,
848        digest: &TransactionDigest,
849    ) -> iota_types::storage::error::Result<Option<TransactionInfo>> {
850        Ok(self.with_store(|store| {
851            let highest_seq = store
852                .get_highest_checkpoint()
853                .map(|cp| cp.sequence_number())?;
854
855            for seq in (0..=highest_seq).rev() {
856                if let Some(checkpoint) = store.get_checkpoint_by_sequence_number(seq) {
857                    if let Some(contents) =
858                        store.get_checkpoint_contents_by_digest(&checkpoint.contents_digest)
859                    {
860                        if contents
861                            .iter()
862                            .any(|exec_digests| exec_digests.transaction == *digest)
863                        {
864                            // object_types left empty — production GrpcIndexesStore
865                            // populates this from input/output objects but that is
866                            // not needed for the simulacrum test harness.
867                            return Some(TransactionInfo {
868                                checkpoint: checkpoint.sequence_number(),
869                                object_types: HashMap::new(),
870                            });
871                        }
872                    }
873                }
874            }
875            None
876        }))
877    }
878
879    fn account_owned_objects_info_iter(
880        &self,
881        _owner: Address,
882        _cursor: Option<&iota_types::storage::OwnedObjectCursor>,
883        _object_type: Option<StructTag>,
884    ) -> iota_types::storage::error::Result<
885        Box<dyn Iterator<Item = iota_types::storage::OwnedObjectIteratorItem> + '_>,
886    > {
887        Ok(Box::new(std::iter::empty()))
888    }
889
890    fn dynamic_field_iter(
891        &self,
892        _parent: iota_sdk_types::ObjectId,
893        _cursor: Option<iota_sdk_types::ObjectId>,
894    ) -> iota_types::storage::error::Result<
895        Box<
896            dyn Iterator<
897                    Item = Result<
898                        iota_types::storage::DynamicFieldKey,
899                        typed_store_error::TypedStoreError,
900                    >,
901                > + '_,
902        >,
903    > {
904        Ok(Box::new(std::iter::empty()))
905    }
906
907    fn get_coin_info(
908        &self,
909        _coin_type: &StructTag,
910    ) -> iota_types::storage::error::Result<Option<iota_types::storage::CoinInfo>> {
911        Ok(None)
912    }
913
914    fn package_versions_iter(
915        &self,
916        _original_package_id: iota_sdk_types::ObjectId,
917        _cursor: Option<u64>,
918    ) -> iota_types::storage::error::Result<
919        Box<dyn Iterator<Item = iota_types::storage::PackageVersionIteratorItem> + '_>,
920    > {
921        Ok(Box::new(std::iter::empty()))
922    }
923}
924
925impl Simulacrum {
926    /// Generate a random transfer transaction.
927    /// TODO: This is here today to make it easier to write tests. But we should
928    /// utilize all the existing code for generating transactions in
929    /// iota-test-transaction-builder by defining a trait
930    /// that both WalletContext and Simulacrum implement. Then we can remove
931    /// this function.
932    pub fn transfer_txn(&self, recipient: Address) -> (TransactionEnvelope, u64) {
933        let (sender, key) = self.with_keystore(|keystore| {
934            let (s, k) = keystore.accounts().next().unwrap();
935            (*s, k.clone())
936        });
937
938        let (object, gas_coin_value) = self.with_store(|store| {
939            let object = store
940                .owned_objects(sender)
941                .find(|object| object.is_gas_coin())
942                .unwrap();
943            let gas_coin = GasCoin::try_from(object).unwrap();
944            (object.clone(), gas_coin.value())
945        });
946        let transfer_amount = gas_coin_value / 2;
947
948        let pt = {
949            let mut builder = ProgrammableTransactionBuilder::new();
950            builder.transfer_iota(recipient, Some(transfer_amount));
951            builder.finish()
952        };
953
954        let kind = TransactionKind::Programmable(pt);
955        let gas_data = GasPayment {
956            objects: vec![object.object_ref()],
957            owner: sender,
958            price: self.reference_gas_price(),
959            budget: 1_000_000_000,
960        };
961        let tx = Transaction::new_with_gas_data(kind, sender, gas_data);
962        let tx = TransactionEnvelope::from_data_and_signer(tx, vec![&key]);
963        (tx, transfer_amount)
964    }
965}
966
967#[cfg(test)]
968mod tests {
969    use std::time::Duration;
970
971    use iota_types::{
972        effects::TransactionEffectsAPI, gas_coin::GasCoin, transaction::TransactionAPI,
973    };
974    use rand::{SeedableRng, rngs::StdRng};
975
976    use super::*;
977
978    #[test]
979    fn deterministic_genesis() {
980        let rng = StdRng::from_seed([9; 32]);
981        let chain1 = Simulacrum::new_with_rng(rng);
982        let genesis_checkpoint_digest1 = chain1
983            .with_store(|store| *store.get_checkpoint_by_sequence_number(0).unwrap().digest());
984
985        let rng = StdRng::from_seed([9; 32]);
986        let chain2 = Simulacrum::new_with_rng(rng);
987        let genesis_checkpoint_digest2 = chain2
988            .with_store(|store| *store.get_checkpoint_by_sequence_number(0).unwrap().digest());
989
990        assert_eq!(genesis_checkpoint_digest1, genesis_checkpoint_digest2);
991
992        // Ensure the committees are different when using different seeds
993        let rng = StdRng::from_seed([0; 32]);
994        let chain3 = Simulacrum::new_with_rng(rng);
995
996        let committee1 = chain1.with_store(|store| store.get_committee_by_epoch(0).cloned());
997        let committee3 = chain3.with_store(|store| store.get_committee_by_epoch(0).cloned());
998        assert_ne!(committee1, committee3);
999    }
1000
1001    #[test]
1002    fn simple() {
1003        let steps = 10;
1004        let sim = Simulacrum::new();
1005
1006        let start_time_ms = sim.with_store(|store| {
1007            let clock = store.get_clock();
1008            println!("clock: {clock:#?}");
1009            clock.timestamp_ms()
1010        });
1011
1012        for _ in 0..steps {
1013            sim.advance_clock(Duration::from_millis(1));
1014            sim.create_checkpoint();
1015            sim.with_store(|store| {
1016                let clock = store.get_clock();
1017                println!("clock: {clock:#?}");
1018            });
1019        }
1020        let end_time_ms = sim.with_store(|store| store.get_clock().timestamp_ms());
1021        assert_eq!(end_time_ms - start_time_ms, steps);
1022        sim.with_store(|store| {
1023            dbg!(store.get_highest_checkpoint());
1024        });
1025    }
1026
1027    #[test]
1028    fn advance_epoch_creates_deny_rules_object() {
1029        let _guard =
1030            iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1031                config.set_deny_rule_governance_for_testing(true);
1032                config.set_deny_rule_governance_on_chain_for_testing(true);
1033                config.set_deny_rule_update_max_entries_per_tx_for_testing(1000);
1034                config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1035                config
1036            });
1037        let sim = Simulacrum::new();
1038
1039        assert!(sim.with_store(|store| {
1040            store
1041                .get_object(&iota_types::IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)
1042                .is_none()
1043        }));
1044
1045        sim.advance_epoch(true);
1046
1047        let object = sim
1048            .with_store(|store| {
1049                store
1050                    .get_object(&iota_types::IOTA_TRANSACTION_DENY_RULES_OBJECT_ID)
1051                    .cloned()
1052            })
1053            .expect("the TransactionDenyRules object must exist after the create");
1054        assert!(object.owner().is_shared());
1055    }
1056
1057    /// The full deny rule state read back by walking the object's
1058    /// `LinkedTable`s matches the deltas applied through real execution.
1059    #[test]
1060    fn walked_object_state_matches_applied_deltas() {
1061        use std::collections::BTreeSet;
1062
1063        use iota_sdk_types::TransactionDenyRulesUpdate;
1064        use iota_types::transaction_deny_rules::{
1065            get_transaction_deny_rules, get_transaction_deny_rules_obj_initial_shared_version,
1066        };
1067
1068        let _guard =
1069            iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1070                config.set_deny_rule_governance_for_testing(true);
1071                config.set_deny_rule_governance_on_chain_for_testing(true);
1072                config.set_deny_rule_update_max_entries_per_tx_for_testing(1000);
1073                config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1074                config
1075            });
1076        let sim = Simulacrum::new();
1077        sim.advance_epoch(true);
1078
1079        let initial_shared_version = sim
1080            .with_store(|store| get_transaction_deny_rules_obj_initial_shared_version(store))
1081            .unwrap()
1082            .expect("object must exist after the create");
1083
1084        let denied_address = Address::new([0xAA; 32]);
1085        let removed_address = Address::new([0xBB; 32]);
1086        let denied_package = ObjectId::new([0x2B; 32]);
1087        let update = |round, added_addresses: BTreeSet<Address>, removed_addresses| {
1088            VerifiedTransaction::new_transaction_deny_rules_update(TransactionDenyRulesUpdate {
1089                epoch: 1,
1090                round,
1091                added_addresses,
1092                removed_addresses,
1093                added_objects: BTreeSet::new(),
1094                removed_objects: BTreeSet::new(),
1095                added_packages: [denied_package].into(),
1096                removed_packages: BTreeSet::new(),
1097                package_publish_disabled: false,
1098                package_upgrade_disabled: false,
1099                shared_object_disabled: true,
1100                user_transaction_disabled: false,
1101                receiving_objects_disabled: false,
1102                move_authenticator_disabled: false,
1103                deny_rules_obj_initial_shared_version: initial_shared_version,
1104            })
1105        };
1106
1107        // Two entries in, then one removed again: the walk crosses re-linked
1108        // nodes, not just appended ones.
1109        let (_, error) = sim
1110            .execute_transaction(
1111                update(0, [denied_address, removed_address].into(), BTreeSet::new()).into(),
1112            )
1113            .unwrap();
1114        assert!(error.is_none());
1115        let (_, error) = sim
1116            .execute_transaction(update(1, BTreeSet::new(), [removed_address].into()).into())
1117            .unwrap();
1118        assert!(error.is_none());
1119
1120        let walked = sim
1121            .with_store(|store| get_transaction_deny_rules(store))
1122            .unwrap()
1123            .expect("object must exist");
1124        assert_eq!(walked.denied_addresses, [denied_address].into());
1125        assert!(walked.denied_objects.is_empty());
1126        assert_eq!(walked.denied_packages, [denied_package].into());
1127        assert!(walked.shared_object_disabled);
1128        assert!(!walked.user_transaction_disabled);
1129    }
1130
1131    /// A chunk at the `deny_rule_update_max_entries_per_tx` ceiling executes:
1132    /// all additions and all removals, which stress different limits (new
1133    /// object ids and event size vs the store entries touched by re-linking).
1134    #[test]
1135    fn deny_rule_update_executes_at_the_chunk_ceiling() {
1136        use std::collections::BTreeSet;
1137
1138        use iota_sdk_types::TransactionDenyRulesUpdate;
1139        use iota_types::transaction_deny_rules::{
1140            get_transaction_deny_rules, get_transaction_deny_rules_obj_initial_shared_version,
1141        };
1142
1143        const CEILING: u64 = 2048;
1144
1145        let _guard =
1146            iota_protocol_config::ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1147                config.set_deny_rule_governance_for_testing(true);
1148                config.set_deny_rule_governance_on_chain_for_testing(true);
1149                config.set_deny_rule_update_max_entries_per_tx_for_testing(CEILING);
1150                config.set_deny_rule_removal_grace_round_floor_for_testing(0);
1151                config
1152            });
1153        let sim = Simulacrum::new();
1154        sim.advance_epoch(true);
1155
1156        let initial_shared_version = sim
1157            .with_store(|store| get_transaction_deny_rules_obj_initial_shared_version(store))
1158            .unwrap()
1159            .expect("object must exist after the create");
1160
1161        let addresses: BTreeSet<Address> = (0..CEILING)
1162            .map(|i| {
1163                let mut bytes = [0u8; 32];
1164                bytes[..8].copy_from_slice(&i.to_be_bytes());
1165                Address::new(bytes)
1166            })
1167            .collect();
1168        let update = |round, added_addresses, removed_addresses| {
1169            VerifiedTransaction::new_transaction_deny_rules_update(TransactionDenyRulesUpdate {
1170                epoch: 1,
1171                round,
1172                added_addresses,
1173                removed_addresses,
1174                added_objects: BTreeSet::new(),
1175                removed_objects: BTreeSet::new(),
1176                added_packages: BTreeSet::new(),
1177                removed_packages: BTreeSet::new(),
1178                package_publish_disabled: false,
1179                package_upgrade_disabled: false,
1180                shared_object_disabled: false,
1181                user_transaction_disabled: false,
1182                receiving_objects_disabled: false,
1183                move_authenticator_disabled: false,
1184                deny_rules_obj_initial_shared_version: initial_shared_version,
1185            })
1186        };
1187
1188        let (_, error) = sim
1189            .execute_transaction(update(0, addresses.clone(), BTreeSet::new()).into())
1190            .unwrap();
1191        assert!(error.is_none());
1192        let walked = sim
1193            .with_store(|store| get_transaction_deny_rules(store))
1194            .unwrap()
1195            .expect("object must exist");
1196        assert_eq!(walked.denied_addresses.len(), CEILING as usize);
1197
1198        let (_, error) = sim
1199            .execute_transaction(update(1, BTreeSet::new(), addresses).into())
1200            .unwrap();
1201        assert!(error.is_none());
1202        let walked = sim
1203            .with_store(|store| get_transaction_deny_rules(store))
1204            .unwrap()
1205            .expect("object must exist");
1206        assert!(walked.denied_addresses.is_empty());
1207    }
1208
1209    #[test]
1210    fn simple_epoch() {
1211        let steps = 10;
1212        let sim = Simulacrum::new();
1213
1214        let start_epoch = sim.with_store(|store| store.get_highest_checkpoint().unwrap().epoch);
1215        for i in 0..steps {
1216            sim.advance_epoch(false);
1217            sim.advance_clock(Duration::from_millis(1));
1218            sim.create_checkpoint();
1219            println!("{i}");
1220        }
1221        let end_epoch = sim.with_store(|store| store.get_highest_checkpoint().unwrap().epoch);
1222        assert_eq!(end_epoch - start_epoch, steps);
1223        sim.with_store(|store| {
1224            dbg!(store.get_highest_checkpoint());
1225        });
1226    }
1227
1228    #[test]
1229    fn transfer() {
1230        let sim = Simulacrum::new();
1231        let recipient = Address::random();
1232        let (tx, transfer_amount) = sim.transfer_txn(recipient);
1233
1234        let gas_id = tx.data().transaction().gas_data().objects[0].object_id;
1235        let effects = sim.execute_transaction(tx).unwrap().0;
1236        let gas_summary = effects.gas_cost_summary();
1237        let gas_paid = gas_summary.net_gas_usage();
1238
1239        sim.with_store(|store| {
1240            assert_eq!(
1241                (transfer_amount as i64 - gas_paid) as u64,
1242                store::SimulatorStore::get_object(store, &gas_id)
1243                    .and_then(|object| GasCoin::try_from(&object).ok())
1244                    .unwrap()
1245                    .value()
1246            );
1247
1248            assert_eq!(
1249                transfer_amount,
1250                store
1251                    .owned_objects(recipient)
1252                    .next()
1253                    .and_then(|object| GasCoin::try_from(object).ok())
1254                    .unwrap()
1255                    .value()
1256            );
1257        });
1258
1259        let checkpoint = sim.create_checkpoint();
1260
1261        assert_eq!(&checkpoint.epoch_rolling_gas_cost_summary, gas_summary);
1262        assert_eq!(checkpoint.network_total_transactions, 2); // genesis + 1 txn
1263    }
1264}