tycho_executor/phase/
compute.rs

1use anyhow::Result;
2use num_bigint::{BigInt, Sign};
3use tycho_types::models::{
4    AccountState, AccountStatus, ComputePhase, ComputePhaseSkipReason, CurrencyCollection,
5    ExecutedComputePhase, IntAddr, IntMsgInfo, MsgType, SkippedComputePhase, StateInit, TickTock,
6};
7use tycho_types::num::Tokens;
8use tycho_types::prelude::*;
9use tycho_vm::{SafeRc, SmcInfoBase, Stack, Tuple, UnpackedInMsgSmcInfo, VmState, tuple};
10
11use crate::phase::receive::{MsgStateInit, ReceivedMessage};
12use crate::util::{
13    StateLimitsResult, check_state_limits_diff, new_varuint24_truncate, new_varuint56_truncate,
14    unlikely,
15};
16use crate::{ExecutorInspector, ExecutorState};
17
18/// Compute phase input context.
19pub struct ComputePhaseContext<'a, 'e> {
20    /// Parsed transaction input.
21    pub input: TransactionInput<'a>,
22    /// Fees collected during the storage phase.
23    pub storage_fee: Tokens,
24    /// Accept message even without opcode.
25    ///
26    /// Should only be used as part of the `run_local` stuff.
27    pub force_accept: bool,
28    /// Executor inspector.
29    pub inspector: Option<&'a mut ExecutorInspector<'e>>,
30}
31
32/// Parsed transaction input.
33#[derive(Debug, Clone, Copy)]
34pub enum TransactionInput<'a> {
35    Ordinary(&'a ReceivedMessage),
36    TickTock(TickTock),
37}
38
39impl<'a> TransactionInput<'a> {
40    const fn is_ordinary(&self) -> bool {
41        matches!(self, Self::Ordinary(_))
42    }
43
44    fn in_msg(&self) -> Option<&'a ReceivedMessage> {
45        match self {
46            Self::Ordinary(msg) => Some(msg),
47            Self::TickTock(_) => None,
48        }
49    }
50
51    fn in_msg_init(&self) -> Option<&'a MsgStateInit> {
52        match self {
53            Self::Ordinary(msg) => msg.init.as_ref(),
54            Self::TickTock(_) => None,
55        }
56    }
57}
58
59/// Executed compute phase with additional info.
60#[derive(Debug)]
61pub struct ComputePhaseFull {
62    /// Resulting comput phase.
63    pub compute_phase: ComputePhase,
64    /// Whether the inbound message was accepted.
65    ///
66    /// NOTE: Message can be accepted even without a committed state,
67    /// so we can't use [`ExecutedComputePhase::success`].
68    pub accepted: bool,
69    /// Original account balance before this phase.
70    pub original_balance: CurrencyCollection,
71    /// New account state.
72    pub new_state: StateInit,
73    /// Resulting actions list.
74    pub actions: Cell,
75}
76
77impl ExecutorState<'_> {
78    /// Compute phase of ordinary or ticktock transactions.
79    ///
80    /// - Tries to deploy or unfreeze account if it was [`Uninit`] or [`Frozen`];
81    /// - Executes contract code and produces a new account state;
82    /// - Produces an action list on successful execution;
83    /// - External messages can be ignored if they were not accepted;
84    /// - Necessary for all types of messages or even without them;
85    ///
86    /// Returns an executed [`ComputePhase`] with extra data.
87    ///
88    /// Fails only on account balance overflow. This should not happen on networks
89    /// with valid value flow.
90    ///
91    /// [`Uninit`]: AccountState::Uninit
92    /// [`Frozen`]: AccountState::Frozen
93    pub fn compute_phase(&mut self, ctx: ComputePhaseContext<'_, '_>) -> Result<ComputePhaseFull> {
94        let is_masterchain = self.address.is_masterchain();
95
96        // Compute original balance for the action phase.
97        let mut original_balance = self.balance.clone();
98        if let Some(msg) = ctx.input.in_msg() {
99            original_balance.try_sub_assign(&msg.balance_remaining)?;
100        }
101
102        // Prepare full phase output.
103        let new_state = if let AccountState::Active(current) = &self.state {
104            current.clone()
105        } else {
106            Default::default()
107        };
108
109        let mut res = ComputePhaseFull {
110            compute_phase: ComputePhase::Skipped(SkippedComputePhase {
111                reason: ComputePhaseSkipReason::NoGas,
112            }),
113            accepted: false,
114            original_balance,
115            new_state,
116            actions: Cell::empty_cell(),
117        };
118
119        // Compute VM gas limits.
120        if self.balance.tokens.is_zero() {
121            res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
122                reason: ComputePhaseSkipReason::NoGas,
123            });
124            return Ok(res);
125        }
126
127        // Skip compute phase for accounts suspended by authority marks.
128        if self.is_suspended_by_marks {
129            res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
130                reason: ComputePhaseSkipReason::Suspended,
131            });
132            return Ok(res);
133        }
134
135        let (msg_balance_remaining, is_external) = match ctx.input.in_msg() {
136            Some(msg) => (msg.balance_remaining.clone(), msg.is_external),
137            None => (CurrencyCollection::ZERO, false),
138        };
139
140        let gas = if unlikely(ctx.force_accept) {
141            tycho_vm::GasParams::getter()
142        } else {
143            self.config.compute_gas_params(
144                &self.balance.tokens,
145                &msg_balance_remaining.tokens,
146                self.is_special,
147                is_masterchain,
148                ctx.input.is_ordinary(),
149                is_external,
150            )
151        };
152        if gas.limit == 0 && gas.credit == 0 {
153            res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
154                reason: ComputePhaseSkipReason::NoGas,
155            });
156            return Ok(res);
157        }
158
159        // Apply internal message state.
160        let state_libs;
161        let msg_libs;
162        let msg_state_used;
163        match (ctx.input.in_msg_init(), &self.state) {
164            // Uninit account cannot run anything without deploy.
165            (None, AccountState::Uninit) => {
166                res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
167                    reason: ComputePhaseSkipReason::NoState,
168                });
169                return Ok(res);
170            }
171            // Frozen account cannot run anything until receives its old state.
172            (None, AccountState::Frozen { .. }) => {
173                res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
174                    reason: ComputePhaseSkipReason::BadState,
175                });
176                return Ok(res);
177            }
178            // Active account simply runs its code. (use libraries from its state).
179            (None, AccountState::Active(StateInit { libraries, .. })) => {
180                state_libs = Some(libraries);
181                msg_libs = None;
182                msg_state_used = false;
183            }
184            // Received a new state init for an uninit account or an old state for a frozen account.
185            (Some(from_msg), AccountState::Uninit | AccountState::Frozen(..)) => {
186                let target_hash = if let AccountState::Frozen(old_hash) = &self.state {
187                    old_hash
188                } else {
189                    &self.address.address
190                };
191
192                if from_msg.root_hash() != target_hash || from_msg.parsed.split_depth.is_some() {
193                    // State hash mismatch, cannot use this state.
194                    // We also forbid using `split_depth` (for now).
195                    res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
196                        reason: ComputePhaseSkipReason::BadState,
197                    });
198                    return Ok(res);
199                }
200
201                // Check if we can use the new state from the message.
202                let mut limits = self.config.size_limits.clone();
203                if is_masterchain && matches!(&self.state, AccountState::Uninit) {
204                    // Forbid public libraries when deploying, allow for unfreezing.
205                    limits.max_acc_public_libraries = 0;
206                }
207
208                if matches!(
209                    check_state_limits_diff(
210                        &res.new_state,
211                        &from_msg.parsed,
212                        &limits,
213                        is_masterchain,
214                        &mut self.cached_storage_stat,
215                    ),
216                    StateLimitsResult::Exceeds
217                ) {
218                    res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
219                        reason: ComputePhaseSkipReason::BadState,
220                    });
221                    return Ok(res);
222                }
223
224                // NOTE: At this point the `cached_storage_stat` will always contain
225                // visited cells because previous state was not active and we
226                // handled a case when check overflowed.
227
228                // Use state
229                res.new_state = from_msg.parsed.clone();
230                self.state = AccountState::Active(res.new_state.clone());
231                msg_state_used = true;
232
233                // Use libraries.
234                state_libs = None;
235                msg_libs = Some(from_msg.parsed.libraries.clone());
236            }
237            (Some(from_msg), AccountState::Active(StateInit { libraries, .. })) => {
238                // Check if a state from the external message has the correct hash.
239                if is_external && from_msg.root_hash() != &self.address.address {
240                    res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
241                        reason: ComputePhaseSkipReason::BadState,
242                    });
243                    return Ok(res);
244                }
245
246                // We use only libraries here.
247                msg_state_used = false;
248
249                // Use libraries.
250                state_libs = Some(libraries);
251                msg_libs = Some(from_msg.parsed.libraries.clone());
252            }
253        }
254
255        // Unpack internal message.
256        let unpacked_in_msg = match ctx.input.in_msg() {
257            Some(msg) => msg.make_tuple()?,
258            None => None,
259        };
260
261        // Build VM stack and register info.
262        let stack = self.prepare_vm_stack(ctx.input);
263
264        let code = res.new_state.code.clone();
265
266        let smc_info = SmcInfoBase::new()
267            .with_now(self.params.block_unixtime)
268            .with_block_lt(self.params.block_lt)
269            .with_tx_lt(self.start_lt)
270            .with_mixed_rand_seed(&self.params.rand_seed, &self.address.address)
271            .with_account_balance(self.balance.clone())
272            .with_account_addr(self.address.clone().into())
273            .with_config(self.config.raw.params.clone())
274            .require_ton_v4()
275            .with_code(code.clone().unwrap_or_default())
276            .with_message_balance(msg_balance_remaining.clone())
277            .with_storage_fees(ctx.storage_fee)
278            .require_ton_v6()
279            .with_unpacked_config(self.config.unpacked.as_tuple())
280            .require_ton_v11()
281            .with_unpacked_in_msg(unpacked_in_msg);
282
283        let libraries = (msg_libs, state_libs, &self.params.libraries);
284        let mut vm = VmState::builder()
285            .with_smc_info(smc_info)
286            .with_code(code)
287            .with_data(res.new_state.data.clone().unwrap_or_default())
288            .with_libraries(&libraries)
289            .with_init_selector(false)
290            .with_raw_stack(stack)
291            .with_gas(gas)
292            .with_modifiers(self.params.vm_modifiers)
293            .build();
294
295        // Connect inspected output as debug.
296        let mut inspector_actions = None;
297        let mut inspector_exit_code = None;
298        let mut inspector_total_gas_used = None;
299        let mut missing_library = None;
300        if let Some(inspector) = ctx.inspector {
301            inspector_actions = Some(&mut inspector.actions);
302            inspector_exit_code = Some(&mut inspector.exit_code);
303            inspector_total_gas_used = Some(&mut inspector.total_gas_used);
304            missing_library = Some(&mut inspector.missing_library);
305            if let Some(debug) = inspector.debug.as_deref_mut() {
306                vm.debug = Some(debug);
307            }
308        }
309
310        // Run VM.
311        let exit_code = !vm.run();
312
313        if let Some(inspector_exit_code) = inspector_exit_code {
314            *inspector_exit_code = Some(exit_code);
315        }
316
317        let consumed_paid_gas = vm.gas.consumed();
318        if let Some(total_gas_used) = inspector_total_gas_used {
319            *total_gas_used = consumed_paid_gas.saturating_add(vm.gas.free_gas_consumed());
320        }
321
322        // Parse VM state.
323        res.accepted = ctx.force_accept || vm.gas.credit() == 0;
324        debug_assert!(
325            is_external || res.accepted,
326            "internal messages must be accepted"
327        );
328
329        let success = res.accepted && vm.committed_state.is_some();
330
331        let gas_used = std::cmp::min(consumed_paid_gas, vm.gas.limit());
332        let gas_fees = if res.accepted && !self.is_special {
333            self.config
334                .gas_prices(is_masterchain)
335                .compute_gas_fee(gas_used)
336        } else {
337            // We don't add any fees for messages that were not accepted.
338            Tokens::ZERO
339        };
340
341        let mut account_activated = false;
342        if res.accepted && msg_state_used {
343            account_activated = self.orig_status != AccountStatus::Active;
344            self.end_status = AccountStatus::Active;
345        }
346
347        if let Some(committed) = vm.committed_state
348            && res.accepted
349        {
350            res.new_state.data = Some(committed.c4);
351            res.actions = committed.c5;
352
353            // Set inspector actions.
354            if let Some(actions) = inspector_actions {
355                *actions = Some(res.actions.clone());
356            }
357        }
358
359        if let Some(missing_library) = missing_library {
360            *missing_library = vm.gas.missing_library();
361        }
362
363        self.balance.try_sub_assign_tokens(gas_fees)?;
364        self.total_fees.try_add_assign(gas_fees)?;
365
366        res.compute_phase = ComputePhase::Executed(ExecutedComputePhase {
367            success,
368            msg_state_used,
369            account_activated,
370            gas_fees,
371            gas_used: new_varuint56_truncate(gas_used),
372            // NOTE: Initial value is stored here (not `vm.gas.limit()`).
373            gas_limit: new_varuint56_truncate(gas.limit),
374            // NOTE: Initial value is stored here (not `vm.gas.credit()`).
375            gas_credit: (gas.credit != 0).then(|| new_varuint24_truncate(gas.credit)),
376            mode: 0,
377            exit_code,
378            exit_arg: if success {
379                None
380            } else {
381                vm.stack.get_exit_arg().filter(|x| *x != 0)
382            },
383            vm_steps: vm.steps.try_into().unwrap_or(u32::MAX),
384            vm_init_state_hash: HashBytes::ZERO,
385            vm_final_state_hash: HashBytes::ZERO,
386        });
387
388        Ok(res)
389    }
390
391    fn prepare_vm_stack(&self, input: TransactionInput<'_>) -> SafeRc<Stack> {
392        SafeRc::new(Stack::with_items(match input {
393            TransactionInput::Ordinary(msg) => {
394                tuple![
395                    int self.balance.tokens,
396                    int msg.balance_remaining.tokens,
397                    cell msg.root.clone(),
398                    slice msg.body.clone(),
399                    int if msg.is_external { -1 } else { 0 },
400                ]
401            }
402            TransactionInput::TickTock(ty) => {
403                tuple![
404                    int self.balance.tokens,
405                    int BigInt::from_bytes_be(Sign::Plus, self.address.address.as_array()),
406                    int match ty {
407                        TickTock::Tick => 0,
408                        TickTock::Tock => -1,
409                    },
410                    int -2,
411                ]
412            }
413        }))
414    }
415}
416
417impl ReceivedMessage {
418    fn make_tuple(&self) -> Result<Option<SafeRc<Tuple>>, tycho_types::error::Error> {
419        let mut cs = self.root.as_slice()?;
420        if MsgType::load_from(&mut cs)? != MsgType::Int {
421            return Ok(None);
422        }
423
424        // Get `src` addr range.
425        let src_addr_slice = {
426            let mut cs = cs;
427            // Skip flags.
428            cs.skip_first(3, 0)?;
429            let mut addr_slice = cs;
430            // Read `src`.
431            IntAddr::load_from(&mut cs)?;
432            addr_slice.skip_last(cs.size_bits(), cs.size_refs())?;
433            addr_slice.range()
434        };
435
436        let info = IntMsgInfo::load_from(&mut cs)?;
437
438        let unpacked = UnpackedInMsgSmcInfo {
439            bounce: info.bounce,
440            bounced: info.bounced,
441            src_addr: (src_addr_slice, self.root.clone()).into(),
442            fwd_fee: info.fwd_fee,
443            created_lt: info.created_lt,
444            created_at: info.created_at,
445            original_value: info.value.tokens,
446            remaining_value: self.balance_remaining.clone(),
447            state_init: self.init.as_ref().map(|init| init.root.clone()),
448        };
449        Ok(Some(unpacked.into_tuple()))
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use std::collections::BTreeMap;
456
457    use tycho_asm_macros::tvmasm;
458    use tycho_types::models::{
459        AuthorityMarksConfig, ExtInMsgInfo, IntMsgInfo, LibDescr, SimpleLib, StdAddr,
460    };
461    use tycho_types::num::{VarUint24, VarUint56, VarUint248};
462
463    use super::*;
464    use crate::ExecutorParams;
465    use crate::tests::{
466        make_custom_config, make_default_config, make_default_params, make_message,
467    };
468
469    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
470    const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
471
472    fn empty_ext_in_msg(addr: &StdAddr) -> Cell {
473        make_message(
474            ExtInMsgInfo {
475                dst: addr.clone().into(),
476                ..Default::default()
477            },
478            None,
479            None,
480        )
481    }
482
483    fn empty_int_msg(addr: &StdAddr, value: impl Into<CurrencyCollection>) -> Cell {
484        make_message(
485            IntMsgInfo {
486                src: addr.clone().into(),
487                dst: addr.clone().into(),
488                value: value.into(),
489                ..Default::default()
490            },
491            None,
492            None,
493        )
494    }
495
496    fn simple_state(code: &[u8]) -> StateInit {
497        StateInit {
498            split_depth: None,
499            special: None,
500            code: Some(Boc::decode(code).unwrap()),
501            data: None,
502            libraries: Dict::new(),
503        }
504    }
505
506    fn init_tracing() {
507        tracing_subscriber::fmt::fmt()
508            .with_env_filter("tycho_vm=trace")
509            .with_writer(tracing_subscriber::fmt::TestWriter::new)
510            .try_init()
511            .ok();
512    }
513
514    fn make_lib_ref(code: &DynCell) -> Cell {
515        let mut b = CellBuilder::new();
516        b.set_exotic(true);
517        b.store_u8(CellType::LibraryReference.to_byte()).unwrap();
518        b.store_u256(code.repr_hash()).unwrap();
519        b.build().unwrap()
520    }
521
522    #[test]
523    fn ext_in_run_no_accept() -> Result<()> {
524        let params = make_default_params();
525        let config = make_default_config();
526        let mut state = ExecutorState::new_active(
527            &params,
528            &config,
529            &STUB_ADDR,
530            OK_BALANCE,
531            Cell::empty_cell(),
532            tvmasm!("INT 123 NOP"),
533        );
534
535        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
536
537        let prev_balance = state.balance.clone();
538        let prev_state = state.state.clone();
539        let prev_total_fees = state.total_fees;
540        let prev_end_status = state.end_status;
541
542        let compute_phase = state.compute_phase(ComputePhaseContext {
543            input: TransactionInput::Ordinary(&msg),
544            storage_fee: Tokens::ZERO,
545            force_accept: false,
546            inspector: None,
547        })?;
548
549        // Original balance must be correct.
550        assert_eq!(prev_balance, compute_phase.original_balance);
551        // Message must not be accepted.
552        assert!(!compute_phase.accepted);
553        // State must not change.
554        assert_eq!(state.state, prev_state);
555        // Status must not change.
556        assert_eq!(prev_end_status, state.end_status);
557        // No actions must be produced.
558        assert_eq!(compute_phase.actions, Cell::empty_cell());
559        // No fees must be paid when message was not accepted.
560        assert_eq!(state.total_fees, prev_total_fees);
561        assert_eq!(state.balance, prev_balance);
562
563        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
564            panic!("expected executed compute phase");
565        };
566
567        assert!(!compute_phase.success);
568        assert!(!compute_phase.msg_state_used);
569        assert!(!compute_phase.account_activated);
570        assert_eq!(compute_phase.gas_fees, Tokens::ZERO);
571        assert_eq!(compute_phase.gas_used, VarUint56::new(0)); // only credit was used
572        assert_eq!(compute_phase.gas_limit, VarUint56::new(0)); // zero, for external messages before accept
573        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
574        assert_eq!(compute_phase.exit_code, 0);
575        assert_eq!(compute_phase.exit_arg, Some(123)); // top int is treated as exit arg if !success
576        assert_eq!(compute_phase.vm_steps, 3); // pushint, nop, implicit ret
577
578        Ok(())
579    }
580
581    #[test]
582    fn ext_in_run_uninit_no_accept() -> Result<()> {
583        let params = make_default_params();
584        let config = make_default_config();
585        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
586
587        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
588
589        let prev_balance = state.balance.clone();
590        let prev_state = state.state.clone();
591        let prev_total_fees = state.total_fees;
592        let prev_end_status = state.end_status;
593
594        let compute_phase = state.compute_phase(ComputePhaseContext {
595            input: TransactionInput::Ordinary(&msg),
596            storage_fee: Tokens::ZERO,
597            force_accept: false,
598            inspector: None,
599        })?;
600
601        // Original balance must be correct.
602        assert_eq!(prev_balance, compute_phase.original_balance);
603        // Message must not be accepted.
604        assert!(!compute_phase.accepted);
605        // State must not change.
606        assert_eq!(state.state, prev_state);
607        // Status must not change.
608        assert_eq!(prev_end_status, state.end_status);
609        // No actions must be produced.
610        assert_eq!(compute_phase.actions, Cell::empty_cell());
611        // No fees must be paid when message was not accepted.
612        assert_eq!(state.total_fees, prev_total_fees);
613        assert_eq!(state.balance, prev_balance);
614
615        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
616            panic!("expected skipped compute phase");
617        };
618        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoState);
619
620        Ok(())
621    }
622
623    #[test]
624    fn ext_in_run_no_code_no_accept() -> Result<()> {
625        let params = make_default_params();
626        let config = make_default_config();
627        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
628        state.state = AccountState::Active(StateInit::default());
629        state.orig_status = AccountStatus::Active;
630        state.end_status = AccountStatus::Active;
631
632        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
633
634        let prev_balance = state.balance.clone();
635        let prev_state = state.state.clone();
636        let prev_total_fees = state.total_fees;
637        let prev_end_status = state.end_status;
638
639        let compute_phase = state.compute_phase(ComputePhaseContext {
640            input: TransactionInput::Ordinary(&msg),
641            storage_fee: Tokens::ZERO,
642            force_accept: false,
643            inspector: None,
644        })?;
645
646        // Original balance must be correct.
647        assert_eq!(prev_balance, compute_phase.original_balance);
648        // Message must not be accepted.
649        assert!(!compute_phase.accepted);
650        // State must not change.
651        assert_eq!(state.state, prev_state);
652        // Status must not change.
653        assert_eq!(prev_end_status, state.end_status);
654        // No actions must be produced.
655        assert_eq!(compute_phase.actions, Cell::empty_cell());
656        // No fees must be paid when message was not accepted.
657        assert_eq!(state.total_fees, prev_total_fees);
658        assert_eq!(state.balance, prev_balance);
659
660        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
661            panic!("expected executed compute phase");
662        };
663
664        assert!(!compute_phase.success);
665        assert!(!compute_phase.msg_state_used);
666        assert!(!compute_phase.account_activated);
667        assert_eq!(compute_phase.gas_fees, Tokens::ZERO);
668        assert_eq!(compute_phase.gas_used, VarUint56::new(0)); // only credit was used
669        assert_eq!(compute_phase.gas_limit, VarUint56::new(0)); // zero, for external messages before accept
670        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
671        assert_eq!(
672            compute_phase.exit_code,
673            tycho_vm::VmException::Fatal.as_exit_code()
674        );
675        assert_eq!(compute_phase.exit_arg, Some(-1)); // top int is treated as exit arg if !success
676        assert_eq!(compute_phase.vm_steps, 0);
677
678        Ok(())
679    }
680
681    #[test]
682    fn ext_in_accept_no_commit() -> Result<()> {
683        let params = make_default_params();
684        let config = make_default_config();
685        let mut state = ExecutorState::new_active(
686            &params,
687            &config,
688            &STUB_ADDR,
689            OK_BALANCE,
690            Cell::empty_cell(),
691            tvmasm!("ACCEPT THROW 42"),
692        );
693
694        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
695
696        let prev_balance = state.balance.clone();
697        let prev_state = state.state.clone();
698        let prev_total_fees = state.total_fees;
699        let prev_end_status = state.end_status;
700
701        let compute_phase = state.compute_phase(ComputePhaseContext {
702            input: TransactionInput::Ordinary(&msg),
703            storage_fee: Tokens::ZERO,
704            force_accept: false,
705            inspector: None,
706        })?;
707
708        // Original balance must be correct.
709        assert_eq!(prev_balance, compute_phase.original_balance);
710        // Message must be accepted.
711        assert!(compute_phase.accepted);
712        // State must not change.
713        assert_eq!(state.state, prev_state);
714        // Status must not change.
715        assert_eq!(prev_end_status, state.end_status);
716        // No actions must be produced.
717        assert_eq!(compute_phase.actions, Cell::empty_cell());
718        // Fees must be paid.
719        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
720        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
721        assert_eq!(state.balance.other, prev_balance.other);
722        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
723
724        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
725            panic!("expected executed compute phase");
726        };
727
728        assert!(!compute_phase.success);
729        assert!(!compute_phase.msg_state_used);
730        assert!(!compute_phase.account_activated);
731        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
732        assert_eq!(compute_phase.gas_used, (10 + 16) * 2 + 50); // two 16bit opcodes + exception
733        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
734        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
735        assert_eq!(compute_phase.exit_code, 42);
736        assert_eq!(compute_phase.exit_arg, None);
737        assert_eq!(compute_phase.vm_steps, 2); // accept, throw
738
739        Ok(())
740    }
741
742    #[test]
743    fn ext_in_accept_invalid_commit() -> Result<()> {
744        init_tracing();
745        let params = make_default_params();
746        let config = make_default_config();
747
748        let mut state = ExecutorState::new_active(
749            &params,
750            &config,
751            &STUB_ADDR,
752            OK_BALANCE,
753            Cell::empty_cell(),
754            tvmasm!(
755                r#"
756                ACCEPT
757                NEWC
758                INT 1 STUR 8
759                INT 7 STUR 8
760                INT 816 STZEROES
761                TRUE ENDXC
762                POP c5
763                "#
764            ),
765        );
766
767        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
768
769        let prev_balance = state.balance.clone();
770        let prev_state = state.state.clone();
771        let prev_total_fees = state.total_fees;
772        let prev_end_status = state.end_status;
773
774        let compute_phase = state.compute_phase(ComputePhaseContext {
775            input: TransactionInput::Ordinary(&msg),
776            storage_fee: Tokens::ZERO,
777            force_accept: false,
778            inspector: None,
779        })?;
780
781        // Original balance must be correct.
782        assert_eq!(prev_balance, compute_phase.original_balance);
783        // Message must be accepted.
784        assert!(compute_phase.accepted);
785        // State must not change.
786        assert_eq!(state.state, prev_state);
787        // Status must not change.
788        assert_eq!(prev_end_status, state.end_status);
789        // No (VALID) actions must be produced.
790        assert_eq!(compute_phase.actions, Cell::empty_cell());
791        // Fees must be paid.
792        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
793        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
794        assert_eq!(state.balance.other, prev_balance.other);
795        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
796
797        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
798            panic!("expected executed compute phase");
799        };
800
801        assert!(!compute_phase.success);
802        assert!(!compute_phase.msg_state_used);
803        assert!(!compute_phase.account_activated);
804        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
805        assert_eq!(compute_phase.gas_used, 783);
806        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
807        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
808        assert_eq!(
809            compute_phase.exit_code,
810            tycho_vm::VmException::CellOverflow as i32
811        );
812        assert_eq!(compute_phase.exit_arg, None);
813        assert_eq!(compute_phase.vm_steps, 12); // accept, throw
814
815        Ok(())
816    }
817
818    #[test]
819    fn suspended_account_skips_compute() -> Result<()> {
820        let params = ExecutorParams {
821            authority_marks_enabled: true,
822            ..make_default_params()
823        };
824
825        let config = make_custom_config(|config| {
826            config.set_authority_marks_config(&AuthorityMarksConfig {
827                authority_addresses: Dict::new(),
828                black_mark_id: 100,
829                white_mark_id: 101,
830            })?;
831            Ok(())
832        });
833
834        let mut state = ExecutorState::new_active(
835            &params,
836            &config,
837            &STUB_ADDR,
838            CurrencyCollection {
839                tokens: OK_BALANCE,
840                other: BTreeMap::from_iter([
841                    (100u32, VarUint248::new(500)), // blocked by black marks
842                ])
843                .try_into()?,
844            },
845            CellBuilder::build_from(u32::MIN)?,
846            tvmasm!("ACCEPT"),
847        );
848
849        let msg = state.receive_in_msg(empty_int_msg(&STUB_ADDR, OK_BALANCE))?;
850        state.credit_phase(&msg)?;
851
852        let prev_balance = state.balance.clone();
853        let prev_state = state.state.clone();
854        let prev_total_fees = state.total_fees;
855        let prev_end_status = state.end_status;
856
857        let compute_phase = state.compute_phase(ComputePhaseContext {
858            input: TransactionInput::Ordinary(&msg),
859            storage_fee: Tokens::ZERO,
860            force_accept: false,
861            inspector: None,
862        })?;
863
864        // Message must not be accepted.
865        assert!(!compute_phase.accepted);
866        // State must not change.
867        assert_eq!(state.state, prev_state);
868        // Status must not change.
869        assert_eq!(prev_end_status, state.end_status);
870        // No actions must be produced.
871        assert_eq!(compute_phase.actions, Cell::empty_cell());
872        // No fees must be paid when message was not accepted.
873        assert_eq!(state.total_fees, prev_total_fees);
874        assert_eq!(state.balance, prev_balance);
875
876        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
877            panic!("expected skipped compute phase");
878        };
879        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::Suspended);
880
881        Ok(())
882    }
883
884    #[test]
885    fn cant_suspend_authority_address() -> Result<()> {
886        let params = ExecutorParams {
887            authority_marks_enabled: true,
888            ..make_default_params()
889        };
890        let config = make_custom_config(|config| {
891            config.set_authority_marks_config(&AuthorityMarksConfig {
892                authority_addresses: Dict::try_from_btree(&BTreeMap::from_iter([(
893                    HashBytes::ZERO,
894                    (),
895                )]))?,
896                black_mark_id: 100,
897                white_mark_id: 101,
898            })?;
899            Ok(())
900        });
901
902        let mut state = ExecutorState::new_active(
903            &params,
904            &config,
905            &StdAddr::new(-1, HashBytes::ZERO),
906            CurrencyCollection {
907                tokens: OK_BALANCE,
908                other: BTreeMap::from_iter([
909                    (100u32, VarUint248::new(1000)), // more black barks than white
910                    (101u32, VarUint248::new(100)),
911                ])
912                .try_into()?,
913            },
914            Cell::default(),
915            tvmasm!("ACCEPT"),
916        );
917
918        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
919
920        let prev_balance = state.balance.clone();
921        let prev_state = state.state.clone();
922        let prev_total_fees = state.total_fees;
923        let prev_end_status = state.end_status;
924
925        let compute_phase = state.compute_phase(ComputePhaseContext {
926            input: TransactionInput::Ordinary(&msg),
927            storage_fee: Tokens::ZERO,
928            force_accept: false,
929            inspector: None,
930        })?;
931
932        // Original balance must be correct.
933        assert_eq!(prev_balance, compute_phase.original_balance);
934        // Message must be accepted.
935        assert!(compute_phase.accepted);
936        // State must not change.
937        assert_eq!(state.state, prev_state);
938        // Status must not change.
939        assert_eq!(prev_end_status, state.end_status);
940        // No actions must be produced.
941        assert_eq!(compute_phase.actions, Cell::empty_cell());
942        // Fees must be paid.
943        let expected_gas_fee = Tokens::new(config.mc_gas_prices.flat_gas_price as _);
944        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
945        assert_eq!(state.balance.other, prev_balance.other);
946        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
947
948        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
949            panic!("expected executed compute phase");
950        };
951
952        assert!(compute_phase.success);
953        assert!(!compute_phase.msg_state_used);
954        assert!(!compute_phase.account_activated);
955        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
956        assert_eq!(compute_phase.gas_used, 31);
957        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
958        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
959        assert_eq!(compute_phase.exit_code, 0);
960        assert_eq!(compute_phase.exit_arg, None);
961        assert_eq!(compute_phase.vm_steps, 2);
962
963        Ok(())
964    }
965
966    #[test]
967    fn ext_in_accept_simple() -> Result<()> {
968        let params = make_default_params();
969        let config = make_default_config();
970        let mut state = ExecutorState::new_active(
971            &params,
972            &config,
973            &STUB_ADDR,
974            OK_BALANCE,
975            Cell::empty_cell(),
976            tvmasm!("ACCEPT NEWC INT 0xdeafbeaf STUR 32 ENDC POP c5"),
977        );
978
979        let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
980
981        let prev_balance = state.balance.clone();
982        let prev_state = state.state.clone();
983        let prev_total_fees = state.total_fees;
984        let prev_end_status = state.end_status;
985
986        let compute_phase = state.compute_phase(ComputePhaseContext {
987            input: TransactionInput::Ordinary(&msg),
988            storage_fee: Tokens::ZERO,
989            force_accept: false,
990            inspector: None,
991        })?;
992
993        // Original balance must be correct.
994        assert_eq!(prev_balance, compute_phase.original_balance);
995        // Message must be accepted.
996        assert!(compute_phase.accepted);
997        // State must not change.
998        assert_eq!(state.state, prev_state);
999        // Status must not change.
1000        assert_eq!(prev_end_status, state.end_status);
1001        // No actions must be produced.
1002        assert_eq!(
1003            compute_phase.actions,
1004            CellBuilder::build_from(0xdeafbeafu32)?
1005        );
1006        // Fees must be paid.
1007        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1008        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1009        assert_eq!(state.balance.other, prev_balance.other);
1010        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1011
1012        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1013            panic!("expected executed compute phase");
1014        };
1015
1016        assert!(compute_phase.success);
1017        assert!(!compute_phase.msg_state_used);
1018        assert!(!compute_phase.account_activated);
1019        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1020        assert_eq!(compute_phase.gas_used, 650);
1021        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
1022        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
1023        assert_eq!(compute_phase.exit_code, 0);
1024        assert_eq!(compute_phase.exit_arg, None);
1025        assert_eq!(compute_phase.vm_steps, 7);
1026
1027        Ok(())
1028    }
1029
1030    #[test]
1031    fn internal_accept_simple() -> Result<()> {
1032        let params = make_default_params();
1033        let config = make_default_config();
1034        let mut state = ExecutorState::new_active(
1035            &params,
1036            &config,
1037            &STUB_ADDR,
1038            OK_BALANCE,
1039            Cell::empty_cell(),
1040            tvmasm!("ACCEPT"),
1041        );
1042
1043        let msg =
1044            state.receive_in_msg(empty_int_msg(&state.address, Tokens::new(1_000_000_000)))?;
1045
1046        state.credit_phase(&msg)?;
1047
1048        let prev_balance = state.balance.clone();
1049        let prev_state = state.state.clone();
1050        let prev_total_fees = state.total_fees;
1051        let prev_end_status = state.end_status;
1052
1053        let compute_phase = state.compute_phase(ComputePhaseContext {
1054            input: TransactionInput::Ordinary(&msg),
1055            storage_fee: Tokens::ZERO,
1056            force_accept: false,
1057            inspector: None,
1058        })?;
1059
1060        // Original balance must be correct.
1061        assert_eq!(
1062            compute_phase.original_balance,
1063            CurrencyCollection::from(OK_BALANCE)
1064        );
1065        // Message must be accepted.
1066        assert!(compute_phase.accepted);
1067        // State must not change.
1068        assert_eq!(state.state, prev_state);
1069        // Status must not change.
1070        assert_eq!(prev_end_status, state.end_status);
1071        // No actions must be produced.
1072        assert_eq!(compute_phase.actions, Cell::empty_cell());
1073        // Fees must be paid.
1074        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1075        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1076        assert_eq!(state.balance.other, prev_balance.other);
1077        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1078
1079        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1080            panic!("expected executed compute phase");
1081        };
1082
1083        assert!(compute_phase.success);
1084        assert!(!compute_phase.msg_state_used);
1085        assert!(!compute_phase.account_activated);
1086        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1087        assert_eq!(compute_phase.gas_used, (10 + 16) + 5); // two 16bit opcodes + implicit ret
1088        assert_eq!(
1089            compute_phase.gas_limit,
1090            VarUint56::new(config.gas_prices.gas_limit)
1091        );
1092        assert_eq!(compute_phase.gas_credit, None);
1093        assert_eq!(compute_phase.exit_code, 0);
1094        assert_eq!(compute_phase.exit_arg, None);
1095        assert_eq!(compute_phase.vm_steps, 2); // accept, implicit ret
1096
1097        Ok(())
1098    }
1099
1100    #[test]
1101    fn internal_no_accept() -> Result<()> {
1102        let params = make_default_params();
1103        let config = make_default_config();
1104        let mut state = ExecutorState::new_active(
1105            &params,
1106            &config,
1107            &STUB_ADDR,
1108            OK_BALANCE,
1109            Cell::empty_cell(),
1110            tvmasm!("NOP"),
1111        );
1112
1113        let msg = state.receive_in_msg(empty_int_msg(&state.address, Tokens::new(1)))?;
1114
1115        state.credit_phase(&msg)?;
1116
1117        let prev_balance = state.balance.clone();
1118        let prev_state = state.state.clone();
1119        let prev_total_fees = state.total_fees;
1120        let prev_end_status = state.end_status;
1121
1122        let compute_phase = state.compute_phase(ComputePhaseContext {
1123            input: TransactionInput::Ordinary(&msg),
1124            storage_fee: Tokens::ZERO,
1125            force_accept: false,
1126            inspector: None,
1127        })?;
1128
1129        // Original balance must be correct.
1130        assert_eq!(
1131            compute_phase.original_balance,
1132            CurrencyCollection::from(OK_BALANCE)
1133        );
1134        // Message must not be accepted.
1135        assert!(!compute_phase.accepted);
1136        // State must not change.
1137        assert_eq!(state.state, prev_state);
1138        // Status must not change.
1139        assert_eq!(prev_end_status, state.end_status);
1140        // No actions must be produced.
1141        assert_eq!(compute_phase.actions, Cell::empty_cell());
1142        // No fees must be paid when message was not accepted.
1143        assert_eq!(state.total_fees, prev_total_fees);
1144        assert_eq!(state.balance, prev_balance);
1145
1146        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1147            panic!("expected skipped compute phase");
1148        };
1149        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoGas);
1150
1151        Ok(())
1152    }
1153
1154    #[test]
1155    fn internal_no_accept_empty_balance() -> Result<()> {
1156        let params = make_default_params();
1157        let config = make_default_config();
1158        let mut state = ExecutorState::new_active(
1159            &params,
1160            &config,
1161            &STUB_ADDR,
1162            Tokens::ZERO,
1163            Cell::empty_cell(),
1164            tvmasm!("NOP"),
1165        );
1166
1167        let msg = state.receive_in_msg(empty_int_msg(&state.address, Tokens::ZERO))?;
1168
1169        state.credit_phase(&msg)?;
1170
1171        let prev_balance = state.balance.clone();
1172        let prev_state = state.state.clone();
1173        let prev_total_fees = state.total_fees;
1174        let prev_end_status = state.end_status;
1175
1176        let compute_phase = state.compute_phase(ComputePhaseContext {
1177            input: TransactionInput::Ordinary(&msg),
1178            storage_fee: Tokens::ZERO,
1179            force_accept: false,
1180            inspector: None,
1181        })?;
1182
1183        // Original balance must be correct.
1184        assert_eq!(compute_phase.original_balance, CurrencyCollection::ZERO);
1185        // Message must not be accepted.
1186        assert!(!compute_phase.accepted);
1187        // State must not change.
1188        assert_eq!(state.state, prev_state);
1189        // Status must not change.
1190        assert_eq!(prev_end_status, state.end_status);
1191        // No actions must be produced.
1192        assert_eq!(compute_phase.actions, Cell::empty_cell());
1193        // No fees must be paid when message was not accepted.
1194        assert_eq!(state.total_fees, prev_total_fees);
1195        assert_eq!(state.balance, prev_balance);
1196
1197        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1198            panic!("expected skipped compute phase");
1199        };
1200        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoGas);
1201
1202        Ok(())
1203    }
1204
1205    #[test]
1206    fn ext_in_deploy_account() -> Result<()> {
1207        let state_init = simple_state(tvmasm!("ACCEPT"));
1208        let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
1209        let addr = StdAddr::new(0, state_init_hash);
1210
1211        let params = make_default_params();
1212        let config = make_default_config();
1213        let mut state = ExecutorState::new_uninit(&params, &config, &addr, OK_BALANCE);
1214
1215        let msg = state.receive_in_msg(make_message(
1216            ExtInMsgInfo {
1217                dst: addr.clone().into(),
1218                ..Default::default()
1219            },
1220            Some(state_init.clone()),
1221            None,
1222        ))?;
1223
1224        let prev_balance = state.balance.clone();
1225        let prev_total_fees = state.total_fees;
1226
1227        let compute_phase = state.compute_phase(ComputePhaseContext {
1228            input: TransactionInput::Ordinary(&msg),
1229            storage_fee: Tokens::ZERO,
1230            force_accept: false,
1231            inspector: None,
1232        })?;
1233
1234        // Original balance must be correct.
1235        assert_eq!(
1236            compute_phase.original_balance,
1237            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1238        );
1239        // Message must be accepted.
1240        assert!(compute_phase.accepted);
1241        // State must change.
1242        assert_eq!(state.state, AccountState::Active(state_init));
1243        // Status must change.
1244        assert_eq!(state.end_status, AccountStatus::Active);
1245        // No actions must be produced.
1246        assert_eq!(compute_phase.actions, Cell::empty_cell());
1247        // Fees must be paid.
1248        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1249        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1250        assert_eq!(state.balance.other, prev_balance.other);
1251        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1252
1253        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1254            panic!("expected executed compute phase");
1255        };
1256
1257        assert!(compute_phase.success);
1258        assert!(compute_phase.msg_state_used);
1259        assert!(compute_phase.account_activated);
1260        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1261        assert_eq!(compute_phase.gas_used, (10 + 16) + 5); // two 16bit opcodes + implicit ret
1262        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
1263        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
1264        assert_eq!(compute_phase.exit_code, 0);
1265        assert_eq!(compute_phase.exit_arg, None);
1266        assert_eq!(compute_phase.vm_steps, 2); // accept, implicit ret
1267
1268        Ok(())
1269    }
1270
1271    #[test]
1272    fn internal_deploy_account() -> Result<()> {
1273        let state_init = simple_state(tvmasm!("ACCEPT"));
1274        let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
1275        let addr = StdAddr::new(0, state_init_hash);
1276
1277        let params = make_default_params();
1278        let config = make_default_config();
1279        let mut state = ExecutorState::new_uninit(&params, &config, &addr, OK_BALANCE);
1280
1281        let msg = state.receive_in_msg(make_message(
1282            IntMsgInfo {
1283                src: addr.clone().into(),
1284                dst: addr.clone().into(),
1285                value: Tokens::new(1_000_000_000).into(),
1286                ..Default::default()
1287            },
1288            Some(state_init.clone()),
1289            None,
1290        ))?;
1291
1292        state.credit_phase(&msg)?;
1293
1294        let prev_balance = state.balance.clone();
1295        let prev_total_fees = state.total_fees;
1296
1297        let compute_phase = state.compute_phase(ComputePhaseContext {
1298            input: TransactionInput::Ordinary(&msg),
1299            storage_fee: Tokens::ZERO,
1300            force_accept: false,
1301            inspector: None,
1302        })?;
1303
1304        // Original balance must be correct.
1305        assert_eq!(
1306            compute_phase.original_balance,
1307            CurrencyCollection::from(OK_BALANCE)
1308        );
1309        // Message must be accepted.
1310        assert!(compute_phase.accepted);
1311        // State must change.
1312        assert_eq!(state.state, AccountState::Active(state_init));
1313        // Status must change.
1314        assert_eq!(state.end_status, AccountStatus::Active);
1315        // No actions must be produced.
1316        assert_eq!(compute_phase.actions, Cell::empty_cell());
1317        // Fees must be paid.
1318        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1319        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1320        assert_eq!(state.balance.other, prev_balance.other);
1321        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1322
1323        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1324            panic!("expected executed compute phase");
1325        };
1326
1327        assert!(compute_phase.success);
1328        assert!(compute_phase.msg_state_used);
1329        assert!(compute_phase.account_activated);
1330        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1331        assert_eq!(compute_phase.gas_used, (10 + 16) + 5); // two 16bit opcodes + implicit ret
1332        assert_eq!(
1333            compute_phase.gas_limit,
1334            VarUint56::new(config.gas_prices.gas_limit)
1335        );
1336        assert_eq!(compute_phase.gas_credit, None);
1337        assert_eq!(compute_phase.exit_code, 0);
1338        assert_eq!(compute_phase.exit_arg, None);
1339        assert_eq!(compute_phase.vm_steps, 2); // accept, implicit ret
1340
1341        Ok(())
1342    }
1343
1344    #[test]
1345    fn ext_in_unfreeze_account() -> Result<()> {
1346        let state_init = simple_state(tvmasm!("ACCEPT"));
1347        let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
1348
1349        let params = make_default_params();
1350        let config = make_default_config();
1351        let mut state =
1352            ExecutorState::new_frozen(&params, &config, &STUB_ADDR, OK_BALANCE, state_init_hash);
1353
1354        let msg = state.receive_in_msg(make_message(
1355            ExtInMsgInfo {
1356                dst: STUB_ADDR.into(),
1357                ..Default::default()
1358            },
1359            Some(state_init.clone()),
1360            None,
1361        ))?;
1362
1363        let prev_balance = state.balance.clone();
1364        let prev_total_fees = state.total_fees;
1365
1366        let compute_phase = state.compute_phase(ComputePhaseContext {
1367            input: TransactionInput::Ordinary(&msg),
1368            storage_fee: Tokens::ZERO,
1369            force_accept: false,
1370            inspector: None,
1371        })?;
1372
1373        // Original balance must be correct.
1374        assert_eq!(
1375            compute_phase.original_balance,
1376            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1377        );
1378        // Message must be accepted.
1379        assert!(compute_phase.accepted);
1380        // State must change.
1381        assert_eq!(state.state, AccountState::Active(state_init));
1382        // Status must change.
1383        assert_eq!(state.end_status, AccountStatus::Active);
1384        // No actions must be produced.
1385        assert_eq!(compute_phase.actions, Cell::empty_cell());
1386        // Fees must be paid.
1387        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1388        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1389        assert_eq!(state.balance.other, prev_balance.other);
1390        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1391
1392        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1393            panic!("expected executed compute phase");
1394        };
1395
1396        assert!(compute_phase.success);
1397        assert!(compute_phase.msg_state_used);
1398        assert!(compute_phase.account_activated);
1399        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1400        assert_eq!(compute_phase.gas_used, (10 + 16) + 5); // two 16bit opcodes + implicit ret
1401        assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
1402        assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
1403        assert_eq!(compute_phase.exit_code, 0);
1404        assert_eq!(compute_phase.exit_arg, None);
1405        assert_eq!(compute_phase.vm_steps, 2); // accept, implicit ret
1406
1407        Ok(())
1408    }
1409
1410    #[test]
1411    fn internal_unfreeze_account() -> Result<()> {
1412        let state_init = simple_state(tvmasm!("ACCEPT"));
1413        let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
1414
1415        let params = make_default_params();
1416        let config = make_default_config();
1417        let mut state =
1418            ExecutorState::new_frozen(&params, &config, &STUB_ADDR, Tokens::ZERO, state_init_hash);
1419
1420        let msg = state.receive_in_msg(make_message(
1421            IntMsgInfo {
1422                src: STUB_ADDR.into(),
1423                dst: STUB_ADDR.into(),
1424                value: Tokens::new(1_000_000_000).into(),
1425                ..Default::default()
1426            },
1427            Some(state_init.clone()),
1428            None,
1429        ))?;
1430
1431        state.credit_phase(&msg)?;
1432
1433        let prev_balance = state.balance.clone();
1434        let prev_total_fees = state.total_fees;
1435
1436        let compute_phase = state.compute_phase(ComputePhaseContext {
1437            input: TransactionInput::Ordinary(&msg),
1438            storage_fee: Tokens::ZERO,
1439            force_accept: false,
1440            inspector: None,
1441        })?;
1442
1443        // Original balance must be correct.
1444        assert_eq!(compute_phase.original_balance, CurrencyCollection::ZERO);
1445        // Message must be accepted.
1446        assert!(compute_phase.accepted);
1447        // State must change.
1448        assert_eq!(state.state, AccountState::Active(state_init));
1449        // Status must change to active.
1450        assert_eq!(state.end_status, AccountStatus::Active);
1451        // No actions must be produced.
1452        assert_eq!(compute_phase.actions, Cell::empty_cell());
1453        // Fees must be paid.
1454        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1455        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1456        assert_eq!(state.balance.other, prev_balance.other);
1457        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1458
1459        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1460            panic!("expected executed compute phase");
1461        };
1462
1463        assert!(compute_phase.success);
1464        assert!(compute_phase.msg_state_used);
1465        assert!(compute_phase.account_activated);
1466        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1467        assert_eq!(compute_phase.gas_used, (10 + 16) + 5); // two 16bit opcodes + implicit ret
1468        assert_eq!(
1469            compute_phase.gas_limit,
1470            VarUint56::new(config.gas_prices.gas_limit)
1471        );
1472        assert_eq!(compute_phase.gas_credit, None);
1473        assert_eq!(compute_phase.exit_code, 0);
1474        assert_eq!(compute_phase.exit_arg, None);
1475        assert_eq!(compute_phase.vm_steps, 2); // accept, implicit ret
1476
1477        Ok(())
1478    }
1479
1480    #[test]
1481    fn ext_in_unfreeze_hash_mismatch() -> Result<()> {
1482        let state_init = simple_state(tvmasm!("ACCEPT"));
1483
1484        let params = make_default_params();
1485        let config = make_default_config();
1486        let mut state =
1487            ExecutorState::new_frozen(&params, &config, &STUB_ADDR, OK_BALANCE, HashBytes::ZERO);
1488
1489        let msg = state.receive_in_msg(make_message(
1490            ExtInMsgInfo {
1491                dst: STUB_ADDR.into(),
1492                ..Default::default()
1493            },
1494            Some(state_init.clone()),
1495            None,
1496        ))?;
1497
1498        let prev_state = state.state.clone();
1499        let prev_balance = state.balance.clone();
1500        let prev_total_fees = state.total_fees;
1501
1502        let compute_phase = state.compute_phase(ComputePhaseContext {
1503            input: TransactionInput::Ordinary(&msg),
1504            storage_fee: Tokens::ZERO,
1505            force_accept: false,
1506            inspector: None,
1507        })?;
1508
1509        // Original balance must be correct.
1510        assert_eq!(
1511            compute_phase.original_balance,
1512            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1513        );
1514        // Message must not be accepted.
1515        assert!(!compute_phase.accepted);
1516        // State must not change.
1517        assert_eq!(state.state, prev_state);
1518        // Status must not change.
1519        assert_eq!(state.end_status, AccountStatus::Frozen);
1520        // No actions must be produced.
1521        assert_eq!(compute_phase.actions, Cell::empty_cell());
1522        // Fees must not be paid.
1523        assert_eq!(state.total_fees, prev_total_fees);
1524        assert_eq!(state.balance, prev_balance);
1525
1526        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1527            panic!("expected skipped compute phase");
1528        };
1529        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
1530
1531        Ok(())
1532    }
1533
1534    #[test]
1535    fn ext_in_deploy_hash_mismatch() -> Result<()> {
1536        let state_init = simple_state(tvmasm!("ACCEPT"));
1537
1538        let params = make_default_params();
1539        let config = make_default_config();
1540        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1541
1542        let msg = state.receive_in_msg(make_message(
1543            ExtInMsgInfo {
1544                dst: STUB_ADDR.into(),
1545                ..Default::default()
1546            },
1547            Some(state_init.clone()),
1548            None,
1549        ))?;
1550
1551        let prev_state = state.state.clone();
1552        let prev_balance = state.balance.clone();
1553        let prev_total_fees = state.total_fees;
1554
1555        let compute_phase = state.compute_phase(ComputePhaseContext {
1556            input: TransactionInput::Ordinary(&msg),
1557            storage_fee: Tokens::ZERO,
1558            force_accept: false,
1559            inspector: None,
1560        })?;
1561
1562        // Original balance must be correct.
1563        assert_eq!(
1564            compute_phase.original_balance,
1565            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1566        );
1567        // Message must not be accepted.
1568        assert!(!compute_phase.accepted);
1569        // State must not change.
1570        assert_eq!(state.state, prev_state);
1571        // Status must not change.
1572        assert_eq!(state.end_status, AccountStatus::Uninit);
1573        // No actions must be produced.
1574        assert_eq!(compute_phase.actions, Cell::empty_cell());
1575        // Fees must not be paid.
1576        assert_eq!(state.total_fees, prev_total_fees);
1577        assert_eq!(state.balance, prev_balance);
1578
1579        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1580            panic!("expected skipped compute phase");
1581        };
1582        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
1583
1584        Ok(())
1585    }
1586
1587    #[test]
1588    fn internal_unfreeze_hash_mismatch() -> Result<()> {
1589        let state_init = simple_state(tvmasm!("ACCEPT"));
1590
1591        let params = make_default_params();
1592        let config = make_default_config();
1593        let mut state =
1594            ExecutorState::new_frozen(&params, &config, &STUB_ADDR, OK_BALANCE, HashBytes::ZERO);
1595
1596        let msg = state.receive_in_msg(make_message(
1597            IntMsgInfo {
1598                src: STUB_ADDR.into(),
1599                dst: STUB_ADDR.into(),
1600                value: Tokens::new(1_000_000_000).into(),
1601                ..Default::default()
1602            },
1603            Some(state_init.clone()),
1604            None,
1605        ))?;
1606
1607        state.credit_phase(&msg)?;
1608
1609        let prev_state = state.state.clone();
1610        let prev_balance = state.balance.clone();
1611        let prev_total_fees = state.total_fees;
1612
1613        let compute_phase = state.compute_phase(ComputePhaseContext {
1614            input: TransactionInput::Ordinary(&msg),
1615            storage_fee: Tokens::ZERO,
1616            force_accept: false,
1617            inspector: None,
1618        })?;
1619
1620        // Original balance must be correct.
1621        assert_eq!(
1622            compute_phase.original_balance,
1623            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1624        );
1625        // Message must not be accepted.
1626        assert!(!compute_phase.accepted);
1627        // State must not change.
1628        assert_eq!(state.state, prev_state);
1629        // Status must not change.
1630        assert_eq!(state.end_status, AccountStatus::Frozen);
1631        // No actions must be produced.
1632        assert_eq!(compute_phase.actions, Cell::empty_cell());
1633        // Fees must not be paid.
1634        assert_eq!(state.total_fees, prev_total_fees);
1635        assert_eq!(state.balance, prev_balance);
1636
1637        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1638            panic!("expected skipped compute phase");
1639        };
1640        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
1641
1642        Ok(())
1643    }
1644
1645    #[test]
1646    fn internal_deploy_hash_mismatch() -> Result<()> {
1647        let state_init = simple_state(tvmasm!("ACCEPT"));
1648
1649        let params = make_default_params();
1650        let config = make_default_config();
1651        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1652
1653        let msg = state.receive_in_msg(make_message(
1654            IntMsgInfo {
1655                src: STUB_ADDR.into(),
1656                dst: STUB_ADDR.into(),
1657                value: Tokens::new(1_000_000_000).into(),
1658                ..Default::default()
1659            },
1660            Some(state_init.clone()),
1661            None,
1662        ))?;
1663
1664        state.credit_phase(&msg)?;
1665
1666        let prev_state = state.state.clone();
1667        let prev_balance = state.balance.clone();
1668        let prev_total_fees = state.total_fees;
1669
1670        let compute_phase = state.compute_phase(ComputePhaseContext {
1671            input: TransactionInput::Ordinary(&msg),
1672            storage_fee: Tokens::ZERO,
1673            force_accept: false,
1674            inspector: None,
1675        })?;
1676
1677        // Original balance must be correct.
1678        assert_eq!(
1679            compute_phase.original_balance,
1680            CurrencyCollection::from(OK_BALANCE - prev_total_fees)
1681        );
1682        // Message must not be accepted.
1683        assert!(!compute_phase.accepted);
1684        // State must not change.
1685        assert_eq!(state.state, prev_state);
1686        // Status must not change.
1687        assert_eq!(state.end_status, AccountStatus::Uninit);
1688        // No actions must be produced.
1689        assert_eq!(compute_phase.actions, Cell::empty_cell());
1690        // Fees must not be paid.
1691        assert_eq!(state.total_fees, prev_total_fees);
1692        assert_eq!(state.balance, prev_balance);
1693
1694        let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
1695            panic!("expected skipped compute phase");
1696        };
1697        assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
1698
1699        Ok(())
1700    }
1701
1702    #[test]
1703    fn tick_special() -> Result<()> {
1704        init_tracing();
1705        let params = make_default_params();
1706        let config = make_default_config();
1707        let mut state = ExecutorState::new_active(
1708            &params,
1709            &config,
1710            &STUB_ADDR,
1711            OK_BALANCE,
1712            Cell::empty_cell(),
1713            tvmasm!("DROP THROWIF 42 ACCEPT"),
1714        );
1715
1716        let prev_balance = state.balance.clone();
1717        let prev_state = state.state.clone();
1718        let prev_total_fees = state.total_fees;
1719        let prev_end_status = state.end_status;
1720
1721        let compute_phase = state.compute_phase(ComputePhaseContext {
1722            input: TransactionInput::TickTock(TickTock::Tick),
1723            storage_fee: Tokens::ZERO,
1724            force_accept: false,
1725            inspector: None,
1726        })?;
1727
1728        // Original balance must be correct.
1729        assert_eq!(
1730            compute_phase.original_balance,
1731            CurrencyCollection::from(OK_BALANCE)
1732        );
1733        // Message must be accepted.
1734        assert!(compute_phase.accepted);
1735        // State must not change.
1736        assert_eq!(state.state, prev_state);
1737        // Status must not change.
1738        assert_eq!(prev_end_status, state.end_status);
1739        // No actions must be produced.
1740        assert_eq!(compute_phase.actions, Cell::empty_cell());
1741        // Fees must be paid.
1742        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1743        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1744        assert_eq!(state.balance.other, prev_balance.other);
1745        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1746
1747        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1748            panic!("expected executed compute phase");
1749        };
1750
1751        assert!(compute_phase.success);
1752        assert!(!compute_phase.msg_state_used);
1753        assert!(!compute_phase.account_activated);
1754        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1755        assert_eq!(compute_phase.gas_used, 75);
1756        assert_eq!(
1757            compute_phase.gas_limit,
1758            VarUint56::new(config.gas_prices.gas_limit)
1759        );
1760        assert_eq!(compute_phase.gas_credit, None);
1761        assert_eq!(compute_phase.exit_code, 0);
1762        assert_eq!(compute_phase.exit_arg, None);
1763        assert_eq!(compute_phase.vm_steps, 4);
1764
1765        Ok(())
1766    }
1767
1768    #[test]
1769    fn code_as_library() -> Result<()> {
1770        init_tracing();
1771        let mut params = make_default_params();
1772        let config = make_default_config();
1773
1774        let code = Boc::decode(tvmasm!("DROP THROWIF 42 ACCEPT"))?;
1775        params.libraries.set(code.repr_hash(), LibDescr {
1776            lib: code.clone(),
1777            publishers: {
1778                let mut p = Dict::new();
1779                p.set(HashBytes::ZERO, ())?;
1780                p
1781            },
1782        })?;
1783
1784        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1785        state.state = AccountState::Active(StateInit {
1786            code: Some(make_lib_ref(code.as_ref())),
1787            ..Default::default()
1788        });
1789        state.orig_status = AccountStatus::Active;
1790        state.end_status = AccountStatus::Active;
1791
1792        let prev_balance = state.balance.clone();
1793        let prev_state = state.state.clone();
1794        let prev_total_fees = state.total_fees;
1795        let prev_end_status = state.end_status;
1796
1797        let compute_phase = state.compute_phase(ComputePhaseContext {
1798            input: TransactionInput::TickTock(TickTock::Tick),
1799            storage_fee: Tokens::ZERO,
1800            force_accept: false,
1801            inspector: None,
1802        })?;
1803
1804        // Original balance must be correct.
1805        assert_eq!(
1806            compute_phase.original_balance,
1807            CurrencyCollection::from(OK_BALANCE)
1808        );
1809        // Message must be accepted.
1810        assert!(compute_phase.accepted);
1811        // State must not change.
1812        assert_eq!(state.state, prev_state);
1813        // Status must not change.
1814        assert_eq!(prev_end_status, state.end_status);
1815        // No actions must be produced.
1816        assert_eq!(compute_phase.actions, Cell::empty_cell());
1817        // Fees must be paid.
1818        let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
1819        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1820        assert_eq!(state.balance.other, prev_balance.other);
1821        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1822
1823        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1824            panic!("expected executed compute phase");
1825        };
1826
1827        assert!(compute_phase.success);
1828        assert!(!compute_phase.msg_state_used);
1829        assert!(!compute_phase.account_activated);
1830        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1831        assert_eq!(compute_phase.gas_used, 285);
1832        assert_eq!(
1833            compute_phase.gas_limit,
1834            VarUint56::new(config.gas_prices.gas_limit)
1835        );
1836        assert_eq!(compute_phase.gas_credit, None);
1837        assert_eq!(compute_phase.exit_code, 0);
1838        assert_eq!(compute_phase.exit_arg, None);
1839        assert_eq!(compute_phase.vm_steps, 5);
1840
1841        Ok(())
1842    }
1843
1844    #[test]
1845    fn internal_use_libraries() -> Result<()> {
1846        init_tracing();
1847        let mut params = make_default_params();
1848        let config = make_default_config();
1849
1850        let state_lib_code = Boc::decode(tvmasm!("THROWIF 42 INT 0"))?;
1851        let account_lib_code = Boc::decode(tvmasm!("THROWIF 43 INT -1"))?;
1852        let msg_lib_code = Boc::decode(tvmasm!("THROWIFNOT 44"))?;
1853
1854        params.libraries.set(HashBytes::ZERO, LibDescr {
1855            lib: state_lib_code.clone(),
1856            publishers: {
1857                let mut p = Dict::new();
1858                p.set(HashBytes([0x00; 32]), ())?;
1859                p
1860            },
1861        })?;
1862
1863        let msg_state_init = StateInit {
1864            libraries: {
1865                let mut p = Dict::new();
1866                p.set(HashBytes([0x22; 32]), SimpleLib {
1867                    public: false,
1868                    root: msg_lib_code,
1869                })?;
1870                p
1871            },
1872            ..Default::default()
1873        };
1874        let addr = StdAddr::new(0, *CellBuilder::build_from(&msg_state_init)?.repr_hash());
1875
1876        let mut state = ExecutorState::new_uninit(&params, &config, &addr, OK_BALANCE);
1877        state.state = AccountState::Active(StateInit {
1878            code: Some(Boc::decode(tvmasm!(
1879                r#"
1880                ACCEPT
1881                PUSHCONT {
1882                    DUMPSTK
1883                    XLOAD
1884                    CTOS
1885                    BLESS
1886                    EXECUTE
1887                }
1888                // Execute state lib code
1889                OVER
1890                PUSHREF @{0000000000000000000000000000000000000000000000000000000000000000}
1891                PUSH s2
1892                EXECUTE
1893                // Execute account lib code
1894                PUSHREF @{1111111111111111111111111111111111111111111111111111111111111111}
1895                PUSH s2
1896                EXECUTE
1897                // Execute msg lib code
1898                PUSHREF @{2222222222222222222222222222222222222222222222222222222222222222}
1899                ROT
1900                EXECUTE
1901                "#
1902            ))?),
1903            libraries: {
1904                let mut p = Dict::new();
1905                p.set(HashBytes([0x11; 32]), SimpleLib {
1906                    public: false,
1907                    root: account_lib_code,
1908                })?;
1909                p
1910            },
1911            ..Default::default()
1912        });
1913        state.orig_status = AccountStatus::Active;
1914        state.end_status = AccountStatus::Active;
1915
1916        let msg = state.receive_in_msg(make_message(
1917            IntMsgInfo {
1918                src: addr.clone().into(),
1919                dst: addr.clone().into(),
1920                value: Tokens::new(1_000_000_000).into(),
1921                ..Default::default()
1922            },
1923            Some(msg_state_init),
1924            None,
1925        ))?;
1926
1927        state.credit_phase(&msg)?;
1928
1929        let prev_balance = state.balance.clone();
1930        let prev_state = state.state.clone();
1931        let prev_total_fees = state.total_fees;
1932        let prev_end_status = state.end_status;
1933
1934        let compute_phase = state.compute_phase(ComputePhaseContext {
1935            input: TransactionInput::Ordinary(&msg),
1936            storage_fee: Tokens::ZERO,
1937            force_accept: false,
1938            inspector: None,
1939        })?;
1940
1941        // Original balance must be correct.
1942        assert_eq!(
1943            compute_phase.original_balance,
1944            CurrencyCollection::from(OK_BALANCE)
1945        );
1946        // Message must be accepted.
1947        assert!(compute_phase.accepted);
1948        // State must not change.
1949        assert_eq!(state.state, prev_state);
1950        // Status must not change.
1951        assert_eq!(prev_end_status, state.end_status);
1952        // No actions must be produced.
1953        assert_eq!(compute_phase.actions, Cell::empty_cell());
1954        // Fees must be paid.
1955        let expected_gas_fee = Tokens::new(1315000);
1956        assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
1957        assert_eq!(state.balance.other, prev_balance.other);
1958        assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
1959
1960        let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
1961            panic!("expected executed compute phase");
1962        };
1963
1964        assert!(compute_phase.success);
1965        assert!(!compute_phase.msg_state_used);
1966        assert!(!compute_phase.account_activated);
1967        assert_eq!(compute_phase.gas_fees, expected_gas_fee);
1968        assert_eq!(compute_phase.gas_used, 1315);
1969        assert_eq!(
1970            compute_phase.gas_limit,
1971            VarUint56::new(config.gas_prices.gas_limit)
1972        );
1973        assert_eq!(compute_phase.gas_credit, None);
1974        assert_eq!(compute_phase.exit_code, 0);
1975        assert_eq!(compute_phase.exit_arg, None);
1976        assert_eq!(compute_phase.vm_steps, 39);
1977
1978        Ok(())
1979    }
1980}