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