From 5367758a59ce940b3f250ecd63b9990a1502d69a Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Wed, 13 Mar 2024 19:11:21 -0400 Subject: [PATCH] Emulate account entry extensions in simulation. (#1363) ### What Emulate account entry extensions in simulation. This also adds a general mechanism for modifying any ledger entries retrieved from the snapshot. ### Why Improve simulation accuracy. Fixes https://github.com/stellar/rs-soroban-env/issues/1357 ### Known limitations N/A --- soroban-simulation/src/simulation.rs | 11 +- soroban-simulation/src/snapshot_source.rs | 175 ++++++++++++++- soroban-simulation/src/test/simulation.rs | 10 +- .../src/test/snapshot_source.rs | 200 +++++++++++++++++- 4 files changed, 384 insertions(+), 12 deletions(-) diff --git a/soroban-simulation/src/simulation.rs b/soroban-simulation/src/simulation.rs index 3298a8dfe..3203df069 100644 --- a/soroban-simulation/src/simulation.rs +++ b/soroban-simulation/src/simulation.rs @@ -3,7 +3,9 @@ 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::SnapshotSourceWithArchive; +use crate::snapshot_source::{ + SimulationSnapshotSource, SimulationSnapshotSourceWithArchive, SnapshotSourceWithArchive, +}; use anyhow::Result; use soroban_env_host::{ e2e_invoke::invoke_host_function_in_recording_mode, @@ -125,6 +127,7 @@ pub fn simulate_invoke_host_function_op( base_prng_seed: [u8; 32], enable_diagnostics: bool, ) -> Result { + let snapshot_source = Rc::new(SimulationSnapshotSource::new_from_rc(snapshot_source)); let budget = network_config.create_budget()?; let mut diagnostic_events = vec![]; let recording_result = invoke_host_function_in_recording_mode( @@ -219,9 +222,10 @@ pub fn simulate_extend_ttl_op( keys_to_extend: &[LedgerKey], extend_to: u32, ) -> Result { + let snapshot_source = SimulationSnapshotSource::new(snapshot_source); let (mut resources, rent_changes) = simulate_extend_ttl_op_resources( keys_to_extend, - snapshot_source, + &snapshot_source, ledger_info.sequence_number, extend_to, )?; @@ -266,8 +270,9 @@ pub fn simulate_restore_op( ledger_info: &LedgerInfo, keys_to_restore: &[LedgerKey], ) -> Result { + let snapshot_source = SimulationSnapshotSourceWithArchive::new(snapshot_source); let (mut resources, rent_changes) = - simulate_restore_op_resources(keys_to_restore, snapshot_source, ledger_info)?; + simulate_restore_op_resources(keys_to_restore, &snapshot_source, ledger_info)?; let operation = OperationBody::RestoreFootprint(RestoreFootprintOp { ext: ExtensionPoint::V0, }); diff --git a/soroban-simulation/src/snapshot_source.rs b/soroban-simulation/src/snapshot_source.rs index 1a4802112..f2eb15537 100644 --- a/soroban-simulation/src/snapshot_source.rs +++ b/soroban-simulation/src/snapshot_source.rs @@ -3,6 +3,11 @@ use crate::simulation::{ simulate_restore_op, RestoreOpSimulationResult, SimulationAdjustmentConfig, }; use anyhow::{anyhow, Result}; +use soroban_env_host::xdr::{ + AccountEntry, AccountEntryExt, AccountEntryExtensionV1, AccountEntryExtensionV1Ext, + AccountEntryExtensionV2, AccountEntryExtensionV2Ext, AccountEntryExtensionV3, ExtensionPoint, + LedgerEntryData, Liabilities, SponsorshipDescriptor, TimePoint, +}; use soroban_env_host::{ ledger_info::get_key_durability, storage::{EntryWithLiveUntil, SnapshotSource}, @@ -10,7 +15,7 @@ use soroban_env_host::{ HostError, LedgerInfo, }; use std::cell::RefCell; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; /// Read-only ledger snapshot accessor that also has access @@ -115,3 +120,171 @@ impl SnapshotSource for AutoRestoringSnapshotSourc } } } + +#[derive(Default)] +struct LedgerEntryUpdater { + updated_entries_cache: BTreeMap, Option>, +} + +impl LedgerEntryUpdater { + fn maybe_update_entry( + &mut self, + key: &Rc, + entry: Option, + ) -> Option { + if let Some(e) = self.updated_entries_cache.get(key) { + return e.clone(); + } + if let Some((entry, live_until)) = &entry { + match &entry.data { + LedgerEntryData::Account(_) => { + let mut updated_entry = (**entry).clone(); + match &mut updated_entry.data { + LedgerEntryData::Account(acc) => { + update_account_entry(acc); + } + _ => (), + } + let entry_with_live_until = Some((Rc::new(updated_entry), *live_until)); + self.updated_entries_cache + .insert(key.clone(), entry_with_live_until.clone()); + return entry_with_live_until; + } + _ => (), + } + } + entry + } +} + +enum SnapshotSourceHolder<'a> { + Ref(&'a dyn SnapshotSource), + Rc(Rc), +} + +impl<'a> SnapshotSource for SnapshotSourceHolder<'a> { + fn get(&self, key: &Rc) -> Result, HostError> { + match self { + SnapshotSourceHolder::Ref(r) => r.get(key), + SnapshotSourceHolder::Rc(r) => r.get(key), + } + } +} + +// This is an internal wrapper for the snapshot sources used in the simulation. +// The purpose of the wrapper is to emulate any ledger entry modifying logic +// that Core might perform before the Soroban host operation is invoked. For example, +// Core might create the account extensions, which would impact the read bytes +// amount and simulated CPU instructions. +pub(crate) struct SimulationSnapshotSource<'a> { + inner_snapshot: SnapshotSourceHolder<'a>, + entry_updater: RefCell, +} + +// This is the same as `SimulationSnapshotSource`, but for +// `SnapshotSourceWithArchive` trait. +// Note, that we don't implement both traits for `SimulationSnapshotSource` in order +// to avoid confusion: we never want to accidentally create a `SnapshotSource` with +// `SnapshotSourceWithArchive` inner snapshot, or vice versa. +pub(crate) struct SimulationSnapshotSourceWithArchive<'a, T: SnapshotSourceWithArchive> { + inner_snapshot: &'a T, + entry_updater: RefCell, +} + +impl<'a> SimulationSnapshotSource<'a> { + pub(crate) fn new(snapshot: &'a dyn SnapshotSource) -> Self { + Self { + inner_snapshot: SnapshotSourceHolder::Ref(snapshot), + entry_updater: RefCell::new(Default::default()), + } + } + + pub(crate) fn new_from_rc(snapshot: Rc) -> Self { + Self { + inner_snapshot: SnapshotSourceHolder::Rc(snapshot), + entry_updater: RefCell::new(Default::default()), + } + } +} + +impl<'a, T: SnapshotSourceWithArchive> SimulationSnapshotSourceWithArchive<'a, T> { + pub(crate) fn new(snapshot: &'a T) -> Self { + Self { + inner_snapshot: &snapshot, + entry_updater: RefCell::new(Default::default()), + } + } +} + +impl<'a> SnapshotSource for SimulationSnapshotSource<'a> { + fn get(&self, key: &Rc) -> Result, HostError> { + Ok(self + .entry_updater + .borrow_mut() + .maybe_update_entry(key, self.inner_snapshot.get(key)?)) + } +} + +impl<'a, T: SnapshotSourceWithArchive> SnapshotSourceWithArchive + for SimulationSnapshotSourceWithArchive<'a, T> +{ + fn get_including_archived( + &self, + key: &Rc, + ) -> Result, HostError> { + Ok(self + .entry_updater + .borrow_mut() + .maybe_update_entry(key, self.inner_snapshot.get_including_archived(key)?)) + } +} + +fn update_account_entry(account_entry: &mut AccountEntry) { + match &mut account_entry.ext { + AccountEntryExt::V0 => { + let mut ext = AccountEntryExtensionV1 { + liabilities: Liabilities { + buying: 0, + selling: 0, + }, + ext: AccountEntryExtensionV1Ext::V0, + }; + fill_account_ext_v2(&mut ext, account_entry.signers.len()); + account_entry.ext = AccountEntryExt::V1(ext); + } + AccountEntryExt::V1(ext) => { + fill_account_ext_v2(ext, account_entry.signers.len()); + } + } +} + +fn fill_account_ext_v2(account_ext_v1: &mut AccountEntryExtensionV1, signers_count: usize) { + match &mut account_ext_v1.ext { + AccountEntryExtensionV1Ext::V0 => { + let mut ext = AccountEntryExtensionV2 { + num_sponsored: 0, + num_sponsoring: 0, + signer_sponsoring_i_ds: vec![SponsorshipDescriptor(None); signers_count] + .try_into() + .unwrap_or_default(), + ext: AccountEntryExtensionV2Ext::V0, + }; + fill_account_ext_v3(&mut ext); + account_ext_v1.ext = AccountEntryExtensionV1Ext::V2(ext); + } + AccountEntryExtensionV1Ext::V2(ext) => fill_account_ext_v3(ext), + } +} + +fn fill_account_ext_v3(account_ext_v2: &mut AccountEntryExtensionV2) { + match account_ext_v2.ext { + AccountEntryExtensionV2Ext::V0 => { + account_ext_v2.ext = AccountEntryExtensionV2Ext::V3(AccountEntryExtensionV3 { + ext: ExtensionPoint::V0, + seq_ledger: 0, + seq_time: TimePoint(0), + }); + } + AccountEntryExtensionV2Ext::V3(_) => (), + } +} diff --git a/soroban-simulation/src/test/simulation.rs b/soroban-simulation/src/test/simulation.rs index e1f98afef..5abc26ca6 100644 --- a/soroban-simulation/src/test/simulation.rs +++ b/soroban-simulation/src/test/simulation.rs @@ -481,15 +481,15 @@ fn test_simulate_invoke_contract_with_auth() { .try_into() .unwrap() }, - instructions: 38139496, - read_bytes: 7440, + instructions: 38210172, + read_bytes: 7492, write_bytes: 76, }, - resource_fee: 39290, + resource_fee: 39363, }) ); - assert_eq!(res.simulated_instructions, 38139496); - assert_eq!(res.simulated_memory, 19069620); + assert_eq!(res.simulated_instructions, 38210172); + assert_eq!(res.simulated_memory, 19104958); assert_eq!( res.modified_entries, vec![LedgerEntryDiff { diff --git a/soroban-simulation/src/test/snapshot_source.rs b/soroban-simulation/src/test/snapshot_source.rs index 6968694dd..a3761279b 100644 --- a/soroban-simulation/src/test/snapshot_source.rs +++ b/soroban-simulation/src/test/snapshot_source.rs @@ -1,13 +1,17 @@ use crate::network_config::NetworkConfig; use crate::simulation::{RestoreOpSimulationResult, SimulationAdjustmentConfig}; -use crate::snapshot_source::AutoRestoringSnapshotSource; +use crate::snapshot_source::{AutoRestoringSnapshotSource, SimulationSnapshotSource}; use crate::testutils::{ledger_entry_to_ledger_key, temp_entry, MockSnapshotSource}; use pretty_assertions::assert_eq; -use soroban_env_host::e2e_testutils::wasm_entry; +use soroban_env_host::e2e_testutils::{account_entry, get_account_id, ledger_entry, wasm_entry}; use soroban_env_host::fees::{FeeConfiguration, RentFeeConfiguration}; use soroban_env_host::storage::SnapshotSource; use soroban_env_host::xdr::{ - ExtensionPoint, LedgerFootprint, SorobanResources, SorobanTransactionData, + AccountEntry, AccountEntryExt, AccountEntryExtensionV1, AccountEntryExtensionV1Ext, + AccountEntryExtensionV2, AccountEntryExtensionV2Ext, AccountEntryExtensionV3, ExtensionPoint, + LedgerEntryData, LedgerFootprint, Liabilities, SequenceNumber, Signer, SignerKey, + SorobanResources, SorobanTransactionData, SponsorshipDescriptor, Thresholds, TimePoint, + Uint256, }; use soroban_env_host::LedgerInfo; use std::rc::Rc; @@ -176,3 +180,193 @@ fn test_automatic_restoration() { None ); } + +#[test] +fn test_simulation_snapshot_source_creates_account_extensions() { + let account_1 = get_account_id([111; 32]); + let account_2 = get_account_id([222; 32]); + let account_3 = get_account_id([33; 32]); + let mut account_without_extensions = account_entry(&account_1); + match &mut account_without_extensions.data { + LedgerEntryData::Account(acc) => { + acc.signers = vec![ + Signer { + key: SignerKey::Ed25519(Uint256([1; 32])), + weight: 1, + }, + Signer { + key: SignerKey::Ed25519(Uint256([2; 32])), + weight: 2, + }, + ] + .try_into() + .unwrap(); + } + _ => (), + } + let mut account_with_ext_v2 = account_entry(&account_2); + match &mut account_with_ext_v2.data { + LedgerEntryData::Account(acc) => { + acc.signers = vec![Signer { + key: SignerKey::Ed25519(Uint256([1; 32])), + weight: 1, + }] + .try_into() + .unwrap(); + acc.ext = AccountEntryExt::V1(AccountEntryExtensionV1 { + liabilities: Liabilities { + buying: 123, + selling: 456, + }, + ext: AccountEntryExtensionV1Ext::V2(AccountEntryExtensionV2 { + num_sponsored: 5, + num_sponsoring: 6, + signer_sponsoring_i_ds: Default::default(), + ext: AccountEntryExtensionV2Ext::V0, + }), + }); + } + _ => (), + } + let mut account_with_ext_v3 = account_entry(&account_3); + match &mut account_with_ext_v3.data { + LedgerEntryData::Account(acc) => { + acc.ext = AccountEntryExt::V1(AccountEntryExtensionV1 { + liabilities: Liabilities { + buying: 789, + selling: 987, + }, + ext: AccountEntryExtensionV1Ext::V2(AccountEntryExtensionV2 { + num_sponsored: 2, + num_sponsoring: 3, + signer_sponsoring_i_ds: Default::default(), + ext: AccountEntryExtensionV2Ext::V3(AccountEntryExtensionV3 { + ext: ExtensionPoint::V0, + seq_ledger: 150, + seq_time: TimePoint(111), + }), + }), + }); + } + _ => (), + }; + let inner_snapshot = MockSnapshotSource::from_entries( + vec![ + (account_without_extensions.clone(), None), + (account_with_ext_v2.clone(), None), + (account_with_ext_v3.clone(), None), + ], + 300, + ) + .unwrap(); + let snapshot = SimulationSnapshotSource::new(&inner_snapshot); + assert_eq!( + snapshot + .get(&Rc::new( + ledger_entry_to_ledger_key(&account_entry(&get_account_id([0; 32]))).unwrap() + )) + .unwrap(), + None + ); + assert_eq!( + snapshot + .get(&Rc::new( + ledger_entry_to_ledger_key(&account_without_extensions).unwrap() + )) + .unwrap(), + Some(( + Rc::new(ledger_entry(LedgerEntryData::Account(AccountEntry { + account_id: get_account_id([111; 32]), + balance: 10_000_000, + seq_num: SequenceNumber(0), + num_sub_entries: 0, + inflation_dest: None, + flags: 0, + home_domain: Default::default(), + thresholds: Thresholds([1, 0, 0, 0]), + signers: vec![ + Signer { + key: SignerKey::Ed25519(Uint256([1; 32])), + weight: 1, + }, + Signer { + key: SignerKey::Ed25519(Uint256([2; 32])), + weight: 2, + }, + ] + .try_into() + .unwrap(), + ext: AccountEntryExt::V1(AccountEntryExtensionV1 { + liabilities: Liabilities { + buying: 0, + selling: 0, + }, + ext: AccountEntryExtensionV1Ext::V2(AccountEntryExtensionV2 { + num_sponsored: 0, + num_sponsoring: 0, + signer_sponsoring_i_ds: vec![SponsorshipDescriptor(None); 2] + .try_into() + .unwrap(), + ext: AccountEntryExtensionV2Ext::V3(AccountEntryExtensionV3 { + ext: ExtensionPoint::V0, + seq_ledger: 0, + seq_time: TimePoint(0), + }), + }), + }), + }))), + None + )) + ); + + assert_eq!( + snapshot + .get(&Rc::new( + ledger_entry_to_ledger_key(&account_with_ext_v2).unwrap() + )) + .unwrap(), + Some(( + Rc::new(ledger_entry(LedgerEntryData::Account(AccountEntry { + account_id: get_account_id([222; 32]), + balance: 10_000_000, + seq_num: SequenceNumber(0), + num_sub_entries: 0, + inflation_dest: None, + flags: 0, + home_domain: Default::default(), + thresholds: Thresholds([1, 0, 0, 0]), + signers: vec![Signer { + key: SignerKey::Ed25519(Uint256([1; 32])), + weight: 1, + }] + .try_into() + .unwrap(), + ext: AccountEntryExt::V1(AccountEntryExtensionV1 { + liabilities: Liabilities { + buying: 123, + selling: 456, + }, + ext: AccountEntryExtensionV1Ext::V2(AccountEntryExtensionV2 { + num_sponsored: 5, + num_sponsoring: 6, + signer_sponsoring_i_ds: Default::default(), + ext: AccountEntryExtensionV2Ext::V3(AccountEntryExtensionV3 { + ext: ExtensionPoint::V0, + seq_ledger: 0, + seq_time: TimePoint(0), + }), + }), + }), + }))), + None + )) + ); + assert_eq!( + snapshot + .get(&Rc::new( + ledger_entry_to_ledger_key(&account_with_ext_v3).unwrap() + )) + .unwrap(), + Some((Rc::new(account_with_ext_v3), None)) + ); +}