Ver Fonte

Soroban: Add `extendTtl` Builtin Method (#1709)

This PR adds support for `extendPersistentTtl()` method in Soroban,
along with a test and documentation. Also, the Soroban testing
infrastructure has been refactored to allow more flexible environment
manipulation.

[Documentation for
`extend_ttl`](https://developers.stellar.org/docs/build/smart-contracts/getting-started/storing-data#managing-contract-data-ttls-with-extend_ttl)
Fixes #1669

#### Changes 
- Added support for `extendPersistentTtl` as a method on `uint64` for
the Soroban target.
- In the Soroban SDK, `extend_ttl` is a generic function (`IntoVal<Env,
Val>`)
```rust
pub fn extend_ttl<K>(&self, key: &K, threshold: u32, extend_to: u32)
where
    K: IntoVal<Env, Val>,
```
but Solidity does not support that, so it's implemented as a method
instead.
- One assertion in the `different_storage_types` test is affected due to
changes in diagnostic capture. A follow-up PR will address this.

---------

Signed-off-by: Tarek <tareknaser360@gmail.com>
Tarek Elsayed há 9 meses atrás
pai
commit
fe96d0dab3

+ 44 - 0
docs/language/builtins.rst

@@ -666,3 +666,47 @@ Assuming `arg1` is 512 and `arg2` is 196, the output to the log will be ``foo en
     When formatting integers in to decimals, types larger than 64 bits require expensive division.
     When formatting integers in to decimals, types larger than 64 bits require expensive division.
     Be mindful this will increase the gas cost. Larger values will incur a higher gas cost.
     Be mindful this will increase the gas cost. Larger values will incur a higher gas cost.
     Alternatively, use a hexadecimal ``{:x}`` format specifier to reduce the cost.
     Alternatively, use a hexadecimal ``{:x}`` format specifier to reduce the cost.
+
+
+extendTtl(uint32 threshold, uint32 extend_to) 
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+The ``extendTtl()`` method allows extending the time-to-live (TTL) of a contract storage entry.
+
+If the entry's TTL is below threshold ledgers, this function updates ``live_until_ledger_seq`` such that TTL equals ``extend_to``. The TTL is defined as:
+
+.. math::
+
+TTL = live_until_ledger_seq - current_ledger
+
+
+.. note:: This method is only available on the Soroban target
+
+.. code-block:: solidity
+
+    /// Extends the TTL for the `count` persistent key to 5000 ledgers
+    /// if the current TTL is smaller than 1000 ledgers
+    function extend_ttl() public view returns (int64) {
+        return count.extendTtl(1000, 5000);
+    }
+
+
+
+For more details on managing contract data TTLs in Soroban, refer to the docs for `TTL <https://developers.stellar.org/docs/build/smart-contracts/getting-started/storing-data#managing-contract-data-ttls-with-extend_ttl>`_.
+
+extendInstanceTtl(uint32 threshold, uint32 extend_to)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+The extendInstanceTtl() function extends the time-to-live (TTL) of contract instance storage.
+
+If the TTL for the current contract instance and code (if applicable) is below threshold ledgers, this function extends ``live_until_ledger_seq`` such that TTL equals ``extend_to``.
+
+.. note:: This is a global function, not a method, and is only available on the Soroban target
+
+.. code-block:: solidity
+
+    /// Extends the TTL for the contract instance storage to 10000 ledgers
+    /// if the current TTL is smaller than 2000 ledgers
+    function extendInstanceTtl() public view returns (int64) {
+        return extendInstanceTtl(2000, 10000);
+    }

+ 38 - 0
src/codegen/expression.rs

@@ -30,6 +30,7 @@ use crate::sema::{
     expression::ResolveTo,
     expression::ResolveTo,
 };
 };
 use crate::Target;
 use crate::Target;
+use core::panic;
 use num_bigint::{BigInt, Sign};
 use num_bigint::{BigInt, Sign};
 use num_traits::{FromPrimitive, One, ToPrimitive, Zero};
 use num_traits::{FromPrimitive, One, ToPrimitive, Zero};
 use solang_parser::pt::{self, CodeLocation, Loc};
 use solang_parser::pt::{self, CodeLocation, Loc};
@@ -2327,6 +2328,43 @@ fn expr_builtin(
 
 
             code(loc, *contract_no, ns, opt)
             code(loc, *contract_no, ns, opt)
         }
         }
