-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Also did some passing-by fixes.
- Loading branch information
Showing
3 changed files
with
187 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
docs/smart-contracts/guides/archival/test-ttl-extension.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
--- | ||
title: Test TTL extension logic in Smart Contracts | ||
hide_table_of_contents: true | ||
--- | ||
|
||
In order to test contracts that extend the contract data [TTL](../../../learn/smart-contract-internals/state-archival.mdx#ttl) via `extend_ttl` storage operations, you can use the TTL getter operation (`get_ttl`) in combination with manipulating the ledger sequence number. Note, that `get_ttl` function is only available for tests and only in Soroban SDK v21+. | ||
|
||
## Example | ||
|
||
Follow along the [example](https://github.com/stellar/soroban-examples/blob/main/ttl/src/lib.rs) that tests TTL extensions. The example has extensive comments, this document just highlights the most important parts. | ||
|
||
We use a very simple contract that only extends an entry for every Soroban storage type: | ||
|
||
```rust | ||
#[contractimpl] | ||
impl TtlContract { | ||
/// Creates a contract entry in every kind of storage. | ||
pub fn setup(env: Env) { | ||
env.storage().persistent().set(&DataKey::MyKey, &0); | ||
env.storage().instance().set(&DataKey::MyKey, &1); | ||
env.storage().temporary().set(&DataKey::MyKey, &2); | ||
} | ||
|
||
/// Extend the persistent entry TTL to 5000 ledgers, when its | ||
/// TTL is smaller than 1000 ledgers. | ||
pub fn extend_persistent(env: Env) { | ||
env.storage() | ||
.persistent() | ||
.extend_ttl(&DataKey::MyKey, 1000, 5000); | ||
} | ||
|
||
/// Extend the instance entry TTL to become at least 10000 ledgers, | ||
/// when its TTL is smaller than 2000 ledgers. | ||
pub fn extend_instance(env: Env) { | ||
env.storage().instance().extend_ttl(2000, 10000); | ||
} | ||
|
||
/// Extend the temporary entry TTL to become at least 7000 ledgers, | ||
/// when its TTL is smaller than 3000 ledgers. | ||
pub fn extend_temporary(env: Env) { | ||
env.storage() | ||
.temporary() | ||
.extend_ttl(&DataKey::MyKey, 3000, 7000); | ||
} | ||
} | ||
``` | ||
|
||
The focus of the example is the tests, so the following code snippets come from `test.rs`. | ||
|
||
It's a good idea to define the custom values of TTL-related network settings, since the defaults are defined by the SDK and aren't immediately obvious for the reader of the tests: | ||
|
||
```rust | ||
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; | ||
}); | ||
``` | ||
|
||
You could also use the current [network settings](../../../reference/resource-limits-fees.mdx#resource-fees) when setting up the tests, but keep in mind that these are subject to change, and the contract should be able to work with any values of these settings. | ||
|
||
Now we run a test scenario that verifies the TTL extension logic (see [`test_extend_ttl_behavior`](https://github.com/stellar/soroban-examples/blob/f595fb5df06058ec0b9b829e9e4d0fe0513e0aa8/ttl/src/test.rs#L38) test for the full scenario). First, we setup the data and ensure that the initial TTL values correspond to the network settings we've defined above: | ||
|
||
```rust | ||
client.setup(); | ||
env.as_contract(&contract_id, || { | ||
// Note, that TTL doesn't include the current ledger, but when entry | ||
// is created the current ledger is counted towards the number of | ||
// ledgers specified by `min_persistent/temp_entry_ttl`, thus | ||
// the TTL is 1 ledger less than the respective setting. | ||
assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 499); | ||
assert_eq!(env.storage().instance().get_ttl(), 499); | ||
assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 99); | ||
}); | ||
``` | ||
|
||
Notice, that we use `env.as_contract` in order to access the contract's storage. | ||
|
||
Then we call the TTL extension operations and verify that they behave as expected, for example: | ||
|
||
```rust | ||
// Extend persistent entry TTL to 5000 ledgers - now it is 5000. | ||
client.extend_persistent(); | ||
env.as_contract(&contract_id, || { | ||
assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 5000); | ||
}); | ||
``` | ||
|
||
In order to test the extension thresholds (i.e. maximum current TTL that requires extension), we need to increase the ledger sequence number: | ||
|
||
```rust | ||
// Now bump the ledger sequence by 5000 in order to sanity-check | ||
// the threshold settings of `extend_ttl` operations. | ||
env.ledger().with_mut(|li| { | ||
li.sequence_number = 100_000 + 5_000; | ||
}); | ||
// Now the TTL of every entry has been reduced by 5000 ledgers. | ||
env.as_contract(&contract_id, || { | ||
assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 0); | ||
assert_eq!(env.storage().instance().get_ttl(), 5000); | ||
assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 2000); | ||
}); | ||
``` | ||
|
||
Then we can extend the entries again and ensure that only entries that are below threshold have been extended (specifically, persistent and temporary entries in this example): | ||
|
||
```rust | ||
client.extend_persistent(); | ||
client.extend_instance(); | ||
client.extend_temporary(); | ||
env.as_contract(&contract_id, || { | ||
assert_eq!(env.storage().persistent().get_ttl(&DataKey::MyKey), 5000); | ||
// Instance TTL hasn't been increased because the remaining TTL | ||
// (5000 ledgers) is larger than the threshold used by | ||
// `extend_instance` (2000 ledgers) | ||
assert_eq!(env.storage().instance().get_ttl(), 5000); | ||
assert_eq!(env.storage().temporary().get_ttl(&DataKey::MyKey), 7000); | ||
}); | ||
``` | ||
|
||
Soroban SDK also emulates the behavior for the entries that have their TTL expired. Temporary entries behave 'as if' they were deleted (see [`test_temp_entry_removal`](https://github.com/stellar/soroban-examples/blob/f595fb5df06058ec0b9b829e9e4d0fe0513e0aa8/ttl/src/test.rs#L112) test for the full scenario): | ||
|
||
```rust | ||
client.extend_temporary(); | ||
// Bump the ledger sequence by 7001 ledgers (one ledger past TTL). | ||
env.ledger().with_mut(|li| { | ||
li.sequence_number = 100_000 + 7001; | ||
}); | ||
// Now the entry is no longer present in the environment. | ||
env.as_contract(&contract_id, || { | ||
assert_eq!(env.storage().temporary().has(&DataKey::MyKey), false); | ||
}); | ||
``` | ||
|
||
Persistent entries are more subtle: when a transaction that is executed on-chain contains a persistent entry that has been archived (i.e. it has it's TTL expired) in the footprint, then the Soroban environment will not even be instantiated. Since this behavior is not directly reproducible in test environment, instead an irrecoverable 'internal' error will be produced as soon as an archived entry is accessed, and the test will `panic`: | ||
|
||
```rust | ||
#[test] | ||
#[should_panic(expected = "[testing-only] Accessed contract instance key that has been archived.")] | ||
fn test_persistent_entry_archival() { | ||
let env = create_env(); | ||
let contract_id = env.register_contract(None, TtlContract); | ||
let client = TtlContractClient::new(&env, &contract_id); | ||
client.setup(); | ||
// Extend the instance TTL to 10000 ledgers. | ||
client.extend_instance(); | ||
// Bump the ledger sequence by 10001 ledgers (one ledger past TTL). | ||
env.ledger().with_mut(|li| { | ||
li.sequence_number = 100_000 + 10_001; | ||
}); | ||
// Now any call involving the expired contract (such as `extend_instance` | ||
// call here) will panic as soon as that contract is accessed. | ||
client.extend_instance(); | ||
} | ||
``` | ||
|
||
## Testing TTL extension for other contract instances | ||
|
||
Sometimes a contract may want to extend TTL of another contracts and/or their Wasm entries (usually that would happen in factory contracts). This logic may be covered in a similar fashion to the example above using `env.deployer().get_contract_instance_ttl(&contract)` to get TTL of any contract's instance, and `env.deployer().get_contract_code_ttl(&contract)` to get TTL of any contract's Wasm entry. You can find an example of using these function in the SDK [test suite](https://github.com/stellar/rs-soroban-sdk/blob/ff05c3d4cbed97db50142372e9d7a4fa4a8d1d5d/soroban-sdk/src/tests/storage_testutils.rs#L76). |