simulacrum/
epoch_state.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::HashSet, sync::Arc};
6
7use anyhow::Result;
8use iota_config::{
9    transaction_deny_config::TransactionDenyConfig, verifier_signing_config::VerifierSigningConfig,
10};
11use iota_execution::Executor;
12use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
13use iota_types::{
14    base_types::ObjectID,
15    committee::{Committee, EpochId},
16    digests::TransactionDigest,
17    effects::{TransactionEffects, TransactionEffectsAPI},
18    error::IotaResult,
19    gas::IotaGasStatus,
20    gas_coin::NANOS_PER_IOTA,
21    inner_temporary_store::InnerTemporaryStore,
22    iota_system_state::{
23        IotaSystemState, IotaSystemStateTrait,
24        epoch_start_iota_system_state::{EpochStartSystemState, EpochStartSystemStateTrait},
25    },
26    metrics::{BytecodeVerifierMetrics, LimitsMetrics},
27    object::{MoveObject, Object, Owner},
28    transaction::{ObjectReadResult, TransactionData, TransactionDataAPI, VerifiedTransaction},
29    transaction_executor::{SimulateTransactionResult, VmChecks},
30};
31
32use crate::SimulatorStore;
33
34pub struct EpochState {
35    epoch_start_state: EpochStartSystemState,
36    committee: Committee,
37    protocol_config: ProtocolConfig,
38    limits_metrics: Arc<LimitsMetrics>,
39    bytecode_verifier_metrics: Arc<BytecodeVerifierMetrics>,
40    executor: Arc<dyn Executor + Send + Sync>,
41    /// A counter that advances each time we advance the clock in order to
42    /// ensure that each update txn has a unique digest. This is reset on
43    /// epoch changes
44    next_consensus_round: u64,
45}
46
47impl EpochState {
48    pub fn new(system_state: IotaSystemState) -> Self {
49        let epoch_start_state = system_state.into_epoch_start_state();
50        let committee = epoch_start_state.get_iota_committee();
51        let protocol_config =
52            ProtocolConfig::get_for_version(epoch_start_state.protocol_version(), Chain::Unknown);
53        let registry = prometheus::Registry::new();
54        let limits_metrics = Arc::new(LimitsMetrics::new(&registry));
55        let bytecode_verifier_metrics = Arc::new(BytecodeVerifierMetrics::new(&registry));
56        let executor = iota_execution::executor(&protocol_config, true, None).unwrap();
57
58        Self {
59            epoch_start_state,
60            committee,
61            protocol_config,
62            limits_metrics,
63            bytecode_verifier_metrics,
64            executor,
65            next_consensus_round: 0,
66        }
67    }
68
69    pub fn epoch(&self) -> EpochId {
70        self.epoch_start_state.epoch()
71    }
72
73    pub fn reference_gas_price(&self) -> u64 {
74        self.epoch_start_state.reference_gas_price()
75    }
76
77    pub fn next_consensus_round(&mut self) -> u64 {
78        let round = self.next_consensus_round;
79        self.next_consensus_round += 1;
80        round
81    }
82
83    pub fn committee(&self) -> &Committee {
84        &self.committee
85    }
86
87    pub fn epoch_start_state(&self) -> EpochStartSystemState {
88        self.epoch_start_state.clone()
89    }
90
91    pub fn protocol_version(&self) -> ProtocolVersion {
92        self.protocol_config().version
93    }
94
95    pub fn protocol_config(&self) -> &ProtocolConfig {
96        &self.protocol_config
97    }
98
99    pub fn execute_transaction(
100        &self,
101        store: &dyn SimulatorStore,
102        deny_config: &TransactionDenyConfig,
103        verifier_signing_config: &VerifierSigningConfig,
104        transaction: &VerifiedTransaction,
105    ) -> Result<(
106        InnerTemporaryStore,
107        IotaGasStatus,
108        TransactionEffects,
109        Result<(), iota_types::error::ExecutionError>,
110    )> {
111        let tx_digest = *transaction.digest();
112        let tx_data = &transaction.data().intent_message().value;
113        let input_object_kinds = tx_data.input_objects()?;
114        let receiving_object_refs = tx_data.receiving_objects();
115
116        iota_transaction_checks::deny::check_transaction_for_signing(
117            tx_data,
118            transaction.tx_signatures(),
119            &input_object_kinds,
120            &receiving_object_refs,
121            deny_config,
122            &store,
123        )?;
124
125        let (input_objects, receiving_objects) = store.read_objects_for_synchronous_execution(
126            &tx_digest,
127            &input_object_kinds,
128            &receiving_object_refs,
129        )?;
130
131        // `MoveAuthenticator`s are not supported in Simulacrum, so we set the
132        // `authenticator_gas_budget` to 0.
133        let authenticator_gas_budget = 0;
134
135        // Run the transaction input checks that would run when submitting the txn to a
136        // validator for signing
137        let (gas_status, checked_input_objects) = iota_transaction_checks::check_transaction_input(
138            &self.protocol_config,
139            self.epoch_start_state.reference_gas_price(),
140            transaction.data().transaction_data(),
141            input_objects,
142            &receiving_objects,
143            &self.bytecode_verifier_metrics,
144            verifier_signing_config,
145            authenticator_gas_budget,
146        )?;
147
148        let transaction_data = transaction.data().transaction_data();
149        let (kind, signer, gas_data) = transaction_data.execution_parts();
150        Ok(self.executor.execute_transaction_to_effects(
151            store.backing_store(),
152            &self.protocol_config,
153            self.limits_metrics.clone(),
154            false,           // enable_expensive_checks
155            &HashSet::new(), // certificate_deny_set
156            &self.epoch_start_state.epoch(),
157            self.epoch_start_state.epoch_start_timestamp_ms(),
158            checked_input_objects,
159            gas_data,
160            gas_status,
161            kind,
162            signer,
163            tx_digest,
164            &mut None,
165        ))
166    }
167
168    /// Simulate a transaction without committing changes.
169    /// This is similar to execute_transaction but:
170    /// - Takes TransactionData instead of VerifiedTransaction (no signature
171    ///   required)
172    /// - Takes VmChecks parameter to control validation strictness
173    /// - Returns SimulateTransactionResult with input/output objects
174    /// - Creates a mock gas object if none provided
175    pub fn simulate_transaction(
176        &self,
177        store: &dyn SimulatorStore,
178        deny_config: &TransactionDenyConfig,
179        verifier_signing_config: &VerifierSigningConfig,
180        mut transaction: TransactionData,
181        checks: VmChecks,
182    ) -> IotaResult<SimulateTransactionResult> {
183        // Cheap validity checks for a transaction, including input size limits.
184        transaction.validity_check_no_gas_check(&self.protocol_config)?;
185
186        let input_object_kinds = transaction.input_objects()?;
187        let receiving_object_refs = transaction.receiving_objects();
188
189        // Check if some transaction elements are denied
190        iota_transaction_checks::deny::check_transaction_for_signing(
191            &transaction,
192            &[],
193            &input_object_kinds,
194            &receiving_object_refs,
195            deny_config,
196            store,
197        )?;
198
199        // Load input and receiving objects
200        let (mut input_objects, receiving_objects) = store.read_objects_for_synchronous_execution(
201            &transaction.digest(),
202            &input_object_kinds,
203            &receiving_object_refs,
204        )?;
205
206        // Create a mock gas object if one was not provided
207        const SIMULATION_GAS_COIN_VALUE: u64 = 1_000_000_000 * NANOS_PER_IOTA; // 1B IOTA
208        let mock_gas_id = if transaction.gas().is_empty() {
209            let mock_gas_object = Object::new_move(
210                MoveObject::new_gas_coin(1.into(), ObjectID::MAX, SIMULATION_GAS_COIN_VALUE),
211                Owner::AddressOwner(transaction.gas_data().owner),
212                TransactionDigest::genesis_marker(),
213            );
214            let mock_gas_object_ref = mock_gas_object.compute_object_reference();
215            transaction.gas_data_mut().payment = vec![mock_gas_object_ref];
216            input_objects.push(ObjectReadResult::new_from_gas_object(&mock_gas_object));
217            Some(mock_gas_object.id())
218        } else {
219            None
220        };
221
222        // `MoveAuthenticator`s are not supported in Simulacrum, so we set the
223        // `authenticator_gas_budget` to 0.
224        let authenticator_gas_budget = 0;
225
226        // Checks enabled -> DRY-RUN (simulating a real TX)
227        // Checks disabled -> DEV-INSPECT (more relaxed Move VM checks)
228        let (gas_status, checked_input_objects) = if checks.enabled() {
229            iota_transaction_checks::check_transaction_input(
230                &self.protocol_config,
231                self.epoch_start_state.reference_gas_price(),
232                &transaction,
233                input_objects,
234                &receiving_objects,
235                &self.bytecode_verifier_metrics,
236                verifier_signing_config,
237                authenticator_gas_budget,
238            )?
239        } else {
240            let checked_input_objects = iota_transaction_checks::check_dev_inspect_input(
241                &self.protocol_config,
242                transaction.kind(),
243                input_objects,
244                receiving_objects,
245            )?;
246            let gas_status = IotaGasStatus::new(
247                transaction.gas_budget(),
248                transaction.gas_price(),
249                self.epoch_start_state.reference_gas_price(),
250                &self.protocol_config,
251            )?;
252
253            (gas_status, checked_input_objects)
254        };
255
256        // Execute the simulation
257        let (kind, signer, gas_data) = transaction.execution_parts();
258        let (inner_temp_store, _, effects, execution_result) =
259            self.executor.dev_inspect_transaction(
260                store.backing_store(),
261                &self.protocol_config,
262                self.limits_metrics.clone(),
263                false,           // expensive_checks
264                &HashSet::new(), // certificate_deny_set
265                &self.epoch_start_state.epoch(),
266                self.epoch_start_state.epoch_start_timestamp_ms(),
267                checked_input_objects,
268                gas_data,
269                gas_status,
270                kind,
271                signer,
272                transaction.digest(),
273                checks.disabled(),
274            );
275
276        Ok(SimulateTransactionResult {
277            input_objects: inner_temp_store.input_objects,
278            output_objects: inner_temp_store.written,
279            events: effects.events_digest().map(|_| inner_temp_store.events),
280            effects,
281            execution_result,
282            mock_gas_id,
283            suggested_gas_price: None,
284        })
285    }
286}