Skip to main content

tycho_executor/phase/
storage.rs

1use anyhow::Result;
2use tycho_types::models::{AccountState, AccountStatus, AccountStatusChange, StoragePhase};
3use tycho_types::num::Tokens;
4
5use crate::phase::receive::ReceivedMessage;
6use crate::{ExecutorInspector, ExecutorState, PublicLibraryChange};
7
8/// Storage phase input context.
9pub struct StoragePhaseContext<'a, 'e> {
10    /// Whether to adjust remaining message balance
11    /// if it becomes greater than the account balance.
12    pub adjust_msg_balance: bool,
13    /// Received message (external or internal).
14    pub received_message: Option<&'a mut ReceivedMessage>,
15    /// Executor inspector.
16    pub inspector: Option<&'a mut ExecutorInspector<'e>>,
17}
18
19impl ExecutorState<'_> {
20    /// Storage phase of ordinary or ticktock transactions.
21    ///
22    /// - Precedes the credit phase when [`bounce_enabled`],
23    ///   otherwise must be called after it;
24    /// - Necessary for all types of messages or even without them;
25    /// - Computes storage fee and due payment;
26    /// - Tries to charge the account balance for the storage fees;
27    /// - Freezes or deletes the account if its balance is not enough
28    ///   (doesn't change the state itself, but rather tells other
29    ///   phases to do so).
30    ///
31    /// Returns an executed [`StoragePhase`].
32    ///
33    /// Fails if called in an older context than account's [`last_paid`].
34    /// Can also fail on [`total_fees`] overflow, but this should
35    /// not happen on networks with valid value flow.
36    ///
37    /// [`bounce_enabled`]: ReceivedMessage::bounce_enabled
38    /// [`last_paid`]: tycho_types::models::StorageInfo::last_paid
39    /// [`total_fees`]: Self::total_fees
40    pub fn storage_phase(&mut self, ctx: StoragePhaseContext<'_, '_>) -> Result<StoragePhase> {
41        anyhow::ensure!(
42            self.params.block_unixtime >= self.storage_stat.last_paid,
43            "current unixtime is less than the account last_paid",
44        );
45
46        let is_masterchain = self.address.is_masterchain();
47        let config = self.config.gas_prices(is_masterchain);
48
49        if self.is_suspended_by_marks {
50            // Skip storage phase for accounts suspended by marks.
51            // Authority has frozen the exact amount of tokens so this value
52            // cannot be changed.
53            return Ok(StoragePhase {
54                storage_fees_collected: Tokens::ZERO,
55                storage_fees_due: self.storage_stat.due_payment,
56                status_change: AccountStatusChange::Unchanged,
57            });
58        }
59
60        // Compute how much this account must pay for storing its state up until now.
61        let mut to_pay = self.config.compute_storage_fees(
62            &self.storage_stat,
63            self.params.block_unixtime,
64            self.is_special,
65            is_masterchain,
66        );
67        if let Some(due_payment) = self.storage_stat.due_payment {
68            // NOTE: We are using saturating math here to reduce strange
69            // invariants. If account must pay more than `Tokens::MAX`,
70            // it will certanly be frozen in almost any real scenario.
71            to_pay = to_pay.saturating_add(due_payment);
72        }
73
74        // Update `last_paid` (only for ordinary accounts).
75        self.storage_stat.last_paid = if self.is_special {
76            0
77        } else {
78            self.params.block_unixtime
79        };
80
81        // Start filling the storage phase.
82        let storage_fees_collected;
83        let storage_fees_due;
84        let status_change;
85
86        if to_pay.is_zero() {
87            // No fees at all.
88            storage_fees_collected = Tokens::ZERO;
89            storage_fees_due = None;
90            status_change = AccountStatusChange::Unchanged;
91        } else if let Some(new_balance) = self.balance.tokens.checked_sub(to_pay) {
92            // Account balance is enough to pay storage fees.
93            storage_fees_collected = to_pay;
94            storage_fees_due = None;
95            status_change = AccountStatusChange::Unchanged;
96
97            // Update account balance.
98            self.balance.tokens = new_balance;
99            // Reset `due_payment` if there was any.
100            self.storage_stat.due_payment = None;
101        } else {
102            // Account balance is not enough to pay storage fees,
103            // so we use all of its balance and try to freeze the account.
104            let fees_due = to_pay - self.balance.tokens;
105
106            storage_fees_collected = std::mem::take(&mut self.balance.tokens);
107            storage_fees_due = Some(fees_due).filter(|t| !t.is_zero());
108
109            debug_assert!(self.balance.tokens.is_zero());
110
111            // NOTE: Keep all cases explicit here without "default" branch.
112            status_change = match &self.state {
113                // Do nothing for special accounts.
114                _ if self.is_special => AccountStatusChange::Unchanged,
115                // Try to delete account.
116                AccountState::Uninit | AccountState::Frozen { .. }
117                    if (matches!(&self.state, AccountState::Uninit)
118                        || !self.params.disable_delete_frozen_accounts)
119                        && fees_due.into_inner() > config.delete_due_limit as u128
120                        && self.balance.other.is_empty() =>
121                {
122                    AccountStatusChange::Deleted
123                }
124                // Do nothing if not deleting.
125                AccountState::Uninit | AccountState::Frozen { .. } => {
126                    AccountStatusChange::Unchanged
127                }
128                // Freeze account with big enough due.
129                AccountState::Active { .. }
130                    if fees_due.into_inner() > config.freeze_due_limit as u128 =>
131                {
132                    AccountStatusChange::Frozen
133                }
134                // Do nothing if `fees_due` is not big enough.
135                AccountState::Active { .. } => AccountStatusChange::Unchanged,
136            };
137
138            if !self.is_special {
139                // Update account's due payment.
140                self.storage_stat.due_payment = storage_fees_due;
141            }
142        };
143
144        // Apply status change.
145        match status_change {
146            AccountStatusChange::Unchanged => {}
147            AccountStatusChange::Frozen => {
148                // NOTE: We are not changing the account state yet, just updating status.
149                self.end_status = AccountStatus::Frozen;
150            }
151            AccountStatusChange::Deleted => {
152                self.end_status = AccountStatus::NotExists;
153            }
154        }
155
156        // Apply public libraries diff in case of masterchain account freeze/deletion.
157        if is_masterchain
158            && status_change != AccountStatusChange::Unchanged
159            && let AccountState::Active(state) = &self.state
160            && let Some(inspector) = ctx.inspector
161        {
162            for entry in state.libraries.values() {
163                let lib = entry?;
164                if lib.public {
165                    inspector
166                        .public_libs_diff
167                        .insert(*lib.root.repr_hash(), PublicLibraryChange::Remove);
168                }
169            }
170        }
171
172        // Adjust message value.
173        if ctx.adjust_msg_balance
174            && let Some(msg) = ctx.received_message
175            && msg.balance_remaining.tokens > self.balance.tokens
176        {
177            msg.balance_remaining.tokens = self.balance.tokens;
178        }
179
180        // Add fees.
181        self.total_fees.try_add_assign(storage_fees_collected)?;
182
183        // Done
184        Ok(StoragePhase {
185            storage_fees_collected,
186            storage_fees_due,
187            status_change,
188        })
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use std::collections::BTreeMap;
195
196    use tycho_asm_macros::tvmasm;
197    use tycho_types::cell::{CellBuilder, HashBytes};
198    use tycho_types::dict::Dict;
199    use tycho_types::models::{
200        AuthorityMarksConfig, CurrencyCollection, StdAddr, StorageInfo, StorageUsed,
201    };
202    use tycho_types::num::{VarUint56, VarUint248};
203
204    use super::*;
205    use crate::tests::{make_custom_config, make_default_config, make_default_params};
206    use crate::util::shift_ceil_price;
207    use crate::{ExecutorParams, ParsedConfig};
208
209    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
210
211    fn fee_for_storing(used: StorageUsed, delta: u32, config: &ParsedConfig) -> Tokens {
212        let prices = config.storage_prices.last().unwrap();
213        let fee = (prices.bit_price_ps as u128)
214            .saturating_mul(used.bits.into_inner() as u128)
215            .saturating_add(
216                (prices.cell_price_ps as u128).saturating_mul(used.cells.into_inner() as u128),
217            )
218            .saturating_mul(delta as _);
219        Tokens::new(shift_ceil_price(fee))
220    }
221
222    #[test]
223    fn account_has_enough_balance() {
224        let mut params = make_default_params();
225        let config = make_default_config();
226
227        params.block_unixtime = 2000;
228
229        let mut state =
230            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(1_000_000_000));
231        state.storage_stat = StorageInfo {
232            used: StorageUsed {
233                bits: VarUint56::new(1000),
234                cells: VarUint56::new(10),
235            },
236            storage_extra: Default::default(),
237            last_paid: 1000,
238            due_payment: None,
239        };
240
241        let prev_storage_stat = state.storage_stat.clone();
242        let prev_balance = state.balance.clone();
243        let prev_status = state.end_status;
244        let prev_total_fees = state.total_fees;
245
246        let storage_phase = state
247            .storage_phase(StoragePhaseContext {
248                adjust_msg_balance: false,
249                received_message: None,
250                inspector: None,
251            })
252            .unwrap();
253
254        // Account status must not change.
255        assert_eq!(state.end_status, prev_status);
256        assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
257        // No extra fees must be taken.
258        assert_eq!(state.balance.other, prev_balance.other);
259        assert_eq!(
260            state.balance.tokens,
261            prev_balance.tokens - storage_phase.storage_fees_collected
262        );
263        assert_eq!(
264            state.total_fees,
265            prev_total_fees + storage_phase.storage_fees_collected
266        );
267        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
268        assert_eq!(
269            storage_phase.storage_fees_collected,
270            fee_for_storing(prev_storage_stat.used.clone(), 1000, &config)
271        );
272        // All fees must be paid.
273        assert!(storage_phase.storage_fees_due.is_none());
274        // Storage stat must be updated.
275        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
276        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
277        assert!(state.storage_stat.due_payment.is_none());
278    }
279
280    #[test]
281    fn account_does_not_have_enough_balance() {
282        let mut params = make_default_params();
283        let config = make_default_config();
284
285        let mut balance = CurrencyCollection::from(Tokens::new(1));
286        let mut storage_stat = StorageInfo {
287            used: StorageUsed {
288                bits: VarUint56::new(1000),
289                cells: VarUint56::new(10),
290            },
291            storage_extra: Default::default(),
292            last_paid: 1000,
293            due_payment: None,
294        };
295
296        for time in [2000, 3000, 4000] {
297            params.block_unixtime = time;
298
299            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, balance);
300            state.storage_stat = storage_stat.clone();
301
302            let prev_storage_stat = state.storage_stat.clone();
303            let prev_balance = state.balance.clone();
304            let prev_status = state.end_status;
305            let prev_total_fees = state.total_fees;
306
307            let storage_phase = state
308                .storage_phase(StoragePhaseContext {
309                    adjust_msg_balance: false,
310                    received_message: None,
311                    inspector: None,
312                })
313                .unwrap();
314
315            // Account status must not change.
316            assert_eq!(state.end_status, prev_status);
317            assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
318            // Account balance in tokens must be empty.
319            assert_eq!(state.balance.tokens, Tokens::ZERO);
320            // No extra fees must be taken.
321            assert_eq!(state.balance.other, prev_balance.other);
322            assert_eq!(
323                state.balance.tokens,
324                prev_balance.tokens - storage_phase.storage_fees_collected
325            );
326            assert_eq!(
327                state.total_fees,
328                prev_total_fees + storage_phase.storage_fees_collected
329            );
330            // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
331            let delta = params.block_unixtime - prev_storage_stat.last_paid;
332            let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
333            let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
334            assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
335            // All fees must be paid.
336            assert_eq!(
337                storage_phase.storage_fees_due,
338                Some(prev_due + target_fee - prev_balance.tokens)
339            );
340            // Storage stat must be updated.
341            assert_eq!(state.storage_stat.used, prev_storage_stat.used);
342            assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
343            assert_eq!(
344                state.storage_stat.due_payment,
345                Some(prev_due + target_fee - prev_balance.tokens)
346            );
347
348            balance = state.balance;
349            storage_stat = state.storage_stat;
350            println!("storage_stat: {storage_stat:#?}");
351        }
352    }
353
354    #[test]
355    fn account_freezes_with_storage_due() {
356        let mut params = make_default_params();
357        let config = make_default_config();
358
359        params.block_unixtime = 2000;
360
361        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::ZERO);
362        state.state = AccountState::Active(Default::default());
363        state.end_status = AccountStatus::Active; // Only active accounts can be frozen.
364        state.storage_stat = StorageInfo {
365            used: StorageUsed {
366                bits: VarUint56::new(1000),
367                cells: VarUint56::new(10),
368            },
369            storage_extra: Default::default(),
370            last_paid: 1000,
371            due_payment: Some(Tokens::new(config.gas_prices.freeze_due_limit as u128 - 50)),
372        };
373
374        let prev_storage_stat = state.storage_stat.clone();
375        let prev_balance = state.balance.clone();
376        let prev_total_fees = state.total_fees;
377
378        let storage_phase = state
379            .storage_phase(StoragePhaseContext {
380                adjust_msg_balance: false,
381                received_message: None,
382                inspector: None,
383            })
384            .unwrap();
385
386        // Account status must not change.
387        assert_eq!(state.end_status, AccountStatus::Frozen);
388        assert_eq!(storage_phase.status_change, AccountStatusChange::Frozen);
389        // Account balance in tokens must be empty.
390        assert_eq!(state.balance.tokens, Tokens::ZERO);
391        // No extra fees must be taken.
392        assert_eq!(state.balance.other, prev_balance.other);
393        assert_eq!(
394            state.balance.tokens,
395            prev_balance.tokens - storage_phase.storage_fees_collected
396        );
397        assert_eq!(
398            state.total_fees,
399            prev_total_fees + storage_phase.storage_fees_collected
400        );
401        // Expect account to pay for storing 1000 bits and 10 cells for 1000 seconds.
402        let delta = params.block_unixtime - prev_storage_stat.last_paid;
403        let prev_due = prev_storage_stat.due_payment.unwrap_or_default();
404        let target_fee = fee_for_storing(prev_storage_stat.used.clone(), delta, &config);
405        assert_eq!(storage_phase.storage_fees_collected, prev_balance.tokens);
406        // All fees must be paid.
407        assert_eq!(
408            storage_phase.storage_fees_due,
409            Some(prev_due + target_fee - prev_balance.tokens)
410        );
411        // Storage stat must be updated.
412        assert_eq!(state.storage_stat.used, prev_storage_stat.used);
413        assert_eq!(state.storage_stat.last_paid, params.block_unixtime);
414        assert_eq!(
415            state.storage_stat.due_payment,
416            Some(prev_due + target_fee - prev_balance.tokens)
417        );
418    }
419
420    #[test]
421    fn suspended_account_storage_phase_skipped() -> anyhow::Result<()> {
422        let params = ExecutorParams {
423            authority_marks_enabled: true,
424            ..make_default_params()
425        };
426
427        let config = make_custom_config(|config| {
428            config.set_authority_marks_config(&AuthorityMarksConfig {
429                authority_addresses: Dict::new(),
430                black_mark_id: 100,
431                white_mark_id: 101,
432            })?;
433            Ok(())
434        });
435
436        let mut state = ExecutorState::new_active(
437            &params,
438            &config,
439            &STUB_ADDR,
440            CurrencyCollection {
441                tokens: Tokens::MAX,
442                other: BTreeMap::from_iter([
443                    (100u32, VarUint248::new(500)), // black marks
444                ])
445                .try_into()?,
446            },
447            CellBuilder::build_from(u32::MIN)?,
448            tvmasm!("ACCEPT"),
449        );
450        state.storage_stat = StorageInfo {
451            used: StorageUsed {
452                bits: VarUint56::new(1000),
453                cells: VarUint56::new(10),
454            },
455            storage_extra: Default::default(),
456            last_paid: 1000,
457            due_payment: None,
458        };
459
460        let prev_storage_stat = state.storage_stat.clone();
461        let prev_balance = state.balance.clone();
462        let prev_status = state.end_status;
463        let prev_total_fees = state.total_fees;
464        let prev_due = state.storage_stat.due_payment;
465
466        let storage_phase = state.storage_phase(StoragePhaseContext {
467            adjust_msg_balance: false,
468            received_message: None,
469            inspector: None,
470        })?;
471
472        // everything should not change
473        assert_eq!(prev_storage_stat, state.storage_stat);
474        assert_eq!(prev_balance, state.balance);
475        assert_eq!(prev_status, state.end_status);
476        assert_eq!(prev_total_fees, state.total_fees);
477
478        assert_eq!(storage_phase.storage_fees_collected, Tokens::ZERO);
479        assert_eq!(storage_phase.storage_fees_due, prev_due);
480        assert_eq!(storage_phase.status_change, AccountStatusChange::Unchanged);
481
482        Ok(())
483    }
484}