use crate::network_config::NetworkConfig;
use crate::simulation::{SimulationAdjustmentConfig, SimulationAdjustmentFactor};
use anyhow::{anyhow, ensure, Context, Result};
use soroban_env_host::e2e_invoke::entry_size_for_rent;
use soroban_env_host::{
fees::{
compute_rent_fee, compute_transaction_resource_fee, LedgerEntryRentChange,
TransactionResources,
},
ledger_info::get_key_durability,
storage::SnapshotSource,
xdr::{
BytesM, ContractDataDurability, DecoratedSignature, Duration, Hash, LedgerBounds,
LedgerEntryType, LedgerFootprint, LedgerKey, Memo, MuxedAccount, MuxedAccountMed25519,
Operation, OperationBody, Preconditions, PreconditionsV2, SequenceNumber, Signature,
SignatureHint, SignerKey, SignerKeyEd25519SignedPayload, SorobanResources,
SorobanResourcesExtV0, SorobanTransactionData, SorobanTransactionDataExt, TimeBounds,
TimePoint, Transaction, TransactionExt, TransactionV1Envelope, Uint256, WriteXdr,
},
LedgerInfo, DEFAULT_XDR_RW_LIMITS,
};
use std::convert::TryInto;
use std::rc::Rc;
impl SimulationAdjustmentConfig {
fn adjust_resources(&self, resources: &mut SorobanResources) {
resources.instructions = self.instructions.adjust_u32(resources.instructions);
resources.disk_read_bytes = self.read_bytes.adjust_u32(resources.disk_read_bytes);
resources.write_bytes = self.write_bytes.adjust_u32(resources.write_bytes);
}
}
impl SimulationAdjustmentFactor {
fn adjust_u32(&self, value: u32) -> u32 {
if value == 0 {
return 0;
}
value.saturating_add(self.additive_factor).max(
((value as f64) * self.multiplicative_factor)
.clamp(0.0, u32::MAX as f64)
.floor() as u32,
)
}
fn adjust_i64(&self, value: i64) -> i64 {
if value == 0 {
return 0;
}
value.saturating_add(self.additive_factor as i64).max(
((value as f64) * self.multiplicative_factor)
.clamp(0.0, i64::MAX as f64)
.floor() as i64,
)
}
}
pub(crate) fn compute_resource_fee(
network_config: &NetworkConfig,
ledger_info: &LedgerInfo,
resources: &TransactionResources,
rent_changes: &[LedgerEntryRentChange],
adjustment_config: &SimulationAdjustmentConfig,
) -> i64 {
let (non_refundable_fee, refundable_fee) =
compute_transaction_resource_fee(resources, &network_config.fee_configuration);
let rent_fee = compute_rent_fee(
rent_changes,
&network_config.rent_fee_configuration,
ledger_info.sequence_number,
);
let refundable_fee = adjustment_config
.refundable_fee
.adjust_i64(refundable_fee.saturating_add(rent_fee));
non_refundable_fee.saturating_add(refundable_fee)
}
pub(crate) fn compute_adjusted_transaction_resources(
operation: OperationBody,
simulated_operation_resources: &mut SorobanResources,
restored_rw_entry_ids: &Vec<u32>,
adjustment_config: &SimulationAdjustmentConfig,
contract_events_and_return_value_size: u32,
) -> Result<TransactionResources> {
adjustment_config.adjust_resources(simulated_operation_resources);
let mut disk_read_entries = 0;
for k in simulated_operation_resources.footprint.read_only.iter() {
match k {
LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => (),
_ => {
disk_read_entries += 1;
}
}
}
for k in simulated_operation_resources.footprint.read_write.iter() {
match k {
LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => (),
_ => {
disk_read_entries += 1;
}
}
}
disk_read_entries += restored_rw_entry_ids.len() as u32;
Ok(TransactionResources {
instructions: simulated_operation_resources.instructions,
disk_read_entries,
write_entries: simulated_operation_resources.footprint.read_write.len() as u32,
disk_read_bytes: simulated_operation_resources.disk_read_bytes,
write_bytes: simulated_operation_resources.write_bytes,
transaction_size_bytes: adjustment_config.tx_size.adjust_u32(
estimate_max_transaction_size_for_operation(
operation,
&simulated_operation_resources,
restored_rw_entry_ids,
)
.context("could not compute the maximum transaction size for operation")?,
),
contract_events_size_bytes: contract_events_and_return_value_size,
})
}
pub(crate) fn simulate_extend_ttl_op_resources(
keys_to_extend: &[LedgerKey],
snapshot: &impl SnapshotSource,
network_config: &NetworkConfig,
current_ledger_seq: u32,
extend_to: u32,
) -> Result<(SorobanResources, Vec<LedgerEntryRentChange>)> {
let mut rent_changes = Vec::<LedgerEntryRentChange>::with_capacity(keys_to_extend.len());
let mut extended_keys = Vec::<LedgerKey>::with_capacity(keys_to_extend.len());
let new_live_until_ledger = current_ledger_seq + extend_to;
for key in keys_to_extend {
let durability = get_key_durability(key).ok_or_else(|| anyhow!("Can't extend TTL for ledger entry with key `{:?}`. Only entries with TTL (contract data or code entries) can have it extended", key))?;
let entry_with_live_until = snapshot.get(&Rc::new(key.clone()))?;
let Some((entry, live_until)) = entry_with_live_until else {
continue;
};
let current_live_until_ledger = live_until.ok_or_else(|| {
anyhow!("Internal error: missing TTL for ledger key that must have TTL: `{key:?}`")
})?;
if new_live_until_ledger <= current_live_until_ledger {
continue;
}
ensure!(
current_live_until_ledger >= current_ledger_seq,
"Can't extend for an expired entry with key: {key:?}. The entry must be restored before it can be extended."
);
extended_keys.push(key.clone());
let budget = network_config.create_budget()?;
let entry_xdr_size = entry.to_xdr(DEFAULT_XDR_RW_LIMITS)?.len().try_into()?;
let entry_size: u32 = entry_size_for_rent(&budget, &entry, entry_xdr_size)?;
rent_changes.push(LedgerEntryRentChange {
is_persistent: durability == ContractDataDurability::Persistent,
is_code_entry: matches!(key.discriminant(), LedgerEntryType::ContractCode),
old_size_bytes: entry_size,
new_size_bytes: entry_size,
old_live_until_ledger: current_live_until_ledger,
new_live_until_ledger,
});
}
extended_keys.sort();
let resources = SorobanResources {
footprint: LedgerFootprint {
read_only: extended_keys.try_into()?,
read_write: Default::default(),
},
instructions: 0,
disk_read_bytes: 0,
write_bytes: 0,
};
Ok((resources, rent_changes))
}
pub(crate) fn simulate_restore_op_resources(
keys_to_restore: &[LedgerKey],
snapshot_source: &impl SnapshotSource,
network_config: &NetworkConfig,
ledger_info: &LedgerInfo,
) -> Result<(SorobanResources, Vec<LedgerEntryRentChange>)> {
let restored_live_until_ledger = ledger_info
.min_live_until_ledger_checked(ContractDataDurability::Persistent)
.ok_or_else(|| {
anyhow!("minimum persistent live until ledger overflows - ledger info is misconfigured")
})?;
let mut restored_bytes = 0_u32;
let mut rent_changes: Vec<LedgerEntryRentChange> = Vec::with_capacity(keys_to_restore.len());
let mut restored_keys = Vec::<LedgerKey>::with_capacity(keys_to_restore.len());
for key in keys_to_restore {
let durability = get_key_durability(key);
ensure!(
durability == Some(ContractDataDurability::Persistent),
"Can't restore a ledger entry with key: {key:?}. Only persistent ledger entries with TTL can be restored."
);
let entry_with_live_until = snapshot_source
.get(&Rc::new(key.clone()))?
.ok_or_else(|| anyhow!("Missing entry to restore for key {key:?}"))?;
let (entry, live_until) = entry_with_live_until;
let current_live_until_ledger = live_until.ok_or_else(|| {
anyhow!("Internal error: missing TTL for ledger key that must have TTL: `{key:?}`")
})?;
if current_live_until_ledger >= ledger_info.sequence_number {
continue;
}
restored_keys.push(key.clone());
let budget = network_config.create_budget()?;
let entry_xdr_size: u32 = entry.to_xdr(DEFAULT_XDR_RW_LIMITS)?.len().try_into()?;
let entry_rent_size: u32 = entry_size_for_rent(&budget, &entry, entry_xdr_size)?;
restored_bytes = restored_bytes.saturating_add(entry_xdr_size);
rent_changes.push(LedgerEntryRentChange {
is_persistent: true,
is_code_entry: matches!(key.discriminant(), LedgerEntryType::ContractCode),
old_size_bytes: 0,
new_size_bytes: entry_rent_size,
old_live_until_ledger: 0,
new_live_until_ledger: restored_live_until_ledger,
});
}
restored_keys.sort();
let resources = SorobanResources {
footprint: LedgerFootprint {
read_only: Default::default(),
read_write: restored_keys.try_into()?,
},
instructions: 0,
disk_read_bytes: restored_bytes,
write_bytes: restored_bytes,
};
Ok((resources, rent_changes))
}
fn estimate_max_transaction_size_for_operation(
operation: OperationBody,
resources: &SorobanResources,
restored_rw_entry_ids: &Vec<u32>,
) -> Result<u32> {
let source = MuxedAccount::MuxedEd25519(MuxedAccountMed25519 {
id: 0,
ed25519: Uint256([0; 32]),
});
let bytes64: BytesM<64> = vec![0; 64].try_into()?;
let signatures: Vec<DecoratedSignature> = vec![
DecoratedSignature {
hint: SignatureHint([0; 4]),
signature: Signature(bytes64.clone()),
};
20
];
let signer_key = SignerKey::Ed25519SignedPayload(SignerKeyEd25519SignedPayload {
ed25519: Uint256([0; 32]),
payload: bytes64.clone(),
});
let envelope = TransactionV1Envelope {
tx: Transaction {
source_account: source.clone(),
fee: 0,
seq_num: SequenceNumber(0),
cond: Preconditions::V2(PreconditionsV2 {
time_bounds: Some(TimeBounds {
min_time: TimePoint(0),
max_time: TimePoint(0),
}),
ledger_bounds: Some(LedgerBounds {
min_ledger: 0,
max_ledger: 0,
}),
min_seq_num: Some(SequenceNumber(0)),
min_seq_age: Duration(0),
min_seq_ledger_gap: 0,
extra_signers: vec![signer_key.clone(), signer_key].try_into()?,
}),
memo: Memo::Hash(Hash([0; 32])),
operations: vec![Operation {
source_account: Some(source),
body: operation,
}]
.try_into()?,
ext: TransactionExt::V1(SorobanTransactionData {
resources: SorobanResources {
footprint: resources.footprint.clone(),
instructions: 0,
disk_read_bytes: 0,
write_bytes: 0,
},
resource_fee: 0,
ext: SorobanTransactionDataExt::V1(SorobanResourcesExtV0 {
archived_soroban_entries: restored_rw_entry_ids.try_into()?,
}),
}),
},
signatures: signatures.try_into()?,
};
let envelope_xdr = envelope.to_xdr(DEFAULT_XDR_RW_LIMITS)?;
let envelope_size = envelope_xdr.len();
Ok(envelope_size.try_into()?)
}