diff --git a/README.md b/README.md index 8f8a0d870..9b48f2150 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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/ diff --git a/docs/smart-contracts/example-contracts/auth.mdx b/docs/smart-contracts/example-contracts/auth.mdx index b3917bc96..12237c5f5 100644 --- a/docs/smart-contracts/example-contracts/auth.mdx +++ b/docs/smart-contracts/example-contracts/auth.mdx @@ -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` diff --git a/docs/smart-contracts/guides/archival/test-ttl-extension.mdx b/docs/smart-contracts/guides/archival/test-ttl-extension.mdx new file mode 100644 index 000000000..98d7d84ea --- /dev/null +++ b/docs/smart-contracts/guides/archival/test-ttl-extension.mdx @@ -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).