+        ast::Builtin::ExtendTtl => {
+            let mut arguments: Vec<Expression> = args
+                .iter()
+                .map(|v| expression(v, cfg, contract_no, func, ns, vartab, opt))
+                .collect();
+
+            // var_no is the first argument of the builtin
+            let var_no = match arguments[0].clone() {
+                Expression::NumberLiteral { value, .. } => value,
+                _ => panic!("First argument of extendTtl() must be a number literal"),
+            }
+            .to_usize()
+            .expect("Unable to convert var_no to usize");
+            let var = ns.contracts[contract_no].variables.get(var_no).unwrap();
+            let storage_type_usize = match var
+            .storage_type
+            .clone()
+            .expect("Unable to get storage type") {
+                solang_parser::pt::StorageType::Temporary(_) => 0,
+                solang_parser::pt::StorageType::Persistent(_) => 1,
+                solang_parser::pt::StorageType::Instance(_) => panic!("Calling extendTtl() on instance storage is not allowed. Use `extendInstanceTtl()` instead."),
+            };
+
+            // append the storage type to the arguments
+            arguments.push(Expression::NumberLiteral {
+                loc: *loc,
+                ty: Type::Uint(32),
+                value: BigInt::from(storage_type_usize),
+            });
+
+            Expression::Builtin {
+                loc: *loc,
+                tys: tys.to_vec(),
+                kind: (&builtin).into(),
+                args: arguments,
+            }
+        }
         _ => {
         _ => {
             let arguments: Vec<Expression> = args
             let arguments: Vec<Expression> = args
                 .iter()
                 .iter()

+ 8 - 0
src/codegen/mod.rs

@@ -97,6 +97,8 @@ impl From<inkwell::OptimizationLevel> for OptimizationLevel {
 pub enum HostFunctions {
 pub enum HostFunctions {
     PutContractData,
     PutContractData,
     GetContractData,
     GetContractData,
+    ExtendContractDataTtl,
+    ExtendCurrentContractInstanceAndCodeTtl,
     LogFromLinearMemory,
     LogFromLinearMemory,
     SymbolNewFromLinearMemory,
     SymbolNewFromLinearMemory,
     VectorNew,
     VectorNew,
@@ -111,6 +113,8 @@ impl HostFunctions {
         match self {
         match self {
             HostFunctions::PutContractData => "l._",
             HostFunctions::PutContractData => "l._",
             HostFunctions::GetContractData => "l.1",
             HostFunctions::GetContractData => "l.1",
+            HostFunctions::ExtendContractDataTtl => "l.7",
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8",
             HostFunctions::LogFromLinearMemory => "x._",
             HostFunctions::LogFromLinearMemory => "x._",
             HostFunctions::SymbolNewFromLinearMemory => "b.j",
             HostFunctions::SymbolNewFromLinearMemory => "b.j",
             HostFunctions::VectorNew => "v._",
             HostFunctions::VectorNew => "v._",
@@ -1794,6 +1798,8 @@ pub enum Builtin {
     WriteUint256LE,
     WriteUint256LE,
     WriteBytes,
     WriteBytes,
     Concat,
     Concat,
+    ExtendTtl,
+    ExtendInstanceTtl,
 }
 }
 
 
 impl From<&ast::Builtin> for Builtin {
 impl From<&ast::Builtin> for Builtin {
@@ -1856,6 +1862,8 @@ impl From<&ast::Builtin> for Builtin {
             ast::Builtin::PrevRandao => Builtin::PrevRandao,
             ast::Builtin::PrevRandao => Builtin::PrevRandao,
             ast::Builtin::ContractCode => Builtin::ContractCode,
             ast::Builtin::ContractCode => Builtin::ContractCode,
             ast::Builtin::StringConcat | ast::Builtin::BytesConcat => Builtin::Concat,
             ast::Builtin::StringConcat | ast::Builtin::BytesConcat => Builtin::Concat,
+            ast::Builtin::ExtendTtl => Builtin::ExtendTtl,
+            ast::Builtin::ExtendInstanceTtl => Builtin::ExtendInstanceTtl,
             _ => panic!("Builtin should not be in the cfg"),
             _ => panic!("Builtin should not be in the cfg"),
         }
         }
     }
     }

+ 4 - 0
src/codegen/tests.rs

@@ -59,6 +59,8 @@ fn test_builtin_conversion() {
         ast::Builtin::WriteUint256LE,
         ast::Builtin::WriteUint256LE,
         ast::Builtin::WriteString,
         ast::Builtin::WriteString,
         ast::Builtin::WriteBytes,
         ast::Builtin::WriteBytes,
+        ast::Builtin::ExtendTtl,
+        ast::Builtin::ExtendInstanceTtl,
     ];
     ];
 
 
     let output: Vec<codegen::Builtin> = vec![
     let output: Vec<codegen::Builtin> = vec![
@@ -115,6 +117,8 @@ fn test_builtin_conversion() {
         codegen::Builtin::WriteUint256LE,
         codegen::Builtin::WriteUint256LE,
         codegen::Builtin::WriteBytes,
         codegen::Builtin::WriteBytes,
         codegen::Builtin::WriteBytes,
         codegen::Builtin::WriteBytes,
+        codegen::Builtin::ExtendTtl,
+        codegen::Builtin::ExtendInstanceTtl,
     ];
     ];
 
 
     for (i, item) in input.iter().enumerate() {
     for (i, item) in input.iter().enumerate() {

+ 15 - 0
src/emit/soroban/mod.rs

@@ -39,6 +39,19 @@ impl HostFunctions {
                 .context
                 .context
                 .i64_type()
                 .i64_type()
                 .fn_type(&[ty.into(), ty.into()], false),
                 .fn_type(&[ty.into(), ty.into()], false),
+            // https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l
+            // ;; If the entry's TTL 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.
+            // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+            HostFunctions::ExtendContractDataTtl => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false),
+            // ;; If the TTL for the current contract instance and 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.
+            // (func $extend_current_contract_instance_and_code_ttl (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into()], false),
             HostFunctions::LogFromLinearMemory => bin
             HostFunctions::LogFromLinearMemory => bin
                 .context
                 .context
                 .i64_type()
                 .i64_type()
@@ -279,6 +292,8 @@ impl SorobanTarget {
         let host_functions = [
         let host_functions = [
             HostFunctions::PutContractData,
             HostFunctions::PutContractData,
             HostFunctions::GetContractData,
             HostFunctions::GetContractData,
+            HostFunctions::ExtendContractDataTtl,
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl,
             HostFunctions::LogFromLinearMemory,
             HostFunctions::LogFromLinearMemory,
             HostFunctions::SymbolNewFromLinearMemory,
             HostFunctions::SymbolNewFromLinearMemory,
             HostFunctions::VectorNew,
             HostFunctions::VectorNew,

+ 150 - 1
src/emit/soroban/target.rs

@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: Apache-2.0
 
 
 use crate::codegen::cfg::HashTy;
 use crate::codegen::cfg::HashTy;
+use crate::codegen::Builtin;
 use crate::codegen::Expression;
 use crate::codegen::Expression;
 use crate::emit::binary::Binary;
 use crate::emit::binary::Binary;
 use crate::emit::soroban::{HostFunctions, SorobanTarget};
 use crate::emit::soroban::{HostFunctions, SorobanTarget};
@@ -19,6 +20,8 @@ use inkwell::values::{
 
 
 use solang_parser::pt::{Loc, StorageType};
 use solang_parser::pt::{Loc, StorageType};
 
 
+use num_traits::ToPrimitive;
+
 use std::collections::HashMap;
 use std::collections::HashMap;
 
 
 // TODO: Implement TargetRuntime for SorobanTarget.
 // TODO: Implement TargetRuntime for SorobanTarget.
@@ -460,7 +463,153 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         function: FunctionValue<'b>,
         function: FunctionValue<'b>,
         ns: &Namespace,
         ns: &Namespace,
     ) -> BasicValueEnum<'b> {
     ) -> BasicValueEnum<'b> {
-        unimplemented!()
+        emit_context!(bin);
+
+        match expr {
+            Expression::Builtin {
+                kind: Builtin::ExtendTtl,
+                args,
+                ..
+            } => {
+                // Get arguments
+                // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+                assert_eq!(args.len(), 4, "extendTtl expects 4 arguments");
+                // SAFETY: We already checked that the length of args is 4 so it is safe to unwrap here
+                let slot_no = match args.first().unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected slot_no to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let threshold = match args.get(1).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let extend_to = match args.get(2).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(2).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let storage_type = match args.get(3).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                    "Expected storage_type to be of type Expression::NumberLiteral. Actual: {:?}",
+                    args.get(3).unwrap()
+                ),
+                }
+                .to_u64()
+                .unwrap();
+
+                // Encode the values (threshold and extend_to)
+                // See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
+                let threshold_u32_val = (threshold << 32) + 4;
+                let extend_to_u32_val = (extend_to << 32) + 4;
+
+                // Call the function
+                let function_name = HostFunctions::ExtendContractDataTtl.name();
+                let function_value = bin.module.get_function(function_name).unwrap();
+
+                let value = bin
+                    .builder
+                    .build_call(
+                        function_value,
+                        &[
+                            bin.context.i64_type().const_int(slot_no, false).into(),
+                            bin.context.i64_type().const_int(storage_type, false).into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(threshold_u32_val, false)
+                                .into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(extend_to_u32_val, false)
+                                .into(),
+                        ],
+                        function_name,
+                    )
+                    .unwrap()
+                    .try_as_basic_value()
+                    .left()
+                    .unwrap()
+                    .into_int_value();
+
+                value.into()
+            }
+            Expression::Builtin {
+                kind: Builtin::ExtendInstanceTtl,
+                args,
+                ..
+            } => {
+                // Get arguments
+                // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+                assert_eq!(args.len(), 2, "extendTtl expects 2 arguments");
+                // SAFETY: We already checked that the length of args is 2 so it is safe to unwrap here
+                let threshold = match args.first().unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let extend_to = match args.get(1).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(2).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+
+                // Encode the values (threshold and extend_to)
+                // See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
+                let threshold_u32_val = (threshold << 32) + 4;
+                let extend_to_u32_val = (extend_to << 32) + 4;
+
+                // Call the function
+                let function_name = HostFunctions::ExtendCurrentContractInstanceAndCodeTtl.name();
+                let function_value = bin.module.get_function(function_name).unwrap();
+
+                let value = bin
+                    .builder
+                    .build_call(
+                        function_value,
+                        &[
+                            bin.context
+                                .i64_type()
+                                .const_int(threshold_u32_val, false)
+                                .into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(extend_to_u32_val, false)
+                                .into(),
+                        ],
+                        function_name,
+                    )
+                    .unwrap()
+                    .try_as_basic_value()
+                    .left()
+                    .unwrap()
+                    .into_int_value();
+
+                value.into()
+            }
+            _ => unimplemented!("unsupported builtin"),
+        }
     }
     }
 
 
     /// Return the return data from an external call (either revert error or return values)
     /// Return the return data from an external call (either revert error or return values)

+ 2 - 0
src/sema/ast.rs

@@ -1779,6 +1779,8 @@ pub enum Builtin {
     TypeInterfaceId,
     TypeInterfaceId,
     TypeRuntimeCode,
     TypeRuntimeCode,
     TypeCreatorCode,
     TypeCreatorCode,
+    ExtendTtl,
+    ExtendInstanceTtl,
 }
 }
 
 
 #[derive(PartialEq, Eq, Clone, Debug)]
 #[derive(PartialEq, Eq, Clone, Debug)]

+ 25 - 2
src/sema/builtin.rs

@@ -36,8 +36,19 @@ pub struct Prototype {
 }
 }
 
 
 // A list of all Solidity builtins functions
 // A list of all Solidity builtins functions
-pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 27]> = Lazy::new(|| {
+pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 28]> = Lazy::new(|| {
     [
     [
+        Prototype {
+            builtin: Builtin::ExtendInstanceTtl,
+            namespace: None,
+            method: vec![],
+            name: "extendInstanceTtl",  
+            params: vec![Type::Uint(32), Type::Uint(32)],
+            ret: vec![Type::Int(64)],
+            target: vec![Target::Soroban],
+            doc: "If the TTL for the current contract instance and 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.",
+            constant: false,
+        },
         Prototype {
         Prototype {
             builtin: Builtin::Assert,
             builtin: Builtin::Assert,
             namespace: None,
             namespace: None,
@@ -547,8 +558,20 @@ pub static BUILTIN_VARIABLE: Lazy<[Prototype; 17]> = Lazy::new(|| {
 });
 });
 
 
 // A list of all Solidity builtins methods
 // A list of all Solidity builtins methods
-pub static BUILTIN_METHODS: Lazy<[Prototype; 27]> = Lazy::new(|| {
+pub static BUILTIN_METHODS: Lazy<[Prototype; 28]> = Lazy::new(|| {
     [
     [
+        Prototype {
+            builtin: Builtin::ExtendTtl,
+            namespace: None,
+            // FIXME: For now as a PoC, we are only supporting this method for type `uint64`
+            method: vec![Type::StorageRef(false, Box::new(Type::Uint(64)))],
+            name: "extendTtl",
+            params: vec![Type::Uint(32), Type::Uint(32)], // Parameters `threshold` and `extend_to` of type `uint32`
+            ret: vec![Type::Int(64)],
+            target: vec![Target::Soroban],
+            doc: "If the entry's TTL 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.",
+            constant: false,
+        },
         Prototype {
         Prototype {
             builtin: Builtin::ReadInt8,
             builtin: Builtin::ReadInt8,
             namespace: None,
             namespace: None,

+ 15 - 3
tests/soroban.rs

@@ -19,9 +19,13 @@ pub struct SorobanEnv {
     compiler_diagnostics: Diagnostics,
     compiler_diagnostics: Diagnostics,
 }
 }
 
 
-pub fn build_solidity(src: &str) -> SorobanEnv {
+pub fn build_solidity<F>(src: &str, configure_env: F) -> SorobanEnv
+where
+    F: FnOnce(&mut SorobanEnv),
+{
     let (wasm_blob, ns) = build_wasm(src);
     let (wasm_blob, ns) = build_wasm(src);
-    SorobanEnv::new_with_contract(wasm_blob).insert_diagnostics(ns.diagnostics)
+
+    SorobanEnv::new_with_contract(wasm_blob, configure_env).insert_diagnostics(ns.diagnostics)
 }
 }
 
 
 fn build_wasm(src: &str) -> (Vec<u8>, Namespace) {
 fn build_wasm(src: &str) -> (Vec<u8>, Namespace) {
@@ -64,9 +68,15 @@ impl SorobanEnv {
         self
         self
     }
     }
 
 
-    pub fn new_with_contract(contract_wasm: Vec<u8>) -> Self {
+    pub fn new_with_contract<F>(contract_wasm: Vec<u8>, configure_env: F) -> Self
+    where
+        F: FnOnce(&mut SorobanEnv),
+    {
         let mut env = Self::new();
         let mut env = Self::new();
+        configure_env(&mut env);
+
         env.register_contract(contract_wasm);
         env.register_contract(contract_wasm);
+
         env
         env
     }
     }
 
 
@@ -88,6 +98,8 @@ impl SorobanEnv {
             args_soroban.push_back(arg)
             args_soroban.push_back(arg)
         }
         }
         println!("args_soroban: {:?}", args_soroban);
         println!("args_soroban: {:?}", args_soroban);
+        // To avoid running out of fuel
+        self.env.cost_estimate().budget().reset_unlimited();
         self.env.invoke_contract(addr, &func, args_soroban)
         self.env.invoke_contract(addr, &func, args_soroban)
     }
     }
 
 

+ 1 - 0
tests/soroban_testcases/cross_contract_calls.rs

@@ -15,6 +15,7 @@ fn simple_cross_contract() {
             }
             }
         }
         }
     }"#,
     }"#,
+        |_| {},
     );
     );
 
 
     let caller = runtime.deploy_contract(
     let caller = runtime.deploy_contract(

+ 2 - 0
tests/soroban_testcases/math.rs

@@ -15,6 +15,7 @@ fn math() {
             }
             }
         }
         }
     }"#,
     }"#,
+        |_| {},
     );
     );
 
 
     let arg: Val = 5_u64.into_val(&runtime.env);
     let arg: Val = 5_u64.into_val(&runtime.env);
@@ -56,6 +57,7 @@ fn math_same_name() {
         }
         }
     }
     }
     "#,
     "#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -3,3 +3,4 @@ mod cross_contract_calls;
 mod math;
 mod math;
 mod print;
 mod print;
 mod storage;
 mod storage;
