Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a guide for TTL testing. #630

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,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 @@ -66,8 +75,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 @@ -228,7 +237,6 @@ const CODE_LANGS = {
[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).
Loading