Skip to content

Commit a75e5c7

Browse files
authored
Fix simulation for auto-restored contract code. (#1590)
### What Fix simulation for auto-restored contract code. When a contract code entry is auto-restored, it will likely miss the module cache (if it's been in the hot archive), so we should charge the recompilation to the budget (in the same way as we already do for the entry that has been just created via upload). ### Why Improving simulation accuracy. ### Known limitations N/A
1 parent d3d9bf8 commit a75e5c7

File tree

4 files changed

+139
-59
lines changed

4 files changed

+139
-59
lines changed

soroban-env-host/src/host/frame.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -720,12 +720,13 @@ impl Host {
720720
// - User invoked us with bad input, eg. calling a
721721
// contract that wasn't provided in footprint/storage.
722722
//
723-
// - User uploaded the wasm _in this transaction_ so we
724-
// didn't cache it when starting the transaction (and
725-
// couldn't add it: additions use a shared wasmi engine
726-
// that owns all the cache entries, and that engine is
727-
// locked while we're running; uploads use a throwaway
728-
// engine for validation purposes).
723+
// - User uploaded the wasm in this ledger so we didn't
724+
// cache it when starting the ledger (and couldn't add
725+
// it: the module cache and the wasmi engine used to
726+
// build modules are both locked and shared across
727+
// threads during execution, we don't want to perturb
728+
// it even if we could; uploads use a throwaway engine
729+
// for validation purposes).
729730
//
730731
// 3. Even more pathological: the module cache was built,
731732
// and contained the module, but someone _removed_ the
@@ -742,23 +743,34 @@ impl Host {
742743
// with a storage error.
743744

744745
#[cfg(any(test, feature = "recording_mode"))]
745-
// In recording mode: if a contract was present in the initial snapshot image, it is part of
746-
// the set of contracts that would have been built into a module cache in enforcing mode;
747-
// we want to suppress the cost of parsing those (simulating them as module cache hits).
748-
//
749-
// If a contract is _not_ in the initial snapshot image, it's because someone just uploaded
750-
// it during execution. Those would be cache misses in enforcing mode, and so it is right to
751-
// continue to charge for them as such (charging the parse cost on each call) in recording.
746+
// In recording mode:
747+
// - We have no _real_ module cache.
748+
// - We have a choice of whether simulate a cache-hit.
749+
// - We will "simulate a hit" by doing a fresh parse but charging it
750+
// to the shadow budget, not the real budget.
751+
// - We _want_ to simulate a miss any time _would_ be a corresponding
752+
// (charged-for) miss in enforcing mode, because otherwise we're
753+
// under-charging and the tx we're simulating will fail in execution.
754+
// - One case we know for sure will cause a miss: if the module
755+
// literally isn't in the snapshot at all. This happens when someone
756+
// uploads a contract and tries running it in the same ledger.
757+
// - Other cases we're _not sure_: a module might be expired and evicted
758+
// (thus removed from module cache) between simulation time and
759+
// enforcement time.
760+
// - But we can and do make an approximation here:
761+
// - If the module is _expired_ we assume it's on its way to eviction
762+
// soon and simulate a miss, risking overcharging.
763+
// - If the module is _not expired_ we assume it'll be survive until
764+
// execution, simulate a hit, and risk undercharging.
752765
if self.in_storage_recording_mode()? {
753766
if let Some((parsed_module, wasmi_linker)) =
754767
self.budget_ref().with_observable_shadow_mode(|| {
755768
use crate::vm::ParsedModule;
756769
let wasm_key = self.contract_code_ledger_key(wasm_hash)?;
757-
if self
758-
.try_borrow_storage()?
759-
.get_snapshot_value(self, &wasm_key)?
760-
.is_some()
761-
{
770+
let is_key_live_in_snapshot = self
771+
.try_borrow_storage_mut()?
772+
.is_key_live_in_snapshot(self, &wasm_key)?;
773+
if is_key_live_in_snapshot {
762774
let (code, costs) = self.retrieve_wasm_from_storage(&wasm_hash)?;
763775
let parsed_module =
764776
ParsedModule::new_with_isolated_engine(self, code.as_slice(), costs)?;

soroban-env-host/src/storage.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -499,13 +499,30 @@ impl Storage {
499499
}
500500

501501
#[cfg(any(test, feature = "recording_mode"))]
502-
pub(crate) fn get_snapshot_value(
502+
/// Returns `true` if the key exists in the snapshot and is live w.r.t
503+
/// the current ledger sequence.
504+
pub(crate) fn is_key_live_in_snapshot(
503505
&self,
504506
host: &Host,
505507
key: &Rc<LedgerKey>,
506-
) -> Result<Option<EntryWithLiveUntil>, HostError> {
508+
) -> Result<bool, HostError> {
507509
match &self.mode {
508-
FootprintMode::Recording(snapshot) => snapshot.get(key),
510+
FootprintMode::Recording(snapshot) => {
511+
let snapshot_value = snapshot.get(key)?;
512+
if let Some((_, live_until_ledger)) = snapshot_value {
513+
if let Some(live_until_ledger) = live_until_ledger {
514+
let current_ledger_sequence =
515+
host.with_ledger_info(|li| Ok(li.sequence_number))?;
516+
Ok(live_until_ledger >= current_ledger_sequence)
517+
} else {
518+
// Non-Soroban entries are always live.
519+
Ok(true)
520+
}
521+
} else {
522+
// Key is not in the snapshot.
523+
Ok(false)
524+
}
525+
}
509526
FootprintMode::Enforcing => Err(host.err(
510527
ScErrorType::Storage,
511528
ScErrorCode::InternalError,

soroban-env-host/src/test/e2e_tests.rs

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use soroban_test_wasms::{
4444
NO_ARGUMENT_CONSTRUCTOR_TEST_CONTRACT_P22, SIMPLE_ACCOUNT_CONTRACT, SUM_I32,
4545
UPDATEABLE_CONTRACT,
4646
};
47+
use std::collections::HashSet;
4748
use std::rc::Rc;
4849

4950
// It's tricky to get exactly the same instruction consumption
@@ -307,8 +308,15 @@ fn invoke_host_function_helper_with_restored_entries(
307308
.unwrap()
308309
})
309310
.collect();
310-
311-
let module_cache = build_module_cache_for_entries(ledger_info, ledger_entries_with_ttl)?;
311+
let mut restored_contracts = HashSet::new();
312+
for restored_id in restored_entry_ids {
313+
let key = &resources.footprint.read_write[*restored_id as usize];
314+
if let LedgerKey::ContractCode(code) = key {
315+
restored_contracts.insert(code.hash.clone());
316+
}
317+
}
318+
let module_cache =
319+
build_module_cache_for_entries(ledger_info, ledger_entries_with_ttl, &restored_contracts)?;
312320

313321
let budget = Budget::default();
314322
budget
@@ -372,12 +380,18 @@ fn invoke_host_function_helper(
372380
fn build_module_cache_for_entries(
373381
ledger_info: &LedgerInfo,
374382
ledger_entries_with_ttl: Vec<(LedgerEntry, Option<u32>)>,
383+
restored_contracts: &HashSet<Hash>,
375384
) -> Result<ModuleCache, HostError> {
376385
let ctx = E2eTestCompilationContext::new()?;
377386
let cache = ModuleCache::new(&ctx)?;
378387
for (e, _) in ledger_entries_with_ttl.iter() {
379388
if let LedgerEntryData::ContractCode(cd) = &e.data {
380389
let contract_id = Hash(sha256_hash_from_bytes_raw(&cd.code, ctx.as_budget())?);
390+
// Restored contracts are not yet in the module cache and need to be
391+
// compiled during execution.
392+
if restored_contracts.contains(&contract_id) {
393+
continue;
394+
}
381395
let code_cost_inputs = match &cd.ext {
382396
ContractCodeEntryExt::V0 => VersionedContractCodeCostInputs::V0 {
383397
wasm_bytes: cd.code.len(),
@@ -442,7 +456,7 @@ fn invoke_host_function_using_simulation_with_signers(
442456
host_fn: &HostFunction,
443457
source_account: &AccountId,
444458
ledger_info: &LedgerInfo,
445-
ledger_entries_with_ttl: Vec<(LedgerEntry, Option<u32>)>,
459+
mut ledger_entries_with_ttl: Vec<(LedgerEntry, Option<u32>)>,
446460
prng_seed: &[u8; 32],
447461
dummy_host: &Host,
448462
signers: &Vec<TestSigner>,
@@ -543,7 +557,21 @@ fn invoke_host_function_using_simulation_with_signers(
543557
* (1.0 + RECORDING_MODE_INSTRUCTIONS_RANGE))
544558
as u32;
545559

546-
let enforcing_result = invoke_host_function_helper(
560+
let restored_live_until_ledger =
561+
ledger_info.sequence_number + ledger_info.min_persistent_entry_ttl - 1;
562+
for restored_id in &recording_result.restored_rw_entry_ids {
563+
let key = recording_result.resources.footprint.read_write[*restored_id as usize].clone();
564+
// Update TTL of the entry corresponding to the key
565+
if let Some((_, ttl)) = ledger_entries_with_ttl
566+
.iter_mut()
567+
.find(|(le, _)| ledger_entry_to_ledger_key(le, &Budget::default()).unwrap() == key)
568+
{
569+
*ttl = Some(restored_live_until_ledger);
570+
} else {
571+
panic!("restored entry not found in the original entries");
572+
}
573+
}
574+
let enforcing_result = invoke_host_function_helper_with_restored_entries(
547575
enable_diagnostics,
548576
host_fn,
549577
&recording_result.resources,
@@ -552,6 +580,7 @@ fn invoke_host_function_using_simulation_with_signers(
552580
ledger_info,
553581
ledger_entries_with_ttl,
554582
prng_seed,
583+
recording_result.restored_rw_entry_ids.as_slice(),
555584
)?;
556585

557586
assert_eq!(
@@ -2246,26 +2275,27 @@ fn test_auto_restore_with_extension_in_recording_mode() {
22462275
&val,
22472276
ContractDataDurability::Persistent,
22482277
);
2278+
let le_with_ttl = vec![
2279+
(
2280+
cd.wasm_entry.clone(),
2281+
Some(ledger_info.sequence_number - 100_000),
2282+
),
2283+
(
2284+
cd.contract_entry.clone(),
2285+
Some(ledger_info.sequence_number - 1),
2286+
),
2287+
(
2288+
persistent_data_entry.clone(),
2289+
Some(ledger_info.sequence_number - 10),
2290+
),
2291+
];
22492292
let res = invoke_host_function_recording_helper(
22502293
true,
22512294
&host_fn,
22522295
&cd.deployer,
22532296
RecordingInvocationAuthMode::Recording(true),
22542297
&ledger_info,
2255-
vec![
2256-
(
2257-
cd.wasm_entry.clone(),
2258-
Some(ledger_info.sequence_number - 100_000),
2259-
),
2260-
(
2261-
cd.contract_entry.clone(),
2262-
Some(ledger_info.sequence_number - 1),
2263-
),
2264-
(
2265-
persistent_data_entry.clone(),
2266-
Some(ledger_info.sequence_number - 10),
2267-
),
2268-
],
2298+
le_with_ttl.clone(),
22692299
&prng_seed(),
22702300
None,
22712301
)
@@ -2345,11 +2375,21 @@ fn test_auto_restore_with_extension_in_recording_mode() {
23452375
.try_into()
23462376
.unwrap(),
23472377
},
2348-
instructions: 1027906,
2378+
instructions: 1562621,
23492379
disk_read_bytes: data_entry_size + wasm_entry_size + instance_entry_size,
23502380
write_bytes: data_entry_size + wasm_entry_size + instance_entry_size,
23512381
}
23522382
);
2383+
2384+
let _ = invoke_host_function_using_simulation(
2385+
true,
2386+
&host_fn,
2387+
&cd.deployer,
2388+
&ledger_info,
2389+
le_with_ttl,
2390+
&prng_seed(),
2391+
)
2392+
.unwrap();
23532393
}
23542394

23552395
#[test]
@@ -2376,26 +2416,27 @@ fn test_auto_restore_with_overwrite_in_recording_mode() {
23762416
&key,
23772417
ContractDataDurability::Persistent,
23782418
);
2419+
let le_with_ttl = vec![
2420+
(
2421+
cd.wasm_entry.clone(),
2422+
Some(ledger_info.sequence_number + 100_000),
2423+
),
2424+
(
2425+
cd.contract_entry.clone(),
2426+
Some(ledger_info.sequence_number - 100),
2427+
),
2428+
(
2429+
persistent_data_entry.clone(),
2430+
Some(ledger_info.sequence_number - 1),
2431+
),
2432+
];
23792433
let res = invoke_host_function_recording_helper(
23802434
true,
23812435
&host_fn,
23822436
&cd.deployer,
23832437
RecordingInvocationAuthMode::Recording(true),
23842438
&ledger_info,
2385-
vec![
2386-
(
2387-
cd.wasm_entry.clone(),
2388-
Some(ledger_info.sequence_number + 100_000),
2389-
),
2390-
(
2391-
cd.contract_entry.clone(),
2392-
Some(ledger_info.sequence_number - 100),
2393-
),
2394-
(
2395-
persistent_data_entry.clone(),
2396-
Some(ledger_info.sequence_number - 1),
2397-
),
2398-
],
2439+
le_with_ttl.clone(),
23992440
&prng_seed(),
24002441
None,
24012442
)
@@ -2477,6 +2518,16 @@ fn test_auto_restore_with_overwrite_in_recording_mode() {
24772518
write_bytes: data_entry_size + instance_entry_size,
24782519
}
24792520
);
2521+
2522+
let _ = invoke_host_function_using_simulation(
2523+
true,
2524+
&host_fn,
2525+
&cd.deployer,
2526+
&ledger_info,
2527+
le_with_ttl,
2528+
&prng_seed(),
2529+
)
2530+
.unwrap();
24802531
}
24812532

24822533
#[test]
@@ -2594,7 +2645,7 @@ fn test_auto_restore_with_expired_temp_entry_in_recording_mode() {
25942645
.try_into()
25952646
.unwrap(),
25962647
},
2597-
instructions: 1026761,
2648+
instructions: 1561476,
25982649
disk_read_bytes: wasm_entry_size + instance_entry_size,
25992650
write_bytes: wasm_entry_size + instance_entry_size,
26002651
}
@@ -2714,7 +2765,7 @@ fn test_auto_restore_with_recreated_temp_entry_in_recording_mode() {
27142765
read_only: vec![cd.contract_key.clone()].try_into().unwrap(),
27152766
read_write: vec![data_key, cd.wasm_key.clone()].try_into().unwrap(),
27162767
},
2717-
instructions: 1028934,
2768+
instructions: 1563649,
27182769
disk_read_bytes: wasm_entry_size,
27192770
write_bytes: wasm_entry_size + temp_entry_size,
27202771
}

soroban-simulation/src/test/simulation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ fn test_simulate_invoke_contract_with_autorestore() {
557557
assert!(res.contract_events.is_empty());
558558
assert!(!res.diagnostic_events.is_empty());
559559

560-
let expected_instructions = 9936120;
560+
let expected_instructions = 10942546;
561561
let wasm_entry_size = contracts[0]
562562
.wasm_entry
563563
.to_xdr(Limits::none())
@@ -589,11 +589,11 @@ fn test_simulate_invoke_contract_with_autorestore() {
589589
disk_read_bytes: wasm_entry_size + contract_1_size,
590590
write_bytes: wasm_entry_size + contract_1_size,
591591
},
592-
resource_fee: 6230340,
592+
resource_fee: 6231346,
593593
})
594594
);
595595
assert_eq!(res.simulated_instructions, expected_instructions);
596-
assert_eq!(res.simulated_memory, 4968053);
596+
assert_eq!(res.simulated_memory, 5471262);
597597
assert_eq!(
598598
res.modified_entries,
599599
vec![

0 commit comments

Comments
 (0)