Skip to content

Commit

Permalink
Add a guide for TTL testing. (#630)
Browse files Browse the repository at this point in the history
Also did some passing-by fixes.
  • Loading branch information
dmkozh authored May 29, 2024
1 parent 1182f7f commit b6d3e71
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 6 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,17 @@ If you have questions, feel free to ask in the [Stellar Developer Discord](https
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)][codespaces]
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][gitpod]

To begin development on the documentation, you will first need [yarn][yarn]
installed on your system. Once it is, you can run the following commands:
### Prerequisites

To begin development on the documentation, you will first need to install the following:

- Node.js (v19, not higher): see https://nodejs.org/en/download/package-manager for details for your system
- npm: e.g. `sudo apt install npm` on Ubuntu
- yarn: `npm install yarn`

### Development

Once all the prerequisites have been installed, you can run the following commands:

```bash
git clone https://github.com/stellar/stellar-docs
Expand All @@ -69,8 +78,8 @@ This will begin the development server, and open a browser window/tab pointing
to `http://localhost:3000/docs/`. This development server will auto-reload when
it detects changes to the repository.

After you've made your changes, please use `prettier` to ensure consistent
formatting throughout the repository:
After you've made your changes, use the following commands to ensure the consistent
MDX file formatting and style across the repository:

```bash
npm run check:mdx # this will search for problems in the MDX files
Expand Down Expand Up @@ -311,7 +320,6 @@ SDF’s failure to enforce any of these terms or guidelines shall not constitute
[coc]: https://github.com/stellar/.github/blob/master/CODE_OF_CONDUCT.md
[docusaurus]: https://docusaurus.io
[mdx]: https://mdxjs.com
[yarn]: https://yarnpkg.com/
[commonmark]: https://commonmark.org/help/
[tutorial]: https://www.markdowntutorial.com/
[guide]: https://www.markdownguide.org/
Expand Down
4 changes: 3 additions & 1 deletion docs/smart-contracts/example-contracts/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ pub enum DataKey {

Different enum values create different key 'namespaces'.

In the example the counter for each address is stored against `DataKey::Counter(Address)`. If the contract needs to start storing other types of data, it can do so by adding additional variants to the enum. :::
In the example the counter for each address is stored against `DataKey::Counter(Address)`. If the contract needs to start storing other types of data, it can do so by adding additional variants to the enum.

:::

### `require_auth`

Expand Down
171 changes: 171 additions & 0 deletions docs/smart-contracts/guides/archival/test-ttl-extension.mdx
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).

0 comments on commit b6d3e71

Please sign in to comment.