Repository: stellar/rs-soroban-env
Pull request #1368 (2024-03-22 17:25:39)

Proposal: Allow Extending Instance and Code TTL With Separate Values on the Host Environment For More Cost-Efficient Implementations
This is a copy of https://github.com/stellar/rs-soroban-env/pull/1355 with clean history. 


Patch:
diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json
index 5d0739f1..f74f1a94 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 c9d07f8c..e2068ba7 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<Self::VmUserState>,
+        contract: AddressObject,
+        threshold: U32Val,
+        extend_to: U32Val,
+    ) -> Result<Void, Self::Error> {
+        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<Self::VmUserState>,
@@ -2097,6 +2113,32 @@ impl VmCallerEnv for Host {
         Ok(Val::VOID)
     }
 
+    fn extend_contract_code_ttl(
+        &self,
+        _vmcaller: &mut VmCaller<Self::VmUserState>,
+        contract: AddressObject,
+        threshold: U32Val,
+        extend_to: U32Val,
+    ) -> Result<Void, Self::Error> {
+        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-macros/src/lib.rs b/soroban-env-macros/src/lib.rs
index 6931da1b..2b39dff8 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::<Token![,]>()?;
                 assert!(pre <= 0xffff_ffff);
                 assert!(proto <= 0xffff_ffff);
+                assert_eq!(proto, LEDGER_PROTOCOL_VERSION as u64);
                 proto << 32 | pre
             },
         })


Test patch:
diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs
index c8d5c0df..6d8b6433 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 00000000..44bb9b87
--- /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/synth_dispatch_host_fn_tests.rs b/soroban-env-macros/src/synth_dispatch_host_fn_tests.rs
index 7976e746..26c01c88 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<String, BTreeSet<String>>, 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<TokenSt
     let mut type_to_fn_arg: BTreeMap<String, Vec<(Function, usize)>> = 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()