use crate::network_config::NetworkConfig;
use crate::resources::{
compute_adjusted_transaction_resources, compute_resource_fee, simulate_extend_ttl_op_resources,
simulate_invoke_host_function_op_resources, simulate_restore_op_resources,
};
use crate::snapshot_source::{
SimulationSnapshotSource, SimulationSnapshotSourceWithArchive, SnapshotSourceWithArchive,
};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use soroban_env_host::budget::Budget;
use soroban_env_host::{Host, TryFromVal};
use soroban_env_host::{
e2e_invoke::invoke_host_function_in_recording_mode,
e2e_invoke::LedgerEntryChange,
storage::SnapshotSource,
xdr::{
AccountId, ContractEvent, DiagnosticEvent, HostFunction, InvokeHostFunctionOp, LedgerKey,
OperationBody, ScVal, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
},
xdr::{ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, ReadXdr, RestoreFootprintOp},
LedgerInfo, DEFAULT_XDR_RW_LIMITS,
};
use std::rc::Rc;
#[derive(Deserialize, Serialize, Default)]
pub struct SimulationAdjustmentFactor {
pub multiplicative_factor: f64,
pub additive_factor: u32,
}
#[derive(Deserialize, Serialize, Default)]
pub struct SimulationAdjustmentConfig {
pub instructions: SimulationAdjustmentFactor,
pub read_bytes: SimulationAdjustmentFactor,
pub write_bytes: SimulationAdjustmentFactor,
pub tx_size: SimulationAdjustmentFactor,
pub refundable_fee: SimulationAdjustmentFactor,
}
#[derive(Eq, PartialEq, Debug, Serialize)]
pub struct LedgerEntryDiff {
pub state_before: Option<LedgerEntry>,
pub state_after: Option<LedgerEntry>,
}
#[derive(Debug, Serialize)]
pub struct InvokeHostFunctionSimulationResult {
pub invoke_result: std::result::Result<ScVal, ScVal>,
pub auth: Vec<SorobanAuthorizationEntry>,
pub contract_events: Vec<ContractEvent>,
pub diagnostic_events: Vec<DiagnosticEvent>,
pub transaction_data: Option<SorobanTransactionData>,
pub simulated_instructions: u32,
pub simulated_memory: u32,
pub modified_entries: Vec<LedgerEntryDiff>,
}
#[derive(Eq, PartialEq, Debug)]
pub struct ExtendTtlOpSimulationResult {
pub transaction_data: SorobanTransactionData,
}
#[derive(Eq, PartialEq, Debug)]
pub struct RestoreOpSimulationResult {
pub transaction_data: SorobanTransactionData,
}
#[allow(clippy::too_many_arguments)]
pub fn simulate_invoke_host_function_op(
snapshot_source: Rc<dyn SnapshotSource>,
network_config: Option<NetworkConfig>,
adjustment_config: &SimulationAdjustmentConfig,
ledger_info: &LedgerInfo,
host_fn: HostFunction,
auth_entries: Option<Vec<SorobanAuthorizationEntry>>,
source_account: &AccountId,
base_prng_seed: [u8; 32],
enable_diagnostics: bool,
) -> Result<InvokeHostFunctionSimulationResult> {
let (budget, network_config) = if let Some(configs) = network_config {
(configs.create_budget()?, configs)
} else {
(Budget::default(), NetworkConfig::default())
};
let mut diagnostic_events = vec![];
let recording_result = invoke_host_function_in_recording_mode(
&budget,
enable_diagnostics,
&host_fn,
source_account,
auth_entries,
ledger_info.clone(),
snapshot_source.clone(),
base_prng_seed,
&mut diagnostic_events,
);
let invoke_result = match &recording_result {
Ok(r) => r.invoke_result.clone(),
Err(e) => Err(e.clone()),
};
let mut simulation_result = InvokeHostFunctionSimulationResult {
invoke_result: invoke_result.map_err(|e| ScVal::try_from_val(&Host::default(), &e.error.to_val()).unwrap()),
simulated_instructions: budget.get_cpu_insns_consumed()?.try_into()?,
simulated_memory: budget.get_mem_bytes_consumed()?.try_into()?,
diagnostic_events,
auth: vec![],
contract_events: vec![],
transaction_data: None,
modified_entries: vec![],
};
let Ok(recording_result) = recording_result else {
return Ok(simulation_result);
};
if recording_result.invoke_result.is_err() {
return Ok(simulation_result);
}
simulation_result.auth = recording_result.auth;
simulation_result.contract_events = recording_result.contract_events;
simulation_result.modified_entries =
extract_modified_entries(&*snapshot_source, &recording_result.ledger_changes)?;
let (mut resources, rent_changes) = simulate_invoke_host_function_op_resources(
&recording_result.ledger_changes,
simulation_result.simulated_instructions,
)?;
let operation = OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
host_function: host_fn,
auth: simulation_result.auth.clone().try_into()?,
});
let transaction_resources = compute_adjusted_transaction_resources(
operation,
&mut resources,
adjustment_config,
recording_result.contract_events_and_return_value_size,
)?;
let resource_fee = compute_resource_fee(
&network_config,
&ledger_info,
&transaction_resources,
&rent_changes,
adjustment_config,
);
simulation_result.transaction_data = Some(create_transaction_data(resources, resource_fee));
Ok(simulation_result)
}
pub fn simulate_extend_ttl_op(
snapshot_source: &impl SnapshotSource,
network_config: &NetworkConfig,
adjustment_config: &SimulationAdjustmentConfig,
ledger_info: &LedgerInfo,
keys_to_extend: &[LedgerKey],
extend_to: u32,
) -> Result<ExtendTtlOpSimulationResult> {
let snapshot_source = SimulationSnapshotSource::new(snapshot_source);
let (mut resources, rent_changes) = simulate_extend_ttl_op_resources(
keys_to_extend,
&snapshot_source,
ledger_info.sequence_number,
extend_to,
)?;
let operation = OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
ext: ExtensionPoint::V0,
extend_to,
});
let transaction_resources =
compute_adjusted_transaction_resources(operation, &mut resources, adjustment_config, 0)?;
let resource_fee = compute_resource_fee(
network_config,
&ledger_info,
&transaction_resources,
&rent_changes,
adjustment_config,
);
Ok(ExtendTtlOpSimulationResult {
transaction_data: create_transaction_data(resources, resource_fee),
})
}
pub fn simulate_restore_op(
snapshot_source: &impl SnapshotSourceWithArchive,
network_config: &NetworkConfig,
adjustment_config: &SimulationAdjustmentConfig,
ledger_info: &LedgerInfo,
keys_to_restore: &[LedgerKey],
) -> Result<RestoreOpSimulationResult> {
let snapshot_source = SimulationSnapshotSourceWithArchive::new(snapshot_source);
let (mut resources, rent_changes) =
simulate_restore_op_resources(keys_to_restore, &snapshot_source, ledger_info)?;
let operation = OperationBody::RestoreFootprint(RestoreFootprintOp {
ext: ExtensionPoint::V0,
});
let transaction_resources =
compute_adjusted_transaction_resources(operation, &mut resources, adjustment_config, 0)?;
let resource_fee = compute_resource_fee(
network_config,
&ledger_info,
&transaction_resources,
&rent_changes,
adjustment_config,
);
Ok(RestoreOpSimulationResult {
transaction_data: create_transaction_data(resources, resource_fee),
})
}
impl SimulationAdjustmentFactor {
pub fn new(multiplicative_factor: f64, additive_factor: u32) -> Self {
Self {
multiplicative_factor,
additive_factor,
}
}
pub fn no_adjustment() -> Self {
Self {
multiplicative_factor: 1.0,
additive_factor: 0,
}
}
}
impl SimulationAdjustmentConfig {
pub fn no_adjustments() -> Self {
Self {
instructions: SimulationAdjustmentFactor::no_adjustment(),
read_bytes: SimulationAdjustmentFactor::no_adjustment(),
write_bytes: SimulationAdjustmentFactor::no_adjustment(),
tx_size: SimulationAdjustmentFactor::no_adjustment(),
refundable_fee: SimulationAdjustmentFactor::no_adjustment(),
}
}
pub fn default_adjustment() -> Self {
Self {
instructions: SimulationAdjustmentFactor::new(1.04, 50_000),
read_bytes: SimulationAdjustmentFactor::no_adjustment(),
write_bytes: SimulationAdjustmentFactor::no_adjustment(),
tx_size: SimulationAdjustmentFactor::new(1.1, 500),
refundable_fee: SimulationAdjustmentFactor::new(1.15, 0),
}
}
}
fn create_transaction_data(
resources: SorobanResources,
resource_fee: i64,
) -> SorobanTransactionData {
SorobanTransactionData {
resources,
resource_fee,
ext: ExtensionPoint::V0,
}
}
fn extract_modified_entries(
snapshot: &(impl SnapshotSource + ?Sized),
ledger_changes: &[LedgerEntryChange],
) -> Result<Vec<LedgerEntryDiff>> {
let mut diffs = vec![];
for c in ledger_changes {
if c.read_only {
continue;
}
let key = LedgerKey::from_xdr(c.encoded_key.clone(), DEFAULT_XDR_RW_LIMITS)?;
let state_before = snapshot.get(&Rc::new(key))?.map(|v| v.0.as_ref().clone());
let state_after = match &c.encoded_new_value {
Some(v) => Some(LedgerEntry::from_xdr(v.clone(), DEFAULT_XDR_RW_LIMITS)?),
None => None,
};
diffs.push(LedgerEntryDiff {
state_before,
state_after,
});
}
Ok(diffs)
}