|
| 1 | +//! Accessing archived persistent entries in tests no longer results in an error. |
| 2 | +//! |
| 3 | +//! Prior to protocol 23 the SDK used to emulate the failure when an archived |
| 4 | +//! ledger entry was accessed in tests. This behavior has never represented the |
| 5 | +//! actual behavior of the network, just one possible scenario when an archived |
| 6 | +//! entry is present in the transaction footprint. |
| 7 | +//! |
| 8 | +//! In protocol 23 automatic entry restoration has been introduced, which makes |
| 9 | +//! it possible for a transaction to restore an archived entry before accessing |
| 10 | +//! it. As this behavior will become the most common case on the network, the |
| 11 | +//! SDK has been changed to emulate automatic restoration in tests as well. |
| 12 | +//! |
| 13 | +//! Note, that instance storage is a persistent entry as well, so it is subject |
| 14 | +//! to the same change. |
| 15 | +//! |
| 16 | +//! ## Example |
| 17 | +//! |
| 18 | +//! Consider the following simple contract that extends entry TTL |
| 19 | +//! along with the test that relies on the error on archived entry access in |
| 20 | +//! SDK 22: |
| 21 | +//! |
| 22 | +//! ``` |
| 23 | +//! #![no_std] |
| 24 | +//! use soroban_sdk::{contract, contractimpl, contracttype, Env}; |
| 25 | +//! |
| 26 | +//! #[contract] |
| 27 | +//! struct Contract; |
| 28 | +//! |
| 29 | +//! #[contracttype] |
| 30 | +//! enum DataKey { |
| 31 | +//! Key, |
| 32 | +//! } |
| 33 | +//! |
| 34 | +//! #[contractimpl] |
| 35 | +//! impl Contract { |
| 36 | +//! pub fn create_and_extend_entry(env: Env) { |
| 37 | +//! env.storage().persistent().set(&DataKey::Key, &123_u32); |
| 38 | +//! // Extend the entry to live for at least 1_000_000 ledgers. |
| 39 | +//! env.storage() |
| 40 | +//! .persistent() |
| 41 | +//! .extend_ttl(&DataKey::Key, 1_000_000, 1_000_000); |
| 42 | +//! } |
| 43 | +//! |
| 44 | +//! pub fn read_entry(env: Env) -> u32 { |
| 45 | +//! env.storage().persistent().get(&DataKey::Key).unwrap() |
| 46 | +//! } |
| 47 | +//! } |
| 48 | +//! |
| 49 | +//! mod test { |
| 50 | +//! extern crate std; |
| 51 | +//! use soroban_sdk::testutils::{storage::Persistent, Ledger}; |
| 52 | +//! |
| 53 | +//! use super::*; |
| 54 | +//! |
| 55 | +//! #[test] |
| 56 | +//! fn test_entry_archived() { |
| 57 | +//! let env = Env::default(); |
| 58 | +//! let contract = env.register(Contract, ()); |
| 59 | +//! let client = ContractClient::new(&env, &contract); |
| 60 | +//! client.create_and_extend_entry(); |
| 61 | +//! let current_ledger = env.ledger().sequence(); |
| 62 | +//! assert_eq!(client.read_entry(), 123); |
| 63 | +//! |
| 64 | +//! // Bump ledger sequence past entry TTL. |
| 65 | +//! env.ledger() |
| 66 | +//! .set_sequence_number(current_ledger + 1_000_000 + 1); |
| 67 | +//! let res = client.try_read_entry(); |
| 68 | +//! // 👀 In SDK 22 `res` would be an error because the entry is archived. |
| 69 | +//! // 👀 In SDK 23 `res` is Ok(123) because the entry is automatically restored. |
| 70 | +//! assert!(res.is_err()); |
| 71 | +//! } |
| 72 | +//! } |
| 73 | +//! |
| 74 | +//! # fn main() { } |
| 75 | +//! ``` |
| 76 | +//! |
| 77 | +//! The best way to address this change is to update the tests to explicitly |
| 78 | +//! verify the expected entry TTL after the extension. This way there is no need |
| 79 | +//! to rely on the storage behavior, and also the test becomes more robust as |
| 80 | +//! it enforces the exact expected TTL value, so there is no risk of bumping |
| 81 | +//! the ledger sequence further than the expected TTL and still having the test |
| 82 | +//! pass. |
| 83 | +//! |
| 84 | +//! The example test above can be re-written as follows: |
| 85 | +//! |
| 86 | +//! ``` |
| 87 | +//! #![no_std] |
| 88 | +//! use soroban_sdk::{contract, contractimpl, contracttype, Env}; |
| 89 | +//! |
| 90 | +//! #[contract] |
| 91 | +//! struct Contract; |
| 92 | +//! |
| 93 | +//! #[contracttype] |
| 94 | +//! enum DataKey { |
| 95 | +//! Key, |
| 96 | +//! } |
| 97 | +//! |
| 98 | +//! #[contractimpl] |
| 99 | +//! impl Contract { |
| 100 | +//! pub fn create_and_extend_entry(env: Env) { |
| 101 | +//! env.storage().persistent().set(&DataKey::Key, &123_u32); |
| 102 | +//! // Extend the entry to live for at least 1_000_000 ledgers. |
| 103 | +//! env.storage() |
| 104 | +//! .persistent() |
| 105 | +//! .extend_ttl(&DataKey::Key, 1_000_000, 1_000_000); |
| 106 | +//! } |
| 107 | +//! |
| 108 | +//! pub fn read_entry(env: Env) -> u32 { |
| 109 | +//! env.storage().persistent().get(&DataKey::Key).unwrap() |
| 110 | +//! } |
| 111 | +//! } |
| 112 | +//! |
| 113 | +//! #[cfg(test)] |
| 114 | +//! mod test { |
| 115 | +//! extern crate std; |
| 116 | +//! use soroban_sdk::testutils::{storage::Persistent, Ledger}; |
| 117 | +//! |
| 118 | +//! use super::*; |
| 119 | +//! |
| 120 | +//! #[test] |
| 121 | +//! fn test_entry_ttl_extended() { |
| 122 | +//! let env = Env::default(); |
| 123 | +//! let contract = env.register(Contract, ()); |
| 124 | +//! let client = ContractClient::new(&env, &contract); |
| 125 | +//! client.create_and_extend_entry(); |
| 126 | +//! assert_eq!(client.read_entry(), 123); |
| 127 | +//! |
| 128 | +//! // 👀 Verify that the entry TTL was extended correctly by 1000000 ledgers. |
| 129 | +//! env.as_contract(&contract, || { |
| 130 | +//! assert_eq!(env.storage().persistent().get_ttl(&DataKey::Key), 1_000_000); |
| 131 | +//! }); |
| 132 | +//! } |
| 133 | +//! |
| 134 | +//! // 👀 This test is not really necessary, but it demonstrates the |
| 135 | +//! // auto-restoration behavior in tests. |
| 136 | +//! #[test] |
| 137 | +//! fn test_auto_restore() { |
| 138 | +//! let env = Env::default(); |
| 139 | +//! let contract = env.register(Contract, ()); |
| 140 | +//! let client = ContractClient::new(&env, &contract); |
| 141 | +//! client.create_and_extend_entry(); |
| 142 | +//! let current_ledger = env.ledger().sequence(); |
| 143 | +//! |
| 144 | +//! // Bump ledger sequence past entry TTL. |
| 145 | +//! env.ledger() |
| 146 | +//! .set_sequence_number(current_ledger + 1_000_000 + 1); |
| 147 | +//! // 👀 Entry can still be accessed because automatic restoration is emulated |
| 148 | +//! // in tests. |
| 149 | +//! assert_eq!(client.read_entry(), 123); |
| 150 | +//! |
| 151 | +//! // 👀 Automatic restoration is also accounted for in cost_estimate(): |
| 152 | +//! let resources = env.cost_estimate().resources(); |
| 153 | +//! // Even though `read_entry` call is normally read-only, auto-restoration |
| 154 | +//! // will cause 2 entry writes here: 1 for the contract instance, another |
| 155 | +//! // one for the restored entry. |
| 156 | +//! assert_eq!(resources.write_entries, 2); |
| 157 | +//! // 2 rent bumps will happen as well for the respective entries. |
| 158 | +//! assert_eq!(resources.persistent_entry_rent_bumps, 2); |
| 159 | +//! |
| 160 | +//! // 👀 Entry TTL after auto-restoration can be observed via get_ttl(). |
| 161 | +//! env.as_contract(&contract, || { |
| 162 | +//! // Auto-restored entries have their TTL extended by the minimum |
| 163 | +//! // possible TTL worth of ledgers (`min_persistent_entry_ttl`), |
| 164 | +//! // including the ledger in which they were restored (that's why |
| 165 | +//! // we subtract 1 here). |
| 166 | +//! assert_eq!( |
| 167 | +//! env.storage().persistent().get_ttl(&DataKey::Key), |
| 168 | +//! env.ledger().get().min_persistent_entry_ttl - 1 |
| 169 | +//! ); |
| 170 | +//! }); |
| 171 | +//! } |
| 172 | +//! } |
| 173 | +//! |
| 174 | +//! # fn main() { } |
| 175 | +//! ``` |
| 176 | +//! |
0 commit comments