+mod ttl;

+ 3 - 0
tests/soroban_testcases/print.rs

@@ -14,6 +14,7 @@ fn log_runtime_error() {
                 return count;
                 return count;
             }
             }
         }"#,
         }"#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();
@@ -34,6 +35,7 @@ fn print() {
                 print("Hello, World!");
                 print("Hello, World!");
             }
             }
         }"#,
         }"#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();
@@ -57,6 +59,7 @@ fn print_then_runtime_error() {
                 return count;
                 return count;
             }
             }
         }"#,
         }"#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();

+ 2 - 0
tests/soroban_testcases/storage.rs

@@ -19,6 +19,7 @@ fn counter() {
                 return count;
                 return count;
             }
             }
         }"#,
         }"#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();
@@ -62,6 +63,7 @@ fn different_storage_types() {
         sesa3--;
         sesa3--;
     }
     }
 }"#,
 }"#,
+        |_| {},
     );
     );
 
 
     let addr = src.contracts.last().unwrap();
     let addr = src.contracts.last().unwrap();

+ 381 - 0
tests/soroban_testcases/ttl.rs

@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::testutils::storage::{Instance, Persistent, Temporary};
+use soroban_sdk::testutils::Ledger;
+
+#[test]
+fn ttl_basic_persistent() {
+    let runtime = build_solidity(
+        r#"contract counter {
+            /// Variable to track the count. Stored in persistent storage
+            uint64 public persistent count = 11;
+
+            /// Extends the TTL for the `count` persistent key to 5000 ledgers
+            /// if the current TTL is smaller than 1000 ledgers
+            function extend_ttl() public view returns (int64) {
+                return count.extendTtl(1000, 5000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for persistent entries - new persistent (and instance)
+                // entries will have this TTL when created.
+                li.min_persistent_entry_ttl = 500;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // initial TTL
+    runtime.env.as_contract(addr, || {
+        // There is only one key in the persistent storage
+        let key = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&key), 499);
+    });
+
+    // Extend persistent entry TTL to 5000 ledgers - now it is 5000.
+    runtime.invoke_contract(addr, "extend_ttl", vec![]);
+
+    runtime.env.as_contract(addr, || {
+        // There is only one key in the persistent storage
+        let key = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&key), 5000);
+    });
+}
+
+#[test]
+fn ttl_basic_temporary() {
+    let runtime = build_solidity(
+        r#"contract temp_counter {
+            /// Variable stored in temporary storage
+            uint64 temporary tempCount = 7;
+
+            /// Extend the temporary entry TTL to become at least 7000 ledgers,
+            /// when its TTL is smaller than 3000 ledgers.
+            function extend_temp_ttl() public view returns (int64) {
+                return tempCount.extendTtl(3000, 7000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for temporary entries - new temporary
+                // entries will have this TTL when created.
+                li.min_temp_entry_ttl = 100;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // initial TTL
+    runtime.env.as_contract(addr, || {
+        let key = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&key), 99);
+    });
+
+    // Extend temporary entry TTL to 7000 ledgers - now it is 7000.
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+
+    runtime.env.as_contract(addr, || {
+        let key = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&key), 7000);
+    });
+}
+
+#[test]
+#[should_panic(
+    expected = "Calling extendTtl() on instance storage is not allowed. Use `extendInstanceTtl()` instead."
+)]
+fn ttl_instance_wrong() {
+    let _runtime = build_solidity(
+        r#"contract instance_counter {
+            uint64 instance instanceCount = 3;
+            
+            function extendInstanceTtl() public view returns (int64) {
+                return instanceCount.extendTtl(700, 3000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+            });
+        },
+    );
+}
+
+#[test]
+fn ttl_instance_correct() {
+    let runtime = build_solidity(
+        r#"contract instance_counter {
+            /// Variable stored in instance storage
+            uint64 instance instanceCount = 3;
+
+            /// Extends the TTL for the instance storage to 10000 ledgers
+            /// if the current TTL is smaller than 2000 ledgers
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for persistent entries - new persistent (and instance)
+                // entries will have this TTL when created.
+                li.min_persistent_entry_ttl = 500;
+                // Minimum TTL for temporary entries - new temporary
+                // entries will have this TTL when created.
+                li.min_temp_entry_ttl = 100;
+                // Maximum TTL of any entry. Note, that entries can have their TTL
+                // extended indefinitely, but each extension can be at most
+                // `max_entry_ttl` ledger from the current `sequence_number`.
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Initial TTL for instance storage
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 499);
+    });
+
+    // Extend instance TTL to 10000 ledgers
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 10000);
+    });
+}
+
+/// This test is adapted from
+/// [Stellar Soroban Examples](https://github.com/stellar/soroban-examples/blob/f595fb5df06058ec0b9b829e9e4d0fe0513e0aa8/ttl).
+#[test]
+fn ttl_combined() {
+    let runtime = build_solidity(
+        r#"
+        contract ttl_storage {
+            uint64 public persistent pCount = 11;
+            uint64 temporary tCount = 7;
+            uint64 instance iCount = 3;
+
+            function extend_persistent_ttl() public view returns (int64) {
+                return pCount.extendTtl(1000, 5000);
+            }
+
+            function extend_temp_ttl() public view returns (int64) {
+                return tCount.extendTtl(3000, 7000);
+            }
+
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+                li.min_persistent_entry_ttl = 500;
+                li.min_temp_entry_ttl = 100;
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Verify initial TTLs
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 499);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 499);
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 99);
+    });
+
+    // Extend persistent storage TTL
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 5000);
+    });
+
+    // Extend instance storage TTL
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 10000);
+    });
+
+    // Extend temporary storage TTL
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+    runtime.env.as_contract(addr, || {
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 7000);
+    });
+
+    // Bump ledger sequence by 5000
+    runtime.env.ledger().with_mut(|li| {
+        li.sequence_number = 105_000;
+    });
+
+    // Verify TTL after ledger increment
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 0);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 5000);
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 2000);
+    });
+
+    // Re-extend all TTLs
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+
+    // Final TTL verification
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 5000);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 5000); // Threshold not met, remains the same
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 7000);
+    });
+}
+
+#[test]
+#[should_panic(expected = "[testing-only] Accessed contract instance key that has been archived.")]
+fn test_persistent_entry_archival() {
+    let runtime = build_solidity(
+        r#"
+        contract persistent_cleanup {
+            uint64 public persistent pCount = 11;
+
+            function extend_persistent_ttl() public view returns (int64) {
+                return pCount.extendTtl(1000, 10000);
+            }
+
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+                li.min_persistent_entry_ttl = 500;
+                li.min_temp_entry_ttl = 100;
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Extend instance TTL
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+
+    // Bump ledger sequence by 10001 (one past persistent TTL)
+    runtime.env.ledger().with_mut(|li| {
+        li.sequence_number = 110_001;
+    });
+
+    // This should panic as the persistent entry is archived
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+}