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