soroban_simulation/
simulation.rs

1use crate::network_config::NetworkConfig;
2use crate::resources::{
3    compute_adjusted_transaction_resources, compute_resource_fee, simulate_extend_ttl_op_resources,
4    simulate_restore_op_resources,
5};
6use crate::snapshot_source::SimulationSnapshotSource;
7use anyhow::Result;
8use soroban_env_host::e2e_invoke::extract_rent_changes;
9use soroban_env_host::xdr::SorobanResourcesExtV0;
10use soroban_env_host::{
11    e2e_invoke::invoke_host_function_in_recording_mode,
12    e2e_invoke::{LedgerEntryChange, RecordingInvocationAuthMode},
13    storage::SnapshotSource,
14    xdr::{
15        AccountId, ContractEvent, DiagnosticEvent, HostFunction, InvokeHostFunctionOp, LedgerKey,
16        OperationBody, ScVal, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
17        SorobanTransactionDataExt,
18    },
19    xdr::{ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, ReadXdr, RestoreFootprintOp},
20    HostError, LedgerInfo, DEFAULT_XDR_RW_LIMITS,
21};
22use std::rc::Rc;
23
24/// Configures the adjustment of a simulated value (e.g. resource or fee).
25/// The value is adjusted to be
26/// `max(value * multiplicative_factor, value + additive_factor)`
27pub struct SimulationAdjustmentFactor {
28    pub multiplicative_factor: f64,
29    pub additive_factor: u32,
30}
31
32/// Configuration for adjusting the resources and fees for a simulated
33/// transaction.
34pub struct SimulationAdjustmentConfig {
35    pub instructions: SimulationAdjustmentFactor,
36    pub read_bytes: SimulationAdjustmentFactor,
37    pub write_bytes: SimulationAdjustmentFactor,
38    pub tx_size: SimulationAdjustmentFactor,
39    pub refundable_fee: SimulationAdjustmentFactor,
40}
41
42/// Represents the state of a `LedgerEntry` before and after the
43/// transaction execution.
44/// `None` represents that entry was not present or removed.
45#[derive(Eq, PartialEq, Debug)]
46pub struct LedgerEntryDiff {
47    pub state_before: Option<LedgerEntry>,
48    pub state_after: Option<LedgerEntry>,
49}
50
51/// Result of simulating `InvokeHostFunctionOp` operation.
52#[derive(Debug)]
53pub struct InvokeHostFunctionSimulationResult {
54    /// Result value of the invoked function or error returned for invocation.
55    pub invoke_result: std::result::Result<ScVal, HostError>,
56    /// Authorization data, either passed through from the call (when provided),
57    /// or recorded during the invocation.
58    pub auth: Vec<SorobanAuthorizationEntry>,
59    /// All the events that contracts emitted during invocation.
60    /// Empty for failed invocations.
61    pub contract_events: Vec<ContractEvent>,
62    /// Diagnostic events recorded during simulation.
63    /// This is populated when diagnostics is enabled and even when the
64    /// invocation fails.
65    pub diagnostic_events: Vec<DiagnosticEvent>,
66    /// Soroban transaction extension containing simulated resources and
67    /// the estimated resource fee.
68    /// `None` for failed invocations.
69    pub transaction_data: Option<SorobanTransactionData>,
70    /// The number of CPU instructions metered during the simulation,
71    /// without any adjustments applied.
72    /// This is expected to not match `transaction_data` in case if
73    /// instructions are adjusted via `SimulationAdjustmentConfig`.
74    pub simulated_instructions: u32,
75    /// The number of memory bytes metered during the simulation,
76    /// without any adjustments applied.
77    pub simulated_memory: u32,
78    /// Differences for any RW entries that have been modified during
79    /// the transaction execution.
80    /// Empty for failed invocations.
81    pub modified_entries: Vec<LedgerEntryDiff>,
82}
83
84/// Result of simulating `ExtendFootprintTtlOp` operation.
85#[derive(Eq, PartialEq, Debug)]
86pub struct ExtendTtlOpSimulationResult {
87    /// Soroban transaction extension containing simulated resources and
88    /// the estimated resource fee.
89    pub transaction_data: SorobanTransactionData,
90}
91
92/// Result of simulating `RestoreFootprintOp` operation.
93#[derive(Eq, PartialEq, Debug)]
94pub struct RestoreOpSimulationResult {
95    /// Soroban transaction extension containing simulated resources and
96    /// the estimated resource fee.
97    pub transaction_data: SorobanTransactionData,
98}
99
100/// Simulates `InvokeHostFunctionOp` operation specified via its
101/// relevant payload parts.
102///
103/// The operation is defined by the host function itself (`host_fn`)
104/// and `auth_mode`. In case if `auth_mode` is `None`, the simulation will
105/// use recording authorization mode and  return non-signed recorded
106/// authorization entries. Otherwise, the signed entries will be used for
107/// authorization and authentication enforcement.
108///
109/// The rest of parameters define the ledger state (`snapshot_source`,
110/// `network_config`, `ledger_info`), simulation adjustment
111/// configuration (`adjustment_config`), and transaction execution
112/// parameters (`source_account`, `base_prng_seed`).
113///
114/// `enable_diagnostics` enables recording of `diagnostic_events` in the
115/// response.
116///
117/// This function makes the best effort at returning non-Err result even
118/// for failed invocations. It should only fail if ledger is
119/// mis-configured (e.g. when computed fees cause overflows).
120#[allow(clippy::too_many_arguments)]
121pub fn simulate_invoke_host_function_op(
122    snapshot_source: Rc<dyn SnapshotSource>,
123    network_config: &NetworkConfig,
124    adjustment_config: &SimulationAdjustmentConfig,
125    ledger_info: &LedgerInfo,
126    host_fn: HostFunction,
127    auth_mode: RecordingInvocationAuthMode,
128    source_account: &AccountId,
129    base_prng_seed: [u8; 32],
130    enable_diagnostics: bool,
131) -> Result<InvokeHostFunctionSimulationResult> {
132    let snapshot_source = Rc::new(SimulationSnapshotSource::new_from_rc(snapshot_source));
133    let budget = network_config.create_budget()?;
134    let mut diagnostic_events = vec![];
135    let recording_result = invoke_host_function_in_recording_mode(
136        &budget,
137        enable_diagnostics,
138        &host_fn,
139        source_account,
140        auth_mode,
141        ledger_info.clone(),
142        snapshot_source.clone(),
143        base_prng_seed,
144        &mut diagnostic_events,
145    );
146    let invoke_result = match &recording_result {
147        Ok(r) => r.invoke_result.clone(),
148        Err(e) => Err(e.clone()),
149    };
150    // We try to fill the simulation result as much as possible:
151    // diagnostics can be populated unconditionally, and we can always
152    // store the budget measurements.
153    let mut simulation_result = InvokeHostFunctionSimulationResult {
154        // Don't distinguish between the errors that happen during invocation vs
155        // during setup as that seems too granular.
156        invoke_result,
157        simulated_instructions: budget.get_cpu_insns_consumed()?.try_into()?,
158        simulated_memory: budget.get_mem_bytes_consumed()?.try_into()?,
159        diagnostic_events,
160        // Fields that should only be populated for successful invocations.
161        auth: vec![],
162        contract_events: vec![],
163        transaction_data: None,
164        modified_entries: vec![],
165    };
166    let Ok(recording_result) = recording_result else {
167        return Ok(simulation_result);
168    };
169    if recording_result.invoke_result.is_err() {
170        return Ok(simulation_result);
171    }
172    // Fill the remaining fields only for successful invocations.
173    simulation_result.auth = recording_result.auth;
174    simulation_result.contract_events = recording_result.contract_events;
175    simulation_result.modified_entries = extract_modified_entries(
176        &*snapshot_source,
177        &recording_result.ledger_changes,
178        &ledger_info,
179    )?;
180    let mut resources = recording_result.resources;
181    let rent_changes = extract_rent_changes(&recording_result.ledger_changes);
182    let operation = OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
183        host_function: host_fn,
184        auth: simulation_result.auth.clone().try_into()?,
185    });
186    let transaction_resources = compute_adjusted_transaction_resources(
187        operation,
188        &mut resources,
189        &recording_result.restored_rw_entry_indices,
190        adjustment_config,
191        recording_result.contract_events_and_return_value_size,
192    )?;
193    let resource_fee = compute_resource_fee(
194        network_config,
195        &ledger_info,
196        &transaction_resources,
197        &rent_changes,
198        adjustment_config,
199    );
200    simulation_result.transaction_data = Some(create_transaction_data(
201        resources,
202        &recording_result.restored_rw_entry_indices,
203        resource_fee,
204    )?);
205
206    Ok(simulation_result)
207}
208
209/// Simulates `ExtendFootprintTtlOp` operation specified via its
210/// relevant payload parts.
211///
212/// The operation is defined by the `keys_to_extend` and the
213/// `extend_to`. The TTL for the provided keys will be extended to
214/// become `ledger_info.sequence_number + extend_to`. Entries that
215/// don't exist in the `snapshot_source` and entries that already
216/// have TTL bigger than the requested extension will be ignored
217/// and excluded from the simulation results.
218///
219/// The rest of parameters define the ledger state (`snapshot_source`,
220/// `network_config`, `ledger_info`) and simulation adjustment
221/// configuration (`adjustment_config`).
222///
223/// This may only return error in case if ledger is mis-configured.
224pub fn simulate_extend_ttl_op(
225    snapshot_source: &impl SnapshotSource,
226    network_config: &NetworkConfig,
227    adjustment_config: &SimulationAdjustmentConfig,
228    ledger_info: &LedgerInfo,
229    keys_to_extend: &[LedgerKey],
230    extend_to: u32,
231) -> Result<ExtendTtlOpSimulationResult> {
232    let snapshot_source = SimulationSnapshotSource::new(snapshot_source);
233    let (mut resources, rent_changes) = simulate_extend_ttl_op_resources(
234        keys_to_extend,
235        &snapshot_source,
236        network_config,
237        ledger_info.sequence_number,
238        extend_to,
239    )?;
240    let operation = OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
241        ext: ExtensionPoint::V0,
242        extend_to,
243    });
244    let transaction_resources = compute_adjusted_transaction_resources(
245        operation,
246        &mut resources,
247        &vec![],
248        adjustment_config,
249        0,
250    )?;
251    let resource_fee = compute_resource_fee(
252        network_config,
253        &ledger_info,
254        &transaction_resources,
255        &rent_changes,
256        adjustment_config,
257    );
258    Ok(ExtendTtlOpSimulationResult {
259        transaction_data: create_transaction_data(resources, &vec![], resource_fee)?,
260    })
261}
262
263/// Simulates `RestoreFootprintTtlOp` operation specified via its
264/// relevant payload parts.
265///
266/// The operation is defined by the specified `keys_to_restore`. The
267/// keys will be restored with TTL set to
268/// `ledger_info.sequence_number + ledger_info.min_persistent_entry_ttl - 1`.
269/// Live entries will be ignored and excluded from the simulation results.
270///
271/// The rest of parameters define the ledger state (`snapshot_source`,
272/// `network_config`, `ledger_info`) and simulation adjustment
273/// configuration (`adjustment_config`). Note, that the `snapshot_source`
274/// has to be able to provide the access to the archived entries.
275///
276/// This will return error if a key can't be restored due to either having
277/// incorrect type/durability (e.g. a temp entry), if a key is missing from
278/// `snapshot_source`, or in case of ledger mis-configuration.
279pub fn simulate_restore_op(
280    snapshot_source: &impl SnapshotSource,
281    network_config: &NetworkConfig,
282    adjustment_config: &SimulationAdjustmentConfig,
283    ledger_info: &LedgerInfo,
284    keys_to_restore: &[LedgerKey],
285) -> Result<RestoreOpSimulationResult> {
286    let snapshot_source = SimulationSnapshotSource::new(snapshot_source);
287    let (mut resources, rent_changes) = simulate_restore_op_resources(
288        keys_to_restore,
289        &snapshot_source,
290        network_config,
291        ledger_info,
292    )?;
293    let operation = OperationBody::RestoreFootprint(RestoreFootprintOp {
294        ext: ExtensionPoint::V0,
295    });
296    let transaction_resources = compute_adjusted_transaction_resources(
297        operation,
298        &mut resources,
299        &vec![],
300        adjustment_config,
301        0,
302    )?;
303    let resource_fee = compute_resource_fee(
304        network_config,
305        &ledger_info,
306        &transaction_resources,
307        &rent_changes,
308        adjustment_config,
309    );
310    Ok(RestoreOpSimulationResult {
311        transaction_data: create_transaction_data(resources, &vec![], resource_fee)?,
312    })
313}
314
315impl SimulationAdjustmentFactor {
316    pub fn new(multiplicative_factor: f64, additive_factor: u32) -> Self {
317        Self {
318            multiplicative_factor,
319            additive_factor,
320        }
321    }
322
323    pub fn no_adjustment() -> Self {
324        Self {
325            multiplicative_factor: 1.0,
326            additive_factor: 0,
327        }
328    }
329}
330
331impl SimulationAdjustmentConfig {
332    pub fn no_adjustments() -> Self {
333        Self {
334            instructions: SimulationAdjustmentFactor::no_adjustment(),
335            read_bytes: SimulationAdjustmentFactor::no_adjustment(),
336            write_bytes: SimulationAdjustmentFactor::no_adjustment(),
337            tx_size: SimulationAdjustmentFactor::no_adjustment(),
338            refundable_fee: SimulationAdjustmentFactor::no_adjustment(),
339        }
340    }
341
342    pub fn default_adjustment() -> Self {
343        Self {
344            instructions: SimulationAdjustmentFactor::new(1.04, 50_000),
345            read_bytes: SimulationAdjustmentFactor::no_adjustment(),
346            write_bytes: SimulationAdjustmentFactor::no_adjustment(),
347            // It's safe to have pretty significant adjustment for tx size, as
348            // unused fee will be refunded.
349            tx_size: SimulationAdjustmentFactor::new(1.1, 500),
350            refundable_fee: SimulationAdjustmentFactor::new(1.15, 0),
351        }
352    }
353}
354
355fn create_transaction_data(
356    resources: SorobanResources,
357    restored_rw_entry_ids: &Vec<u32>,
358    resource_fee: i64,
359) -> Result<SorobanTransactionData> {
360    Ok(SorobanTransactionData {
361        resources,
362        resource_fee,
363        ext: if restored_rw_entry_ids.is_empty() {
364            SorobanTransactionDataExt::V0
365        } else {
366            SorobanTransactionDataExt::V1(SorobanResourcesExtV0 {
367                archived_soroban_entries: restored_rw_entry_ids.try_into()?,
368            })
369        },
370    })
371}
372
373fn extract_modified_entries(
374    snapshot: &(impl SnapshotSource + ?Sized),
375    ledger_changes: &[LedgerEntryChange],
376    ledger_info: &LedgerInfo,
377) -> Result<Vec<LedgerEntryDiff>> {
378    let mut diffs = vec![];
379    for c in ledger_changes {
380        if c.read_only {
381            continue;
382        }
383        let key = LedgerKey::from_xdr(c.encoded_key.clone(), DEFAULT_XDR_RW_LIMITS)?;
384        let state_before =
385            if let Some((entry_before, live_until_before)) = snapshot.get(&Rc::new(key))? {
386                let mut state_before = Some(entry_before.as_ref().clone());
387                if let Some(live_until_before) = live_until_before {
388                    if live_until_before < ledger_info.sequence_number {
389                        state_before = None;
390                    }
391                }
392                state_before
393            } else {
394                None
395            };
396
397        let state_after = match &c.encoded_new_value {
398            Some(v) => Some(LedgerEntry::from_xdr(v.clone(), DEFAULT_XDR_RW_LIMITS)?),
399            None => None,
400        };
401        diffs.push(LedgerEntryDiff {
402            state_before,
403            state_after,
404        });
405    }
406    Ok(diffs)
407}