diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json index 5d0739f10..f74f1a94d 100644 --- a/soroban-env-common/env.json +++ b/soroban-env-common/env.json @@ -1447,6 +1447,48 @@ ], "return": "AddressObject", "docs": "Get the id of the Stellar Asset contract corresponding to the provided asset without creating the instance. `serialized_asset` is `stellar::Asset` XDR serialized to bytes format. Returns the address of the would-be asset contract." + }, + { + "export": "c", + "name": "extend_contract_instance_ttl", + "args": [ + { + "name": "contract", + "type": "AddressObject" + }, + { + "name": "threshold", + "type": "U32Val" + }, + { + "name": "extend_to", + "type": "U32Val" + } + ], + "return": "Void", + "docs": "If the TTL for the provided contract instance is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.", + "min_supported_protocol": 21 + }, + { + "export": "d", + "name": "extend_contract_code_ttl", + "args": [ + { + "name": "contract", + "type": "AddressObject" + }, + { + "name": "threshold", + "type": "U32Val" + }, + { + "name": "extend_to", + "type": "U32Val" + } + ], + "return": "Void", + "docs": "If the TTL for the provided contract's code (if applicable) is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.", + "min_supported_protocol": 21 } ] }, diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index c9d07f8c2..e2068ba72 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -2081,6 +2081,22 @@ impl VmCallerEnv for Host { Ok(Val::VOID) } + fn extend_contract_instance_ttl( + &self, + _vmcaller: &mut VmCaller, + contract: AddressObject, + threshold: U32Val, + extend_to: U32Val, + ) -> Result { + let contract_id = self.contract_id_from_address(contract)?; + let key = self.contract_instance_ledger_key(&contract_id)?; + self.try_borrow_storage_mut()? + .extend_ttl(self, key, threshold.into(), extend_to.into()) + .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))?; + + Ok(Val::VOID) + } + fn extend_contract_instance_and_code_ttl( &self, _vmcaller: &mut VmCaller, @@ -2097,6 +2113,32 @@ impl VmCallerEnv for Host { Ok(Val::VOID) } + fn extend_contract_code_ttl( + &self, + _vmcaller: &mut VmCaller, + contract: AddressObject, + threshold: U32Val, + extend_to: U32Val, + ) -> Result { + let contract_id = self.contract_id_from_address(contract)?; + let key = self.contract_instance_ledger_key(&contract_id)?; + + match self + .retrieve_contract_instance_from_storage(&key)? + .executable + { + ContractExecutable::Wasm(wasm_hash) => { + let key = self.contract_code_ledger_key(&wasm_hash)?; + self.try_borrow_storage_mut()? + .extend_ttl(self, key, threshold.into(), extend_to.into()) + .map_err(|e| self.decorate_contract_code_storage_error(e, &wasm_hash))?; + } + ContractExecutable::StellarAsset => {} + } + + Ok(Val::VOID) + } + // Notes on metering: covered by the components. fn create_contract( &self, diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs index c8d5c0dfc..6d8b6433f 100644 --- a/soroban-env-host/src/test.rs +++ b/soroban-env-host/src/test.rs @@ -20,6 +20,7 @@ mod hostile_opt; mod invocation; mod ledger; mod lifecycle; +mod lifetime_extension; mod linear_memory; mod map; #[cfg(feature = "testutils")] diff --git a/soroban-env-host/src/test/lifetime_extension.rs b/soroban-env-host/src/test/lifetime_extension.rs new file mode 100644 index 000000000..44bb9b874 --- /dev/null +++ b/soroban-env-host/src/test/lifetime_extension.rs @@ -0,0 +1,140 @@ +// Note: ignoring error handling safety in these tests. +use crate::xdr::{ContractExecutable, Hash}; +use soroban_env_common::{AddressObject, Env}; +use soroban_test_wasms::CONTRACT_STORAGE; + +use crate::Host; + +struct InstanceCodeTest { + host: Host, + contract_id: AddressObject, + contract: Hash, + code: Hash, +} + +impl InstanceCodeTest { + // We can potentially add some customizability for the ledger here. + fn setup() -> Self { + let host = Host::test_host_with_recording_footprint(); + let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE); + let hash = host.contract_id_from_address(contract_id).unwrap(); + + let code = if let ContractExecutable::Wasm(hash) = host + .retrieve_contract_instance_from_storage( + &host.contract_instance_ledger_key(&hash).unwrap(), + ) + .unwrap() + .executable + { + hash + } else { + panic!("Expected Wasm executable") + }; + + host.set_ledger_info(crate::LedgerInfo { + protocol_version: 21, + sequence_number: 4090, + max_entry_ttl: 10000, + ..Default::default() + }) + .unwrap(); + + Self { + host, + contract_id, + contract: hash, + code, + } + } +} + +mod separate_instance_code_extension { + use crate::budget::AsBudget; + + use super::*; + + #[cfg(feature = "next")] + #[test] + fn extend_only_instance() { + let InstanceCodeTest { + host, + contract_id, + contract, + .. + } = InstanceCodeTest::setup(); + + assert!(host + .extend_contract_instance_ttl(contract_id, 5.into(), 5000.into()) + .is_ok()); + let entry_with_live_until = host + .try_borrow_storage_mut() + .unwrap() + .get_with_live_until_ledger( + &host.contract_instance_ledger_key(&contract).unwrap(), + host.as_budget(), + ) + .unwrap(); + + assert_eq!(entry_with_live_until.1, Some(9090)); + } + + #[cfg(feature = "next")] + #[test] + fn extend_only_code() { + let InstanceCodeTest { + host, + contract_id, + code, + .. + } = InstanceCodeTest::setup(); + + assert!(host + .extend_contract_code_ttl(contract_id, 5.into(), 5000.into()) + .is_ok()); + let entry_with_live_until = host + .try_borrow_storage_mut() + .unwrap() + .get_with_live_until_ledger( + &host.contract_code_ledger_key(&code).unwrap(), + host.as_budget(), + ) + .unwrap(); + + assert_eq!(entry_with_live_until.1, Some(9090)); + } + + #[test] + fn extend_code_and_instance() { + let InstanceCodeTest { + host, + contract_id, + code, + contract, + } = InstanceCodeTest::setup(); + + assert!(host + .extend_contract_instance_and_code_ttl(contract_id, 5.into(), 5000.into()) + .is_ok()); + let code_entry_with_live_until = host + .try_borrow_storage_mut() + .unwrap() + .get_with_live_until_ledger( + &host.contract_code_ledger_key(&code).unwrap(), + host.as_budget(), + ) + .unwrap(); + + assert_eq!(code_entry_with_live_until.1, Some(9090)); + + let instance_entry_with_live_until = host + .try_borrow_storage_mut() + .unwrap() + .get_with_live_until_ledger( + &host.contract_instance_ledger_key(&contract).unwrap(), + host.as_budget(), + ) + .unwrap(); + + assert_eq!(instance_entry_with_live_until.1, Some(9090)); + } +} diff --git a/soroban-env-macros/src/lib.rs b/soroban-env-macros/src/lib.rs index 6931da1bd..2b39dff85 100644 --- a/soroban-env-macros/src/lib.rs +++ b/soroban-env-macros/src/lib.rs @@ -19,6 +19,15 @@ use stellar_xdr::next as xdr; use crate::xdr::{Limits, ScEnvMetaEntry, WriteXdr}; +// We need the protocol version for some tests generated by this crate. +// Unfortunately it is not available at this layer and can't read from +// `meta.rs`, since this is at the lower layer (`meta.rs` is compile-time +// generated by routines here) +#[cfg(not(feature = "next"))] +pub(crate) const LEDGER_PROTOCOL_VERSION: u32 = 20; +#[cfg(feature = "next")] +pub(crate) const LEDGER_PROTOCOL_VERSION: u32 = 21; + struct MetaInput { pub interface_version: u64, } @@ -37,6 +46,7 @@ impl Parse for MetaInput { input.parse::()?; assert!(pre <= 0xffff_ffff); assert!(proto <= 0xffff_ffff); + assert_eq!(proto, LEDGER_PROTOCOL_VERSION as u64); proto << 32 | pre }, }) diff --git a/soroban-env-macros/src/synth_dispatch_host_fn_tests.rs b/soroban-env-macros/src/synth_dispatch_host_fn_tests.rs index 7976e7466..26c01c880 100644 --- a/soroban-env-macros/src/synth_dispatch_host_fn_tests.rs +++ b/soroban-env-macros/src/synth_dispatch_host_fn_tests.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Error, LitStr}; -use crate::Function; +use crate::{Function, LEDGER_PROTOCOL_VERSION}; use std::collections::{BTreeMap, BTreeSet}; @@ -80,6 +80,16 @@ fn dfs(edges: &BTreeMap>, ty: &String, set: &mut BTreeS } } +fn check_function_protocol_is_in_range(func: &Function) -> bool { + let min_supported_proto_is_too_new = func + .min_supported_protocol + .is_some_and(|v| v > LEDGER_PROTOCOL_VERSION); + let max_supported_proto_is_too_old = func + .max_supported_protocol + .is_some_and(|v| v < LEDGER_PROTOCOL_VERSION); + !min_supported_proto_is_too_new && !max_supported_proto_is_too_old +} + // This requires the input to be a valid signature const SPECIAL_CASES: [&str; 1] = ["recover_key_ecdsa_secp256k1"]; @@ -220,11 +230,15 @@ pub fn generate_hostfn_call_with_wrong_types(file_lit: LitStr) -> Result> = BTreeMap::new(); for m in root.modules.clone() { for f in m.functions.clone() { - for (i, a) in f.args.iter().enumerate() { - type_to_fn_arg - .entry(a.r#type.clone()) - .or_default() - .push((f.clone(), i)); + // checks if the current ledger protocol version falls between + // supported protocol versions of this function + if check_function_protocol_is_in_range(&f) { + for (i, a) in f.args.iter().enumerate() { + type_to_fn_arg + .entry(a.r#type.clone()) + .or_default() + .push((f.clone(), i)); + } } } } @@ -324,6 +338,7 @@ pub fn generate_hostfn_call_with_invalid_obj_handles( .modules .iter() .flat_map(|m| m.functions.clone().into_iter()) + .filter(check_function_protocol_is_in_range) .flat_map(|f| { f.args .clone()