diff --git a/docs/language/builtins.rst b/docs/language/builtins.rst index 18f36fd8f..e92695fe8 100644 --- a/docs/language/builtins.rst +++ b/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. 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. + + +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 `_. + +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); + } diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index e6aee2094..e27ac30bf 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -30,6 +30,7 @@ use crate::sema::{ expression::ResolveTo, }; use crate::Target; +use core::panic; use num_bigint::{BigInt, Sign}; use num_traits::{FromPrimitive, One, ToPrimitive, Zero}; use solang_parser::pt::{self, CodeLocation, Loc}; @@ -2327,6 +2328,43 @@ fn expr_builtin( code(loc, *contract_no, ns, opt) } + ast::Builtin::ExtendTtl => { + let mut arguments: Vec = 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 = args .iter() diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 7115721ef..d6d2356e1 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -97,6 +97,8 @@ impl From for OptimizationLevel { pub enum HostFunctions { PutContractData, GetContractData, + ExtendContractDataTtl, + ExtendCurrentContractInstanceAndCodeTtl, LogFromLinearMemory, SymbolNewFromLinearMemory, VectorNew, @@ -111,6 +113,8 @@ impl HostFunctions { match self { HostFunctions::PutContractData => "l._", HostFunctions::GetContractData => "l.1", + HostFunctions::ExtendContractDataTtl => "l.7", + HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8", HostFunctions::LogFromLinearMemory => "x._", HostFunctions::SymbolNewFromLinearMemory => "b.j", HostFunctions::VectorNew => "v._", @@ -1794,6 +1798,8 @@ pub enum Builtin { WriteUint256LE, WriteBytes, Concat, + ExtendTtl, + ExtendInstanceTtl, } impl From<&ast::Builtin> for Builtin { @@ -1856,6 +1862,8 @@ impl From<&ast::Builtin> for Builtin { ast::Builtin::PrevRandao => Builtin::PrevRandao, ast::Builtin::ContractCode => Builtin::ContractCode, 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"), } } diff --git a/src/codegen/tests.rs b/src/codegen/tests.rs index 1c895fbd8..1f5af92ad 100644 --- a/src/codegen/tests.rs +++ b/src/codegen/tests.rs @@ -59,6 +59,8 @@ fn test_builtin_conversion() { ast::Builtin::WriteUint256LE, ast::Builtin::WriteString, ast::Builtin::WriteBytes, + ast::Builtin::ExtendTtl, + ast::Builtin::ExtendInstanceTtl, ]; let output: Vec = vec![ @@ -115,6 +117,8 @@ fn test_builtin_conversion() { codegen::Builtin::WriteUint256LE, codegen::Builtin::WriteBytes, codegen::Builtin::WriteBytes, + codegen::Builtin::ExtendTtl, + codegen::Builtin::ExtendInstanceTtl, ]; for (i, item) in input.iter().enumerate() { diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 6c01dd394..72fcd7f8e 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -39,6 +39,19 @@ impl HostFunctions { .context .i64_type() .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 .context .i64_type() @@ -279,6 +292,8 @@ impl SorobanTarget { let host_functions = [ HostFunctions::PutContractData, HostFunctions::GetContractData, + HostFunctions::ExtendContractDataTtl, + HostFunctions::ExtendCurrentContractInstanceAndCodeTtl, HostFunctions::LogFromLinearMemory, HostFunctions::SymbolNewFromLinearMemory, HostFunctions::VectorNew, diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index 1f473f4bb..a5cb032e0 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::codegen::cfg::HashTy; +use crate::codegen::Builtin; use crate::codegen::Expression; use crate::emit::binary::Binary; use crate::emit::soroban::{HostFunctions, SorobanTarget}; @@ -19,6 +20,8 @@ use inkwell::values::{ use solang_parser::pt::{Loc, StorageType}; +use num_traits::ToPrimitive; + use std::collections::HashMap; // TODO: Implement TargetRuntime for SorobanTarget. @@ -460,7 +463,153 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { function: FunctionValue<'b>, ns: &Namespace, ) -> 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) diff --git a/src/sema/ast.rs b/src/sema/ast.rs index 3be779d1c..f94139ebb 100644 --- a/src/sema/ast.rs +++ b/src/sema/ast.rs @@ -1779,6 +1779,8 @@ pub enum Builtin { TypeInterfaceId, TypeRuntimeCode, TypeCreatorCode, + ExtendTtl, + ExtendInstanceTtl, } #[derive(PartialEq, Eq, Clone, Debug)] diff --git a/src/sema/builtin.rs b/src/sema/builtin.rs index 6cefddfb1..8c8918fb9 100644 --- a/src/sema/builtin.rs +++ b/src/sema/builtin.rs @@ -36,8 +36,19 @@ pub struct Prototype { } // 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 { builtin: Builtin::Assert, namespace: None, @@ -547,8 +558,20 @@ pub static BUILTIN_VARIABLE: Lazy<[Prototype; 17]> = Lazy::new(|| { }); // 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 { builtin: Builtin::ReadInt8, namespace: None, diff --git a/tests/soroban.rs b/tests/soroban.rs index 3c6c07c3e..ec5ff9816 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -19,9 +19,13 @@ pub struct SorobanEnv { compiler_diagnostics: Diagnostics, } -pub fn build_solidity(src: &str) -> SorobanEnv { +pub fn build_solidity(src: &str, configure_env: F) -> SorobanEnv +where + F: FnOnce(&mut SorobanEnv), +{ 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, Namespace) { @@ -64,9 +68,15 @@ impl SorobanEnv { self } - pub fn new_with_contract(contract_wasm: Vec) -> Self { + pub fn new_with_contract(contract_wasm: Vec, configure_env: F) -> Self + where + F: FnOnce(&mut SorobanEnv), + { let mut env = Self::new(); + configure_env(&mut env); + env.register_contract(contract_wasm); + env } @@ -88,6 +98,8 @@ impl SorobanEnv { args_soroban.push_back(arg) } 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) } diff --git a/tests/soroban_testcases/cross_contract_calls.rs b/tests/soroban_testcases/cross_contract_calls.rs index acff16028..b6408c597 100644 --- a/tests/soroban_testcases/cross_contract_calls.rs +++ b/tests/soroban_testcases/cross_contract_calls.rs @@ -15,6 +15,7 @@ fn simple_cross_contract() { } } }"#, + |_| {}, ); let caller = runtime.deploy_contract( diff --git a/tests/soroban_testcases/math.rs b/tests/soroban_testcases/math.rs index 00220937b..488f050de 100644 --- a/tests/soroban_testcases/math.rs +++ b/tests/soroban_testcases/math.rs @@ -15,6 +15,7 @@ fn math() { } } }"#, + |_| {}, ); let arg: Val = 5_u64.into_val(&runtime.env); @@ -56,6 +57,7 @@ fn math_same_name() { } } "#, + |_| {}, ); let addr = src.contracts.last().unwrap(); diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 159182465..c6cda1bd5 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -3,3 +3,4 @@ mod cross_contract_calls; mod math; mod print; mod storage; +mod ttl; diff --git a/tests/soroban_testcases/print.rs b/tests/soroban_testcases/print.rs index 0bc157ff2..03989f67b 100644 --- a/tests/soroban_testcases/print.rs +++ b/tests/soroban_testcases/print.rs @@ -14,6 +14,7 @@ fn log_runtime_error() { return count; } }"#, + |_| {}, ); let addr = src.contracts.last().unwrap(); @@ -34,6 +35,7 @@ fn print() { print("Hello, World!"); } }"#, + |_| {}, ); let addr = src.contracts.last().unwrap(); @@ -57,6 +59,7 @@ fn print_then_runtime_error() { return count; } }"#, + |_| {}, ); let addr = src.contracts.last().unwrap(); diff --git a/tests/soroban_testcases/storage.rs b/tests/soroban_testcases/storage.rs index 120fcf6d2..f09b088e4 100644 --- a/tests/soroban_testcases/storage.rs +++ b/tests/soroban_testcases/storage.rs @@ -19,6 +19,7 @@ fn counter() { return count; } }"#, + |_| {}, ); let addr = src.contracts.last().unwrap(); @@ -62,6 +63,7 @@ fn different_storage_types() { sesa3--; } }"#, + |_| {}, ); let addr = src.contracts.last().unwrap(); diff --git a/tests/soroban_testcases/ttl.rs b/tests/soroban_testcases/ttl.rs new file mode 100644 index 000000000..6821bd03b --- /dev/null +++ b/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![]); +}