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