Skip to content

Commit fe96d0d

Browse files
authored
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>
1 parent fa338eb commit fe96d0d

File tree

15 files changed

+691
-6
lines changed

15 files changed

+691
-6
lines changed

docs/language/builtins.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,3 +666,47 @@ Assuming `arg1` is 512 and `arg2` is 196, the output to the log will be ``foo en
666666
When formatting integers in to decimals, types larger than 64 bits require expensive division.
667667
Be mindful this will increase the gas cost. Larger values will incur a higher gas cost.
668668
Alternatively, use a hexadecimal ``{:x}`` format specifier to reduce the cost.
669+
670+
671+
extendTtl(uint32 threshold, uint32 extend_to)
672+
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
673+
674+
The ``extendTtl()`` method allows extending the time-to-live (TTL) of a contract storage entry.
675+
676+
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:
677+
678+
.. math::
679+
680+
TTL = live_until_ledger_seq - current_ledger
681+
682+
683+
.. note:: This method is only available on the Soroban target
684+
685+
.. code-block:: solidity
686+
687+
/// Extends the TTL for the `count` persistent key to 5000 ledgers
688+
/// if the current TTL is smaller than 1000 ledgers
689+
function extend_ttl() public view returns (int64) {
690+
return count.extendTtl(1000, 5000);
691+
}
692+
693+
694+
695+
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>`_.
696+
697+
extendInstanceTtl(uint32 threshold, uint32 extend_to)
698+
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
699+
700+
The extendInstanceTtl() function extends the time-to-live (TTL) of contract instance storage.
701+
702+
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``.
703+
704+
.. note:: This is a global function, not a method, and is only available on the Soroban target
705+
706+
.. code-block:: solidity
707+
708+
/// Extends the TTL for the contract instance storage to 10000 ledgers
709+
/// if the current TTL is smaller than 2000 ledgers
710+
function extendInstanceTtl() public view returns (int64) {
711+
return extendInstanceTtl(2000, 10000);
712+
}

src/codegen/expression.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::sema::{
3030
expression::ResolveTo,
3131
};
3232
use crate::Target;
33+
use core::panic;
3334
use num_bigint::{BigInt, Sign};
3435
use num_traits::{FromPrimitive, One, ToPrimitive, Zero};
3536
use solang_parser::pt::{self, CodeLocation, Loc};
@@ -2327,6 +2328,43 @@ fn expr_builtin(
23272328

23282329
code(loc, *contract_no, ns, opt)
23292330
}
2331+
ast::Builtin::ExtendTtl => {
2332+
let mut arguments: Vec<Expression> = args
2333+
.iter()
2334+
.map(|v| expression(v, cfg, contract_no, func, ns, vartab, opt))
2335+
.collect();
2336+
2337+
// var_no is the first argument of the builtin
2338+
let var_no = match arguments[0].clone() {
2339+
Expression::NumberLiteral { value, .. } => value,
2340+
_ => panic!("First argument of extendTtl() must be a number literal"),
2341+
}
2342+
.to_usize()
2343+
.expect("Unable to convert var_no to usize");
2344+
let var = ns.contracts[contract_no].variables.get(var_no).unwrap();
2345+
let storage_type_usize = match var
2346+
.storage_type
2347+
.clone()
2348+
.expect("Unable to get storage type") {
2349+
solang_parser::pt::StorageType::Temporary(_) => 0,
2350+
solang_parser::pt::StorageType::Persistent(_) => 1,
2351+
solang_parser::pt::StorageType::Instance(_) => panic!("Calling extendTtl() on instance storage is not allowed. Use `extendInstanceTtl()` instead."),
2352+
};
2353+
2354+
// append the storage type to the arguments
2355+
arguments.push(Expression::NumberLiteral {
2356+
loc: *loc,
2357+
ty: Type::Uint(32),
2358+
value: BigInt::from(storage_type_usize),
2359+
});
2360+
2361+
Expression::Builtin {
2362+
loc: *loc,
2363+
tys: tys.to_vec(),
2364+
kind: (&builtin).into(),
2365+
args: arguments,
2366+
}
2367+
}
23302368
_ => {
23312369
let arguments: Vec<Expression> = args
23322370
.iter()

src/codegen/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ impl From<inkwell::OptimizationLevel> for OptimizationLevel {
9797
pub enum HostFunctions {
9898
PutContractData,
9999
GetContractData,
100+
ExtendContractDataTtl,
101+
ExtendCurrentContractInstanceAndCodeTtl,
100102
LogFromLinearMemory,
101103
SymbolNewFromLinearMemory,
102104
VectorNew,
@@ -111,6 +113,8 @@ impl HostFunctions {
111113
match self {
112114
HostFunctions::PutContractData => "l._",
113115
HostFunctions::GetContractData => "l.1",
116+
HostFunctions::ExtendContractDataTtl => "l.7",
117+
HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8",
114118
HostFunctions::LogFromLinearMemory => "x._",
115119
HostFunctions::SymbolNewFromLinearMemory => "b.j",
116120
HostFunctions::VectorNew => "v._",
@@ -1794,6 +1798,8 @@ pub enum Builtin {
17941798
WriteUint256LE,
17951799
WriteBytes,
17961800
Concat,
1801+
ExtendTtl,
1802+
ExtendInstanceTtl,
17971803
}
17981804

17991805
impl From<&ast::Builtin> for Builtin {
@@ -1856,6 +1862,8 @@ impl From<&ast::Builtin> for Builtin {
18561862
ast::Builtin::PrevRandao => Builtin::PrevRandao,
18571863
ast::Builtin::ContractCode => Builtin::ContractCode,
18581864
ast::Builtin::StringConcat | ast::Builtin::BytesConcat => Builtin::Concat,
1865+
ast::Builtin::ExtendTtl => Builtin::ExtendTtl,
1866+
ast::Builtin::ExtendInstanceTtl => Builtin::ExtendInstanceTtl,
18591867
_ => panic!("Builtin should not be in the cfg"),
18601868
}
18611869
}

src/codegen/tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ fn test_builtin_conversion() {
5959
ast::Builtin::WriteUint256LE,
6060
ast::Builtin::WriteString,
6161
ast::Builtin::WriteBytes,
62+
ast::Builtin::ExtendTtl,
63+
ast::Builtin::ExtendInstanceTtl,
6264
];
6365

6466
let output: Vec<codegen::Builtin> = vec![
@@ -115,6 +117,8 @@ fn test_builtin_conversion() {
115117
codegen::Builtin::WriteUint256LE,
116118
codegen::Builtin::WriteBytes,
117119
codegen::Builtin::WriteBytes,
120+
codegen::Builtin::ExtendTtl,
121+
codegen::Builtin::ExtendInstanceTtl,
118122
];
119123

120124
for (i, item) in input.iter().enumerate() {

src/emit/soroban/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ impl HostFunctions {
3939
.context
4040
.i64_type()
4141
.fn_type(&[ty.into(), ty.into()], false),
42+
// https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l
43+
// ;; 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.
44+
// (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))
45+
HostFunctions::ExtendContractDataTtl => bin
46+
.context
47+
.i64_type()
48+
.fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false),
49+
// ;; 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.
50+
// (func $extend_current_contract_instance_and_code_ttl (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
51+
HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => bin
52+
.context
53+
.i64_type()
54+
.fn_type(&[ty.into(), ty.into()], false),
4255
HostFunctions::LogFromLinearMemory => bin
4356
.context
4457
.i64_type()
@@ -279,6 +292,8 @@ impl SorobanTarget {
279292
let host_functions = [
280293
HostFunctions::PutContractData,
281294
HostFunctions::GetContractData,
295+
HostFunctions::ExtendContractDataTtl,
296+
HostFunctions::ExtendCurrentContractInstanceAndCodeTtl,
282297
HostFunctions::LogFromLinearMemory,
283298
HostFunctions::SymbolNewFromLinearMemory,
284299
HostFunctions::VectorNew,

src/emit/soroban/target.rs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
use crate::codegen::cfg::HashTy;
4+
use crate::codegen::Builtin;
45
use crate::codegen::Expression;
56
use crate::emit::binary::Binary;
67
use crate::emit::soroban::{HostFunctions, SorobanTarget};
@@ -19,6 +20,8 @@ use inkwell::values::{
1920

2021
use solang_parser::pt::{Loc, StorageType};
2122

23+
use num_traits::ToPrimitive;
24+
2225
use std::collections::HashMap;
2326

2427
// TODO: Implement TargetRuntime for SorobanTarget.
@@ -460,7 +463,153 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
460463
function: FunctionValue<'b>,
461464
ns: &Namespace,
462465
) -> BasicValueEnum<'b> {
463-
unimplemented!()
466+
emit_context!(bin);
467+
468+
match expr {
469+
Expression::Builtin {
470+
kind: Builtin::ExtendTtl,
471+
args,
472+
..
473+
} => {
474+
// Get arguments
475+
// (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))
476+
assert_eq!(args.len(), 4, "extendTtl expects 4 arguments");
477+
// SAFETY: We already checked that the length of args is 4 so it is safe to unwrap here
478+
let slot_no = match args.first().unwrap() {
479+
Expression::NumberLiteral { value, .. } => value,
480+
_ => panic!(
481+
"Expected slot_no to be of type Expression::NumberLiteral. Actual: {:?}",
482+
args.get(1).unwrap()
483+
),
484+
}
485+
.to_u64()
486+
.unwrap();
487+
let threshold = match args.get(1).unwrap() {
488+
Expression::NumberLiteral { value, .. } => value,
489+
_ => panic!(
490+
"Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
491+
args.get(1).unwrap()
492+
),
493+
}
494+
.to_u64()
495+
.unwrap();
496+
let extend_to = match args.get(2).unwrap() {
497+
Expression::NumberLiteral { value, .. } => value,
498+
_ => panic!(
499+
"Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
500+
args.get(2).unwrap()
501+
),
502+
}
503+
.to_u64()
504+
.unwrap();
505+
let storage_type = match args.get(3).unwrap() {
506+
Expression::NumberLiteral { value, .. } => value,
507+
_ => panic!(
508+
"Expected storage_type to be of type Expression::NumberLiteral. Actual: {:?}",
509+
args.get(3).unwrap()
510+
),
511+
}
512+
.to_u64()
513+
.unwrap();
514+
515+
// Encode the values (threshold and extend_to)
516+
// See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
517+
let threshold_u32_val = (threshold << 32) + 4;
518+
let extend_to_u32_val = (extend_to << 32) + 4;
519+
520+
// Call the function
521+
let function_name = HostFunctions::ExtendContractDataTtl.name();
522+
let function_value = bin.module.get_function(function_name).unwrap();
523+
524+
let value = bin
525+
.builder
526+
.build_call(
527+
function_value,
528+
&[
529+
bin.context.i64_type().const_int(slot_no, false).into(),
530+
bin.context.i64_type().const_int(storage_type, false).into(),
531+
bin.context
532+
.i64_type()
533+
.const_int(threshold_u32_val, false)
534+
.into(),
535+
bin.context
536+
.i64_type()
537+
.const_int(extend_to_u32_val, false)
538+
.into(),
539+
],
540+
function_name,
541+
)
542+
.unwrap()
543+
.try_as_basic_value()
544+
.left()
545+
.unwrap()
546+
.into_int_value();
547+
548+
value.into()
549+
}
550+
Expression::Builtin {
551+
kind: Builtin::ExtendInstanceTtl,
552+
args,
553+
..
554+
} => {
555+
// Get arguments
556+
// (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))
557+
assert_eq!(args.len(), 2, "extendTtl expects 2 arguments");
558+
// SAFETY: We already checked that the length of args is 2 so it is safe to unwrap here
559+
let threshold = match args.first().unwrap() {
560+
Expression::NumberLiteral { value, .. } => value,
561+
_ => panic!(
562+
"Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
563+
args.get(1).unwrap()
564+
),
565+
}
566+
.to_u64()
567+
.unwrap();
568+
let extend_to = match args.get(1).unwrap() {
569+
Expression::NumberLiteral { value, .. } => value,
570+
_ => panic!(
571+
"Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
572+
args.get(2).unwrap()
573+
),
574+
}
575+
.to_u64()
576+
.unwrap();
577+
578+
// Encode the values (threshold and extend_to)
579+
// See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
580+
let threshold_u32_val = (threshold << 32) + 4;
581+
let extend_to_u32_val = (extend_to << 32) + 4;
582+
583+
// Call the function
584+
let function_name = HostFunctions::ExtendCurrentContractInstanceAndCodeTtl.name();
585+
let function_value = bin.module.get_function(function_name).unwrap();
586+
587+
let value = bin
588+
.builder
589+
.build_call(
590+
function_value,
591+
&[
592+
bin.context
593+
.i64_type()
594+
.const_int(threshold_u32_val, false)
595+
.into(),
596+
bin.context
597+
.i64_type()
598+
.const_int(extend_to_u32_val, false)
599+
.into(),
600+
],
601+
function_name,
602+
)
603+
.unwrap()
604+
.try_as_basic_value()
605+
.left()
606+
.unwrap()
607+
.into_int_value();
608+
609+
value.into()
610+
}
611+
_ => unimplemented!("unsupported builtin"),
612+
}
464613
}
465614

466615
/// Return the return data from an external call (either revert error or return values)

src/sema/ast.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,6 +1779,8 @@ pub enum Builtin {
17791779
TypeInterfaceId,
17801780
TypeRuntimeCode,
17811781
TypeCreatorCode,
1782+
ExtendTtl,
1783+
ExtendInstanceTtl,
17821784
}
17831785

17841786
#[derive(PartialEq, Eq, Clone, Debug)]

0 commit comments

Comments
 (0)