Skip to main content

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_sdk_types::{Transaction, TransactionEffects};
14use iota_transaction_checks::VerifierLimitsSource;
15use iota_types::{
16    committee::{Committee, EpochId},
17    effects::TransactionEffectsAPI,
18    error::IotaResult,
19    gas::IotaGasStatus,
20    gas_coin::mock_simulation_gas_coin,
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    transaction::{ObjectReadResult, TransactionAPI, VerifiedTransaction},
28    transaction_executor::{SimulateTransactionResult, VmChecks},
29};
30
31use crate::SimulatorStore;
32
33pub struct EpochState {
34    epoch_start_state: EpochStartSystemState,
35    committee: Committee,
36    protocol_config: ProtocolConfig,
37    limits_metrics: Arc<LimitsMetrics>,
38    bytecode_verifier_metrics: Arc<BytecodeVerifierMetrics>,
39    executor: Arc<dyn Executor + Send + Sync>,
40    /// A counter that advances each time we advance the clock in order to
41    /// ensure that each update txn has a unique digest. This is reset on
42    /// epoch changes
43    next_consensus_round: u64,
44}
45
46impl EpochState {
47    pub fn new(system_state: IotaSystemState) -> Self {
48        let epoch_start_state = system_state.into_epoch_start_state();
49        let protocol_config =
50            ProtocolConfig::get_for_version(epoch_start_state.protocol_version(), Chain::Unknown);
51        Self::new_impl(protocol_config, epoch_start_state)
52    }
53
54    /// Like [`Self::new`], but reuses the given `protocol_config` instead of
55    /// resolving it from the protocol version, keeping any feature flags
56    /// customized for testing.
57    ///
58    /// # Panics
59    ///
60    /// Panics if the config's version does not match the system state's
61    /// protocol version.
62    pub fn new_with_config(protocol_config: ProtocolConfig, system_state: IotaSystemState) -> Self {
63        let epoch_start_state = system_state.into_epoch_start_state();
64        assert_eq!(
65            protocol_config.version,
66            epoch_start_state.protocol_version()
67        );
68        Self::new_impl(protocol_config, epoch_start_state)
69    }
70
71    fn new_impl(protocol_config: ProtocolConfig, epoch_start_state: EpochStartSystemState) -> Self {
72        let committee = epoch_start_state.get_iota_committee();
73        let registry = prometheus_filtered::Registry::new();
74        let limits_metrics = Arc::new(LimitsMetrics::new(&registry));
75        let bytecode_verifier_metrics = Arc::new(BytecodeVerifierMetrics::new(&registry));
76        let executor = iota_execution::executor(&protocol_config, true, None).unwrap();
77
78        Self {
79            epoch_start_state,
80            committee,
81            protocol_config,
82            limits_metrics,
83            bytecode_verifier_metrics,
84            executor,
85            next_consensus_round: 0,
86        }
87    }
88
89    pub fn epoch(&self) -> EpochId {
90        self.epoch_start_state.epoch()
91    }
92
93    pub fn reference_gas_price(&self) -> u64 {
94        self.epoch_start_state.reference_gas_price()
95    }
96
97    pub fn next_consensus_round(&mut self) -> u64 {
98        let round = self.next_consensus_round;
99        self.next_consensus_round += 1;
100        round
101    }
102
103    pub fn committee(&self) -> &Committee {
104        &self.committee
105    }
106
107    pub fn epoch_start_state(&self) -> EpochStartSystemState {
108        self.epoch_start_state.clone()
109    }
110
111    pub fn protocol_version(&self) -> ProtocolVersion {
112        self.protocol_config().version
113    }
114
115    pub fn protocol_config(&self) -> &ProtocolConfig {
116        &self.protocol_config
117    }
118
119    pub fn execute_transaction(
120        &self,
121        store: &dyn SimulatorStore,
122        deny_config: &TransactionDenyConfig,
123        verifier_signing_config: &VerifierSigningConfig,
124        transaction: &VerifiedTransaction,
125    ) -> Result<(
126        InnerTemporaryStore,
127        IotaGasStatus,
128        TransactionEffects,
129        Result<(), iota_types::error::ExecutionError>,
130    )> {
131        let tx_digest = *transaction.digest();
132        let tx = transaction.data().transaction();
133        let input_object_kinds = tx.input_objects()?;
134        let receiving_object_refs = tx.receiving_objects();
135
136        iota_transaction_checks::deny::check_transaction_for_validation(
137            tx,
138            transaction.signatures(),
139            &input_object_kinds,
140            &receiving_object_refs,
141            deny_config,
142            &store,
143        )?;
144
145        let (input_objects, receiving_objects) = store.read_objects_for_synchronous_execution(
146            &tx_digest,
147            &input_object_kinds,
148            &receiving_object_refs,
149        )?;
150
151        // `MoveAuthenticator`s are not supported in Simulacrum, so we set the
152        // `authenticator_gas_budget` to 0.
153        let authenticator_gas_budget = 0;
154
155        // Run the transaction input checks that would run when submitting the txn to a
156        // validator for signing
157        let (gas_status, checked_input_objects) = iota_transaction_checks::check_transaction_input(
158            &self.protocol_config,
159            self.epoch_start_state.reference_gas_price(),
160            transaction.data().transaction(),
161            input_objects,
162            &receiving_objects,
163            &self.bytecode_verifier_metrics,
164            VerifierLimitsSource::NodeConfig(verifier_signing_config),
165            authenticator_gas_budget,
166        )?;
167
168        let transaction = transaction.data().transaction();
169        let (kind, signer, gas_data) = transaction.execution_parts();
170        let (inner_temp_store, gas_status, effects, _timings, result) =
171            self.executor.execute_transaction_to_effects(
172                store.backing_store(),
173                &self.protocol_config,
174                self.limits_metrics.clone(),
175                false,           // enable_expensive_checks
176                &HashSet::new(), // certificate_deny_set
177                &self.epoch_start_state.epoch(),
178                self.epoch_start_state.epoch_start_timestamp_ms(),
179                checked_input_objects,
180                gas_data,
181                gas_status,
182                kind,
183                signer,
184                tx_digest,
185                &mut None,
186            );
187        Ok((inner_temp_store, gas_status, effects, result))
188    }
189
190    /// Simulate a transaction without committing changes.
191    /// This is similar to execute_transaction but:
192    /// - Takes Transaction instead of VerifiedTransaction (no signature required)
193    /// - Takes VmChecks parameter to control validation strictness
194    /// - Returns SimulateTransactionResult with input/output objects
195    /// - Creates a mock gas object if none provided
196    pub fn simulate_transaction(
197        &self,
198        store: &dyn SimulatorStore,
199        deny_config: &TransactionDenyConfig,
200        verifier_signing_config: &VerifierSigningConfig,
201        mut transaction: Transaction,
202        checks: VmChecks,
203    ) -> IotaResult<SimulateTransactionResult> {
204        transaction.check_serialized_size(&self.protocol_config)?;
205
206        // Cheap validity checks for a transaction, including input size limits.
207        transaction.validity_check_no_gas_check(&self.protocol_config)?;
208
209        // The full validity check caps the gas payment size alongside requiring a
210        // gas payment at all, which a simulation relaxes so it can mock one. The cap
211        // still applies, and is cheapest before any object is loaded.
212        transaction.check_gas_payment_size(&self.protocol_config)?;
213
214        let input_object_kinds = transaction.input_objects()?;
215        let receiving_object_refs = transaction.receiving_objects();
216
217        // Check if some transaction elements are denied
218        iota_transaction_checks::deny::check_transaction_for_validation(
219            &transaction,
220            &[],
221            &input_object_kinds,
222            &receiving_object_refs,
223            deny_config,
224            store,
225        )?;
226
227        // Load input and receiving objects
228        let (mut input_objects, receiving_objects) = store.read_objects_for_synchronous_execution(
229            &transaction.digest(),
230            &input_object_kinds,
231            &receiving_object_refs,
232        )?;
233
234        // Create a mock gas object if one was not provided
235        let mock_gas_id = if transaction.gas().is_empty() {
236            let mock_gas_object = mock_simulation_gas_coin(transaction.gas_data().owner);
237            let mock_gas_object_ref = mock_gas_object.object_ref();
238            transaction.gas_data_mut().objects = vec![mock_gas_object_ref];
239            input_objects.push(ObjectReadResult::new_from_gas_object(&mock_gas_object));
240            Some(mock_gas_object.id())
241        } else {
242            None
243        };
244
245        iota_types::gas::fill_in_unset_simulation_gas(
246            &mut transaction,
247            &input_objects,
248            self.epoch_start_state.reference_gas_price(),
249            &self.protocol_config,
250        );
251
252        // `MoveAuthenticator`s are not supported in Simulacrum, so we set the
253        // `authenticator_gas_budget` to 0.
254        let authenticator_gas_budget = 0;
255
256        // Checks enabled -> DRY-RUN (simulating a real TX)
257        // Checks disabled -> DEV-INSPECT (more relaxed Move VM checks)
258        let (gas_status, checked_input_objects) = if checks.enabled() {
259            iota_transaction_checks::check_transaction_input(
260                &self.protocol_config,
261                self.epoch_start_state.reference_gas_price(),
262                &transaction,
263                input_objects,
264                &receiving_objects,
265                &self.bytecode_verifier_metrics,
266                VerifierLimitsSource::NodeConfig(verifier_signing_config),
267                authenticator_gas_budget,
268            )?
269        } else {
270            // Execution smashes the gas coins and reserves the whole budget from them
271            // before running any command, treating the input checks as having verified
272            // that they are gas coins at all — so with those checks skipped here, this
273            // has to stand in for them. With the checks enabled,
274            // `check_transaction_input` covers it.
275            iota_types::gas::check_gas_coins_cover_budget_in_simulation(
276                &input_objects,
277                transaction.gas(),
278                transaction.gas_budget(),
279            )?;
280
281            let checked_input_objects = iota_transaction_checks::check_simulation_input(
282                &self.protocol_config,
283                transaction.kind(),
284                input_objects,
285                receiving_objects,
286            )?;
287            let gas_status = IotaGasStatus::new(
288                transaction.gas_budget(),
289                transaction.gas_price(),
290                self.epoch_start_state.reference_gas_price(),
291                &self.protocol_config,
292            )?;
293
294            (gas_status, checked_input_objects)
295        };
296
297        // Execute the simulation
298        let (kind, signer, gas_data) = transaction.execution_parts();
299        let (inner_temp_store, _, effects, execution_result) =
300            self.executor.dev_inspect_transaction(
301                store.backing_store(),
302                &self.protocol_config,
303                self.limits_metrics.clone(),
304                false,           // expensive_checks
305                &HashSet::new(), // certificate_deny_set
306                &self.epoch_start_state.epoch(),
307                self.epoch_start_state.epoch_start_timestamp_ms(),
308                checked_input_objects,
309                gas_data,
310                gas_status,
311                kind,
312                signer,
313                transaction.digest(),
314                checks.disabled(),
315            );
316
317        let mut input_objects = inner_temp_store.input_objects;
318        iota_types::storage::extend_input_objects_with_loaded_runtime_objects(
319            &mut input_objects,
320            &effects,
321            &inner_temp_store.loaded_runtime_objects,
322            store.backing_store().as_object_store(),
323        );
324
325        Ok(SimulateTransactionResult {
326            input_objects,
327            output_objects: inner_temp_store.written,
328            events: effects.events_digest().map(|_| inner_temp_store.events),
329            effects,
330            execution_result,
331            mock_gas_id,
332            suggested_gas_price: None,
333            gas_data: transaction.gas_data().clone(),
334        })
335    }
336}