tycho_executor/phase/
action.rs

1use anyhow::Result;
2use tycho_types::cell::{CellTreeStats, Lazy};
3use tycho_types::error::Error;
4use tycho_types::models::{
5    AccountState, AccountStatus, AccountStatusChange, ActionPhase, ChangeLibraryMode,
6    CurrencyCollection, ExecutedComputePhase, ExtraCurrencyCollection, LibRef, OutAction,
7    OwnedMessage, OwnedRelaxedMessage, RelaxedMsgInfo, ReserveCurrencyFlags, SendMsgFlags,
8    SimpleLib, StateInit, StorageUsedShort,
9};
10use tycho_types::num::{Tokens, VarUint56};
11use tycho_types::prelude::*;
12
13use crate::phase::receive::ReceivedMessage;
14use crate::util::{
15    ExtStorageStat, StateLimitsResult, StorageStatLimits, check_rewrite_dst_addr,
16    check_rewrite_src_addr, check_state_limits, check_state_limits_diff,
17};
18use crate::{ExecutorInspector, ExecutorState, PublicLibraryChange};
19
20/// Action phase input context.
21pub struct ActionPhaseContext<'a, 'e> {
22    /// Received message (external or internal).
23    pub received_message: Option<&'a mut ReceivedMessage>,
24    /// Original account balance before the compute phase.
25    pub original_balance: CurrencyCollection,
26    /// New account state to apply.
27    pub new_state: StateInit,
28    /// Actions list.
29    pub actions: Cell,
30    /// Successfully executed compute phase.
31    pub compute_phase: &'a ExecutedComputePhase,
32    /// Executor inspector.
33    pub inspector: Option<&'a mut ExecutorInspector<'e>>,
34}
35
36/// Executed action phase with additional info.
37#[derive(Debug)]
38pub struct ActionPhaseFull {
39    /// Resulting action phase.
40    pub action_phase: ActionPhase,
41    /// Additional fee in case of failure.
42    pub action_fine: Tokens,
43    /// Whether state can't be updated due to limits.
44    pub state_exceeds_limits: bool,
45    /// Whether bounce phase is required.
46    pub bounce: bool,
47}
48
49impl ExecutorState<'_> {
50    pub fn action_phase(&mut self, mut ctx: ActionPhaseContext<'_, '_>) -> Result<ActionPhaseFull> {
51        const MAX_ACTIONS: u16 = 255;
52
53        let mut res = ActionPhaseFull {
54            action_phase: ActionPhase {
55                success: false,
56                valid: false,
57                no_funds: false,
58                status_change: AccountStatusChange::Unchanged,
59                total_fwd_fees: None,
60                total_action_fees: None,
61                result_code: -1,
62                result_arg: None,
63                total_actions: 0,
64                special_actions: 0,
65                skipped_actions: 0,
66                messages_created: 0,
67                action_list_hash: *ctx.actions.repr_hash(),
68                total_message_size: StorageUsedShort::ZERO,
69            },
70            action_fine: Tokens::ZERO,
71            state_exceeds_limits: false,
72            bounce: false,
73        };
74
75        // Unpack actions list.
76        let mut action_idx = 0u16;
77
78        let mut list = Vec::new();
79        let mut actions = ctx.actions.as_ref();
80        loop {
81            if actions.is_exotic() {
82                // Actions list item must be an ordinary cell.
83                res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
84                res.action_phase.result_arg = Some(action_idx as _);
85                res.action_phase.valid = false;
86                return Ok(res);
87            }
88
89            // NOTE: We have checked that this cell is an ordinary.
90            let mut cs = actions.as_slice_allow_exotic();
91            if cs.is_empty() {
92                // Actions list terminates with an empty cell.
93                break;
94            }
95
96            list.push(actions);
97
98            actions = match cs.load_reference() {
99                Ok(child) => child,
100                Err(_) => {
101                    // Each action must contain at least one reference.
102                    res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
103                    res.action_phase.result_arg = Some(action_idx as _);
104                    res.action_phase.valid = false;
105                    return Ok(res);
106                }
107            };
108
109            action_idx += 1;
110            if action_idx > MAX_ACTIONS {
111                // There can be at most N actions.
112                res.action_phase.result_code = ResultCode::TooManyActions as i32;
113                res.action_phase.result_arg = Some(action_idx as _);
114                res.action_phase.valid = false;
115                return Ok(res);
116            }
117        }
118
119        res.action_phase.total_actions = action_idx;
120
121        // Parse actions.
122        let mut parsed_list = Vec::with_capacity(list.len());
123        for (action_idx, item) in list.into_iter().rev().enumerate() {
124            let mut cs = item.as_slice_allow_exotic();
125            cs.load_reference().ok(); // Skip first reference.
126
127            // Try to parse one action.
128            let mut cs_parsed = cs;
129            if let Ok(item) = OutAction::load_from(&mut cs_parsed) {
130                if cs_parsed.is_empty() {
131                    // Add this action if slices contained it exclusively.
132                    parsed_list.push(Some(item));
133                    continue;
134                }
135            }
136
137            // Special brhaviour for `SendMsg` action when we can at least parse its flags.
138            if cs.size_bits() >= 40 && cs.load_u32()? == OutAction::TAG_SEND_MSG {
139                let mode = SendMsgFlags::from_bits_retain(cs.load_u8()?);
140                if mode.contains(SendMsgFlags::IGNORE_ERROR) {
141                    // "IGNORE_ERROR" flag means that we can just skip this action.
142                    res.action_phase.skipped_actions += 1;
143                    parsed_list.push(None);
144                    continue;
145                } else if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
146                    // "BOUNCE_ON_ERROR" flag means that we fail the action phase,
147                    // but require a bounce phase to run afterwards.
148                    res.bounce = true;
149                }
150            }
151
152            res.action_phase.result_code = ResultCode::ActionInvalid as i32;
153            res.action_phase.result_arg = Some(action_idx as _);
154            res.action_phase.valid = false;
155            return Ok(res);
156        }
157
158        // Action list itself is ok.
159        res.action_phase.valid = true;
160
161        // Execute actions.
162        let mut action_ctx = ActionContext {
163            need_bounce_on_fail: false,
164            strict_extra_currency: self.params.strict_extra_currency,
165            received_message: ctx.received_message,
166            original_balance: &ctx.original_balance,
167            remaining_balance: self.balance.clone(),
168            reserved_balance: CurrencyCollection::ZERO,
169            action_fine: &mut res.action_fine,
170            new_state: &mut ctx.new_state,
171            end_lt: self.end_lt,
172            out_msgs: Vec::new(),
173            delete_account: false,
174            public_libs_diff: ctx.inspector.is_some().then(Vec::new),
175            compute_phase: ctx.compute_phase,
176            action_phase: &mut res.action_phase,
177        };
178
179        for (action_idx, action) in parsed_list.into_iter().enumerate() {
180            let Some(action) = action else {
181                continue;
182            };
183
184            action_ctx.need_bounce_on_fail = false;
185            action_ctx.action_phase.result_code = -1;
186            action_ctx.action_phase.result_arg = Some(action_idx as _);
187
188            let action = match action {
189                OutAction::SendMsg { mode, out_msg } => {
190                    let mut rewrite = None;
191                    loop {
192                        match self.do_send_message(mode, &out_msg, &mut action_ctx, rewrite) {
193                            Ok(SendMsgResult::Sent) => break Ok(()),
194                            Ok(SendMsgResult::Rewrite(r)) => rewrite = Some(r),
195                            Err(e) => break Err(e),
196                        }
197                    }
198                }
199                OutAction::SetCode { new_code } => self.do_set_code(new_code, &mut action_ctx),
200                OutAction::ReserveCurrency { mode, value } => {
201                    self.do_reserve_currency(mode, value, &mut action_ctx)
202                }
203                OutAction::ChangeLibrary { mode, lib } => {
204                    self.do_change_library(mode, lib, &mut action_ctx)
205                }
206            };
207
208            if let Err(ActionFailed) = action {
209                let result_code = &mut action_ctx.action_phase.result_code;
210                if *result_code == -1 {
211                    *result_code = ResultCode::ActionInvalid as i32;
212                }
213                if *result_code == ResultCode::NotEnoughBalance as i32
214                    || *result_code == ResultCode::NotEnoughExtraBalance as i32
215                {
216                    action_ctx.action_phase.no_funds = true;
217                }
218
219                // TODO: Enforce state limits here if we want to persist
220                // library changes even if action phase fails. This is
221                // not the case for now, but this is how the reference
222                // implementation works.
223
224                // Apply action fine to the balance.
225                action_ctx.apply_fine_on_error(
226                    &mut self.balance,
227                    &mut self.total_fees,
228                    self.params.charge_action_fees_on_fail,
229                )?;
230
231                // Apply flags.
232                res.bounce |= action_ctx.need_bounce_on_fail;
233
234                // Ignore all other action.
235                return Ok(res);
236            }
237        }
238
239        // Check that the new state does not exceed size limits.
240        // TODO: Ignore this step if account is going to be deleted anyway?
241        if !self.is_special {
242            let limits = &self.config.size_limits;
243            let is_masterchain = self.address.is_masterchain();
244            let check = match &self.state {
245                AccountState::Active(current_state) => check_state_limits_diff(
246                    current_state,
247                    action_ctx.new_state,
248                    limits,
249                    is_masterchain,
250                    &mut self.cached_storage_stat,
251                ),
252                AccountState::Uninit | AccountState::Frozen(_) => check_state_limits(
253                    action_ctx.new_state.code.as_ref(),
254                    action_ctx.new_state.data.as_ref(),
255                    &action_ctx.new_state.libraries,
256                    limits,
257                    is_masterchain,
258                    &mut self.cached_storage_stat,
259                ),
260            };
261
262            if matches!(check, StateLimitsResult::Exceeds) {
263                // Apply action fine to the balance.
264                action_ctx.apply_fine_on_error(
265                    &mut self.balance,
266                    &mut self.total_fees,
267                    self.params.charge_action_fees_on_fail,
268                )?;
269
270                // Apply flags.
271                res.bounce |= action_ctx.need_bounce_on_fail;
272                res.action_phase.result_code = ResultCode::StateOutOfLimits as i32;
273                res.state_exceeds_limits = true;
274                return Ok(res);
275            }
276
277            // NOTE: At this point if the state was successfully updated
278            // (`check_state_limits[_diff]` returned `StateLimitsResult::Fits`)
279            // cached storage stat will contain all visited cells for it.
280        }
281
282        if !action_ctx.action_fine.is_zero() {
283            action_ctx
284                .action_phase
285                .total_action_fees
286                .get_or_insert_default()
287                .try_add_assign(*action_ctx.action_fine)?;
288        }
289
290        action_ctx
291            .remaining_balance
292            .try_add_assign(&action_ctx.reserved_balance)?;
293
294        action_ctx.action_phase.result_code = 0;
295        action_ctx.action_phase.result_arg = None;
296        action_ctx.action_phase.success = true;
297
298        if action_ctx.delete_account {
299            if self.params.strict_extra_currency {
300                // Require only native balance to be zero for strict EC behaviour.
301                debug_assert!(action_ctx.remaining_balance.tokens.is_zero());
302            } else {
303                // Otherwise account must have a completely empty balance.
304                debug_assert!(action_ctx.remaining_balance.is_zero());
305            }
306            action_ctx.action_phase.status_change = AccountStatusChange::Deleted;
307            self.end_status = if action_ctx.remaining_balance.is_zero() {
308                // Delete account only if its balance is completely empty
309                // (both native and extra currency balance is zero).
310                AccountStatus::NotExists
311            } else {
312                // Leave account as uninit if it still has some extra currencies.
313                AccountStatus::Uninit
314            };
315            self.cached_storage_stat = None;
316        }
317
318        if let Some(fees) = action_ctx.action_phase.total_action_fees {
319            // NOTE: Forwarding fees are not collected here.
320            self.total_fees.try_add_assign(fees)?;
321        }
322        self.balance = action_ctx.remaining_balance;
323
324        if let Some(inspector) = ctx.inspector {
325            inspector.public_libs_diff = action_ctx.public_libs_diff.unwrap_or_default();
326        }
327
328        self.out_msgs = action_ctx.out_msgs;
329        self.end_lt = action_ctx.end_lt;
330        self.state = AccountState::Active(ctx.new_state);
331
332        Ok(res)
333    }
334
335    /// `SendMsg` action.
336    fn do_send_message(
337        &self,
338        mode: SendMsgFlags,
339        out_msg: &Lazy<OwnedRelaxedMessage>,
340        ctx: &mut ActionContext<'_>,
341        mut rewrite: Option<MessageRewrite>,
342    ) -> Result<SendMsgResult, ActionFailed> {
343        const MASK: u8 = SendMsgFlags::all().bits();
344        const INVALID_MASK: SendMsgFlags =
345            SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::WITH_REMAINING_BALANCE);
346        const EXT_MSG_MASK: u8 = SendMsgFlags::PAY_FEE_SEPARATELY
347            .union(SendMsgFlags::IGNORE_ERROR)
348            .union(SendMsgFlags::BOUNCE_ON_ERROR)
349            .bits();
350        const DELETE_MASK: SendMsgFlags =
351            SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::DELETE_IF_EMPTY);
352
353        // Check and apply mode flags.
354        if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
355            ctx.need_bounce_on_fail = true;
356        }
357
358        if mode.bits() & !MASK != 0 || mode.contains(INVALID_MASK) {
359            // - Mode has some unknown bits;
360            // - Or "ALL_BALANCE" flag was used with "WITH_REMAINING_BALANCE".
361            return Err(ActionFailed);
362        }
363
364        // We should only skip if at least the mode is correct.
365        let skip_invalid = mode.contains(SendMsgFlags::IGNORE_ERROR);
366        let check_skip_invalid = |e: ResultCode, ctx: &mut ActionContext<'_>| {
367            if skip_invalid {
368                ctx.action_phase.skipped_actions += 1;
369                Ok(SendMsgResult::Sent)
370            } else {
371                ctx.action_phase.result_code = e as i32;
372                Err(ActionFailed)
373            }
374        };
375
376        // Output message must be an ordinary cell.
377        if out_msg.is_exotic() {
378            return Err(ActionFailed);
379        }
380
381        // Unpack message.
382        let mut relaxed_info;
383        let mut state_init_cs;
384        let mut body_cs;
385
386        {
387            let mut cs = out_msg.as_slice_allow_exotic();
388
389            relaxed_info = RelaxedMsgInfo::load_from(&mut cs)?;
390            state_init_cs = load_state_init_as_slice(&mut cs)?;
391            body_cs = load_body_as_slice(&mut cs)?;
392
393            if !cs.is_empty() {
394                // Any remaining data in the message slice is treated as malicious data.
395                return Err(ActionFailed);
396            }
397        }
398
399        // Apply rewrite.
400        let rewritten_state_init_cb;
401        if let Some(MessageRewrite::StateInitToCell) = rewrite {
402            if state_init_cs.size_refs() >= 2 {
403                // Move state init to cell if it is more optimal.
404                rewritten_state_init_cb = rewrite_state_init_to_cell(state_init_cs);
405                state_init_cs = rewritten_state_init_cb.as_full_slice();
406            } else {
407                // Or try to move body to cell instead.
408                rewrite = Some(MessageRewrite::BodyToCell);
409            }
410        }
411
412        let rewritten_body_cs;
413        if let Some(MessageRewrite::BodyToCell) = rewrite {
414            if body_cs.size_bits() > 1 && !body_cs.get_bit(0).unwrap() {
415                // Try to move a non-empty plain body to cell.
416                rewritten_body_cs = rewrite_body_to_cell(body_cs);
417                body_cs = rewritten_body_cs.as_full_slice();
418            }
419        }
420
421        // Check info.
422        let mut use_mc_prices = self.address.is_masterchain();
423        match &mut relaxed_info {
424            // Check internal outbound message.
425            RelaxedMsgInfo::Int(info) => {
426                // Rewrite source address.
427                if !check_rewrite_src_addr(&self.address, &mut info.src) {
428                    // NOTE: For some reason we are not ignoring this error.
429                    ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
430                    return Err(ActionFailed);
431                };
432
433                // Rewrite destination address.
434                if !check_rewrite_dst_addr(&self.config.workchains, &mut info.dst) {
435                    return check_skip_invalid(ResultCode::InvalidDstAddr, ctx);
436                }
437                use_mc_prices |= info.dst.is_masterchain();
438
439                // Rewrite extra currencies.
440                if self.params.strict_extra_currency {
441                    match normalize_extra_balance(
442                        std::mem::take(&mut info.value.other),
443                        MAX_MSG_EXTRA_CURRENCIES,
444                    ) {
445                        Ok(other) => info.value.other = other,
446                        Err(BalanceExtraError::InvalidDict(_)) => {
447                            return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
448                        }
449                        Err(BalanceExtraError::OutOfLimit) => {
450                            return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
451                        }
452                    }
453                }
454
455                // Reset fees.
456                info.ihr_fee = Tokens::ZERO;
457                info.fwd_fee = Tokens::ZERO;
458
459                // Rewrite message timings.
460                info.created_at = self.params.block_unixtime;
461                info.created_lt = ctx.end_lt;
462
463                // Clear flags.
464                info.ihr_disabled = true;
465                info.bounced = false;
466            }
467            // Check external outbound message.
468            RelaxedMsgInfo::ExtOut(info) => {
469                if mode.bits() & !EXT_MSG_MASK != 0 {
470                    // Invalid mode for an outgoing external message.
471                    return Err(ActionFailed);
472                }
473
474                // Rewrite source address.
475                if !check_rewrite_src_addr(&self.address, &mut info.src) {
476                    ctx.action_phase.result_code = ResultCode::InvalidSrcAddr as i32;
477                    return Err(ActionFailed);
478                }
479
480                // Rewrite message timings.
481                info.created_at = self.params.block_unixtime;
482                info.created_lt = ctx.end_lt;
483            }
484        };
485
486        // Compute fine per cell. Account is required to pay it for every visited cell.
487        let prices = self.config.fwd_prices(use_mc_prices);
488        let mut max_cell_count = self.config.size_limits.max_msg_cells;
489        let fine_per_cell;
490        if self.is_special {
491            fine_per_cell = 0;
492        } else {
493            fine_per_cell = (prices.cell_price >> 16) / 4;
494
495            let mut funds = ctx.remaining_balance.tokens;
496            if let RelaxedMsgInfo::Int(info) = &relaxed_info {
497                if !mode.contains(SendMsgFlags::ALL_BALANCE)
498                    && !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY)
499                {
500                    let mut new_funds = info.value.tokens;
501
502                    if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
503                        && (|| {
504                            let msg_balance_remaining = match &ctx.received_message {
505                                Some(msg) => msg.balance_remaining.tokens,
506                                None => Tokens::ZERO,
507                            };
508                            new_funds.try_add_assign(msg_balance_remaining)?;
509                            new_funds.try_sub_assign(ctx.compute_phase.gas_fees)?;
510                            new_funds.try_sub_assign(*ctx.action_fine)?;
511
512                            Ok::<_, tycho_types::error::Error>(())
513                        })()
514                        .is_err()
515                    {
516                        return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
517                    }
518
519                    funds = std::cmp::min(funds, new_funds);
520                }
521            }
522
523            if funds < Tokens::new(max_cell_count as u128 * fine_per_cell as u128) {
524                debug_assert_ne!(fine_per_cell, 0);
525                max_cell_count = (funds.into_inner() / fine_per_cell as u128)
526                    .try_into()
527                    .unwrap_or(u32::MAX);
528            }
529        }
530
531        let collect_fine = |cells: u32, ctx: &mut ActionContext<'_>| {
532            let mut fine = Tokens::new(
533                fine_per_cell.saturating_mul(std::cmp::min(max_cell_count, cells) as u64) as _,
534            );
535            fine = std::cmp::min(fine, ctx.remaining_balance.tokens);
536            ctx.action_fine.try_add_assign(fine)?;
537            ctx.remaining_balance.try_sub_assign_tokens(fine)
538        };
539
540        // Compute size of the message.
541        let stats = 'stats: {
542            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
543                bit_count: self.config.size_limits.max_msg_bits,
544                cell_count: max_cell_count,
545            });
546
547            'valid: {
548                for cell in state_init_cs.references() {
549                    if !stats.add_cell(cell) {
550                        break 'valid;
551                    }
552                }
553
554                for cell in body_cs.references() {
555                    if !stats.add_cell(cell) {
556                        break 'valid;
557                    }
558                }
559
560                // Add EC dict when `strict` behaviour is disabled.
561                if !self.params.strict_extra_currency {
562                    if let RelaxedMsgInfo::Int(int) = &relaxed_info {
563                        if let Some(cell) = int.value.other.as_dict().root() {
564                            if !stats.add_cell(cell.as_ref()) {
565                                break 'valid;
566                            }
567                        }
568                    }
569                }
570
571                break 'stats stats.stats();
572            }
573
574            collect_fine(stats.cells, ctx)?;
575            return check_skip_invalid(ResultCode::MessageOutOfLimits, ctx);
576        };
577
578        // Make sure that `check_skip_invalid` will collect fine.
579        let check_skip_invalid = move |e: ResultCode, ctx: &mut ActionContext<'_>| {
580            collect_fine(stats.cell_count as _, ctx)?;
581            check_skip_invalid(e, ctx)
582        };
583
584        // Compute forwarding fees.
585        let fwd_fee = if self.is_special {
586            Tokens::ZERO
587        } else {
588            prices.compute_fwd_fee(stats)
589        };
590
591        // Finalize message.
592        let msg;
593        let fees_collected;
594        match &mut relaxed_info {
595            RelaxedMsgInfo::Int(info) => {
596                // Rewrite message value and compute how much will be withdwarn.
597                let value_to_pay = match ctx.rewrite_message_value(&mut info.value, mode, fwd_fee) {
598                    Ok(total_value) => total_value,
599                    Err(_) => return check_skip_invalid(ResultCode::NotEnoughBalance, ctx),
600                };
601
602                // Check if remaining balance is enough to pay `total_value`.
603                if ctx.remaining_balance.tokens < value_to_pay {
604                    return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
605                }
606
607                // Check that the number of extra currencies is whithin the limit.
608                if self.params.strict_extra_currency
609                    && !check_extra_balance(&info.value.other, MAX_MSG_EXTRA_CURRENCIES)
610                {
611                    return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
612                }
613
614                // Try to withdraw extra currencies from the remaining balance.
615                let other = match ctx.remaining_balance.other.checked_sub(&info.value.other) {
616                    Ok(other) => other,
617                    Err(_) => return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx),
618                };
619
620                // Split forwarding fee.
621                fees_collected = prices.get_first_part(fwd_fee);
622                info.fwd_fee = fwd_fee - fees_collected;
623
624                // Finalize message.
625                msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
626                    Ok(msg) => msg,
627                    Err(_) => match MessageRewrite::next(rewrite) {
628                        Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
629                        None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
630                    },
631                };
632
633                // Clear message balance if it was used.
634                if let Some(msg) = &mut ctx.received_message {
635                    if mode.contains(SendMsgFlags::ALL_BALANCE)
636                        || mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
637                    {
638                        if self.params.strict_extra_currency {
639                            // Only native balance was used.
640                            msg.balance_remaining.tokens = Tokens::ZERO;
641                        } else {
642                            // All balance was used.
643                            msg.balance_remaining = CurrencyCollection::ZERO;
644                        }
645                    }
646                }
647
648                // Update the remaining balance.
649                ctx.remaining_balance.tokens -= value_to_pay;
650                ctx.remaining_balance.other = other;
651            }
652            RelaxedMsgInfo::ExtOut(_) => {
653                // Check if the remaining balance is enough to pay forwarding fees.
654                if ctx.remaining_balance.tokens < fwd_fee {
655                    return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
656                }
657
658                // Finalize message.
659                msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
660                    Ok(msg) => msg,
661                    Err(_) => match MessageRewrite::next(rewrite) {
662                        Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
663                        None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
664                    },
665                };
666
667                // Update the remaining balance.
668                ctx.remaining_balance.tokens -= fwd_fee;
669                fees_collected = fwd_fee;
670            }
671        }
672
673        update_total_msg_stat(
674            &mut ctx.action_phase.total_message_size,
675            stats,
676            msg.bit_len(),
677        );
678
679        ctx.action_phase.messages_created += 1;
680        ctx.end_lt += 1;
681
682        ctx.out_msgs.push(msg);
683
684        *ctx.action_phase.total_action_fees.get_or_insert_default() += fees_collected;
685        *ctx.action_phase.total_fwd_fees.get_or_insert_default() += fwd_fee;
686
687        if mode.contains(DELETE_MASK) {
688            ctx.delete_account = if self.params.strict_extra_currency {
689                // Delete when native balance was used.
690                debug_assert!(ctx.remaining_balance.tokens.is_zero());
691                ctx.reserved_balance.tokens.is_zero()
692            } else {
693                // Delete when full balance was used.
694                debug_assert!(ctx.remaining_balance.is_zero());
695                ctx.reserved_balance.is_zero()
696            };
697        }
698
699        Ok(SendMsgResult::Sent)
700    }
701
702    /// `SetCode` action.
703    fn do_set_code(&self, new_code: Cell, ctx: &mut ActionContext<'_>) -> Result<(), ActionFailed> {
704        // Update context.
705        ctx.new_state.code = Some(new_code);
706        ctx.action_phase.special_actions += 1;
707
708        // Done
709        Ok(())
710    }
711
712    /// `ReserveCurrency` action.
713    fn do_reserve_currency(
714        &self,
715        mode: ReserveCurrencyFlags,
716        mut reserve: CurrencyCollection,
717        ctx: &mut ActionContext<'_>,
718    ) -> Result<(), ActionFailed> {
719        const MASK: u8 = ReserveCurrencyFlags::all().bits();
720
721        // Check and apply mode flags.
722        if mode.contains(ReserveCurrencyFlags::BOUNCE_ON_ERROR) {
723            ctx.need_bounce_on_fail = true;
724        }
725
726        if mode.bits() & !MASK != 0 {
727            // Invalid mode.
728            return Err(ActionFailed);
729        }
730
731        if self.params.strict_extra_currency && !reserve.other.is_empty() {
732            // Cannot reserve extra currencies when strict behaviour is enabled.
733            return Err(ActionFailed);
734        }
735
736        if mode.contains(ReserveCurrencyFlags::WITH_ORIGINAL_BALANCE) {
737            if mode.contains(ReserveCurrencyFlags::REVERSE) {
738                if self.params.strict_extra_currency {
739                    reserve.tokens = ctx
740                        .original_balance
741                        .tokens
742                        .checked_sub(reserve.tokens)
743                        .ok_or(ActionFailed)?;
744                } else {
745                    reserve = ctx.original_balance.checked_sub(&reserve)?;
746                }
747            } else if self.params.strict_extra_currency {
748                reserve.try_add_assign_tokens(ctx.original_balance.tokens)?;
749            } else {
750                reserve.try_add_assign(ctx.original_balance)?;
751            }
752        } else if mode.contains(ReserveCurrencyFlags::REVERSE) {
753            // Invalid mode.
754            return Err(ActionFailed);
755        }
756
757        if mode.contains(ReserveCurrencyFlags::IGNORE_ERROR) {
758            // Clamp balance.
759            reserve = reserve.checked_clamp(&ctx.remaining_balance)?;
760        }
761
762        // Reserve balance.
763        let mut new_balance = CurrencyCollection {
764            tokens: match ctx.remaining_balance.tokens.checked_sub(reserve.tokens) {
765                Some(tokens) => tokens,
766                None => {
767                    ctx.action_phase.result_code = ResultCode::NotEnoughBalance as i32;
768                    return Err(ActionFailed);
769                }
770            },
771            other: match ctx.remaining_balance.other.checked_sub(&reserve.other) {
772                Ok(other) => other,
773                Err(_) => {
774                    ctx.action_phase.result_code = ResultCode::NotEnoughExtraBalance as i32;
775                    return Err(ActionFailed);
776                }
777            },
778        };
779
780        // Always normalize reserved balance.
781        reserve.other.normalize()?;
782
783        // Apply "ALL_BUT" flag. Leave only "new_balance", reserve everything else.
784        if mode.contains(ReserveCurrencyFlags::ALL_BUT) {
785            if self.params.strict_extra_currency {
786                std::mem::swap(&mut new_balance.tokens, &mut reserve.tokens);
787            } else {
788                std::mem::swap(&mut new_balance, &mut reserve);
789            }
790        }
791
792        // Update context.
793        ctx.remaining_balance = new_balance;
794        ctx.reserved_balance.try_add_assign(&reserve)?;
795        ctx.action_phase.special_actions += 1;
796
797        // Done
798        Ok(())
799    }
800
801    /// `ChangeLibrary` action.
802    fn do_change_library(
803        &self,
804        mode: ChangeLibraryMode,
805        mut lib: LibRef,
806        ctx: &mut ActionContext<'_>,
807    ) -> Result<(), ActionFailed> {
808        // Having both "ADD_PRIVATE" and "ADD_PUBLIC" flags is invalid.
809        const INVALID_MODE: ChangeLibraryMode = ChangeLibraryMode::from_bits_retain(
810            ChangeLibraryMode::ADD_PRIVATE.bits() | ChangeLibraryMode::ADD_PUBLIC.bits(),
811        );
812
813        // Check and apply mode flags.
814        if mode.contains(ChangeLibraryMode::BOUNCE_ON_ERROR) {
815            ctx.need_bounce_on_fail = true;
816        }
817
818        if mode.contains(INVALID_MODE) {
819            return Err(ActionFailed);
820        }
821
822        let hash = match &lib {
823            LibRef::Cell(cell) => cell.repr_hash(),
824            LibRef::Hash(hash) => hash,
825        };
826
827        let is_masterchain = self.address.is_masterchain();
828
829        let add_public = mode.contains(ChangeLibraryMode::ADD_PUBLIC);
830        if add_public || mode.contains(ChangeLibraryMode::ADD_PRIVATE) {
831            // Add new library.
832            let mut was_public = None;
833            if let Ok(Some(prev)) = ctx.new_state.libraries.get(hash) {
834                if prev.public == add_public {
835                    // Do nothing if library already exists with the same `public` flag.
836                    ctx.action_phase.special_actions += 1;
837                    return Ok(());
838                } else {
839                    // If library exists allow changing its `public` flag when `LibRef::Hash` was used.
840                    was_public = Some(prev.public);
841                    lib = LibRef::Cell(prev.root);
842                }
843            }
844
845            let LibRef::Cell(root) = lib else {
846                ctx.action_phase.result_code = ResultCode::NoLibCode as i32;
847                return Err(ActionFailed);
848            };
849
850            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
851                bit_count: u32::MAX,
852                cell_count: self.config.size_limits.max_library_cells,
853            });
854            if !stats.add_cell(root.as_ref()) {
855                ctx.action_phase.result_code = ResultCode::LibOutOfLimits as i32;
856                return Err(ActionFailed);
857            }
858
859            // Add library.
860            match ctx.new_state.libraries.set(*root.repr_hash(), SimpleLib {
861                public: add_public,
862                root: root.clone(),
863            }) {
864                // Track removed public libs by an inspector for masterchain accounts.
865                Ok(_) if is_masterchain => {
866                    // Track removed or added public libs by an inspector.
867                    if let Some(diff) = &mut ctx.public_libs_diff {
868                        match (was_public, add_public) {
869                            // Add new public libraries.
870                            (None, true) | (Some(false), true) => {
871                                diff.push(PublicLibraryChange::Add(root))
872                            }
873                            // Remove public libraries
874                            (Some(true), false) => {
875                                diff.push(PublicLibraryChange::Remove(*root.repr_hash()));
876                            }
877                            // Ignore unchanged or private libraries.
878                            _ => {}
879                        }
880                    }
881                }
882                Ok(_) => {}
883                Err(_) => {
884                    ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
885                    return Err(ActionFailed);
886                }
887            }
888        } else {
889            // Remove library.
890            match ctx.new_state.libraries.remove(hash) {
891                // Track removed public libs by an inspector for masterchain accounts.
892                Ok(Some(lib)) if is_masterchain && lib.public => {
893                    if let Some(diff) = &mut ctx.public_libs_diff {
894                        diff.push(PublicLibraryChange::Remove(*hash));
895                    }
896                }
897                Ok(_) => {}
898                Err(_) => {
899                    ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
900                    return Err(ActionFailed);
901                }
902            }
903        }
904
905        // Update context.
906        ctx.action_phase.special_actions += 1;
907
908        // Done
909        Ok(())
910    }
911}
912
913struct ActionContext<'a> {
914    need_bounce_on_fail: bool,
915    strict_extra_currency: bool,
916    received_message: Option<&'a mut ReceivedMessage>,
917    original_balance: &'a CurrencyCollection,
918    remaining_balance: CurrencyCollection,
919    reserved_balance: CurrencyCollection,
920    action_fine: &'a mut Tokens,
921    new_state: &'a mut StateInit,
922    end_lt: u64,
923    out_msgs: Vec<Lazy<OwnedMessage>>,
924    delete_account: bool,
925    public_libs_diff: Option<Vec<PublicLibraryChange>>,
926
927    compute_phase: &'a ExecutedComputePhase,
928    action_phase: &'a mut ActionPhase,
929}
930
931impl ActionContext<'_> {
932    fn apply_fine_on_error(
933        &mut self,
934        balance: &mut CurrencyCollection,
935        total_fees: &mut Tokens,
936        charge_action_fees: bool,
937    ) -> Result<(), Error> {
938        // Compute the resulting action fine (it must not be greater than the account balance).
939        if charge_action_fees {
940            self.action_fine
941                .try_add_assign(self.action_phase.total_action_fees.unwrap_or_default())?;
942        }
943
944        // Reset forwarding fee since no messages were actually sent.
945        // NOTE: This behaviour is not present in the reference implementation
946        //       but it seems to be more correct.
947        self.action_phase.total_fwd_fees = None;
948
949        // Charge the account balance for the action fine.
950        self.action_phase.total_action_fees = Some(*self.action_fine).filter(|t| !t.is_zero());
951
952        balance.tokens.try_sub_assign(*self.action_fine)?;
953        total_fees.try_add_assign(*self.action_fine)
954    }
955
956    fn rewrite_message_value(
957        &self,
958        value: &mut CurrencyCollection,
959        mut mode: SendMsgFlags,
960        fees_total: Tokens,
961    ) -> Result<Tokens, ActionFailed> {
962        // Update `value` based on flags.
963        if mode.contains(SendMsgFlags::ALL_BALANCE) {
964            // Attach all remaining balance to the message.
965            if self.strict_extra_currency {
966                // Attach only native balance when strict behaviour is enabled.
967                value.tokens = self.remaining_balance.tokens;
968            } else {
969                // Attach both native and extra currencies otherwise.
970                *value = self.remaining_balance.clone();
971            };
972            // Pay fees from the attached value.
973            mode.remove(SendMsgFlags::PAY_FEE_SEPARATELY);
974        } else if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE) {
975            if let Some(msg) = &self.received_message {
976                // Attach all remaining balance of the inbound message.
977                // (in addition to the original value).
978                if self.strict_extra_currency {
979                    // Attach only native balance when strict behaviour is enabled.
980                    value.try_add_assign_tokens(msg.balance_remaining.tokens)?;
981                } else {
982                    // Attach both native and extra currencies otherwise.
983                    value.try_add_assign(&msg.balance_remaining)?;
984                }
985            }
986
987            if !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
988                // Try to exclude fees from the attached value.
989                value.try_sub_assign_tokens(*self.action_fine)?;
990                value.try_sub_assign_tokens(self.compute_phase.gas_fees)?;
991            }
992        }
993
994        // Compute `value + fees`.
995        let mut total = value.tokens;
996        if mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
997            total.try_add_assign(fees_total)?;
998        } else {
999            value.tokens.try_sub_assign(fees_total)?;
1000        }
1001
1002        // Done
1003        Ok(total)
1004    }
1005}
1006
1007struct ActionFailed;
1008
1009impl From<anyhow::Error> for ActionFailed {
1010    #[inline]
1011    fn from(_: anyhow::Error) -> Self {
1012        Self
1013    }
1014}
1015
1016impl From<Error> for ActionFailed {
1017    #[inline]
1018    fn from(_: Error) -> Self {
1019        Self
1020    }
1021}
1022
1023#[derive(Debug, Clone, Copy)]
1024enum SendMsgResult {
1025    Sent,
1026    Rewrite(MessageRewrite),
1027}
1028
1029#[derive(Debug, Clone, Copy)]
1030enum MessageRewrite {
1031    StateInitToCell,
1032    BodyToCell,
1033}
1034
1035impl MessageRewrite {
1036    pub fn next(rewrite: Option<Self>) -> Option<Self> {
1037        match rewrite {
1038            None => Some(Self::StateInitToCell),
1039            Some(Self::StateInitToCell) => Some(Self::BodyToCell),
1040            Some(Self::BodyToCell) => None,
1041        }
1042    }
1043}
1044
1045fn load_state_init_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1046    let mut res_cs = *cs;
1047
1048    // (Maybe (Either StateInit ^StateInit))
1049    if cs.load_bit()? {
1050        if cs.load_bit()? {
1051            // right$1 ^StateInit
1052            let state_root = cs.load_reference()?;
1053            if state_root.is_exotic() {
1054                // Only ordinary cells are allowed as state init.
1055                return Err(Error::InvalidData);
1056            }
1057
1058            // Validate `StateInit` by reading.
1059            let mut cs = state_root.as_slice_allow_exotic();
1060            StateInit::load_from(&mut cs)?;
1061
1062            // And ensure that nothing more was left.
1063            if !cs.is_empty() {
1064                return Err(Error::CellOverflow);
1065            }
1066        } else {
1067            // left$0 StateInit
1068
1069            // Validate `StateInit` by reading.
1070            StateInit::load_from(cs)?;
1071        }
1072    }
1073
1074    res_cs.skip_last(cs.size_bits(), cs.size_refs())?;
1075    Ok(res_cs)
1076}
1077
1078fn load_body_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1079    let res_cs = *cs;
1080
1081    if cs.load_bit()? {
1082        // right$1 ^X
1083        cs.skip_first(0, 1)?;
1084    } else {
1085        // left$0 X
1086        cs.load_remaining();
1087    }
1088
1089    Ok(res_cs)
1090}
1091
1092fn rewrite_state_init_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1093    // Skip prefix `just$1 (left$0 ...)`.
1094    let prefix = cs.load_small_uint(2).unwrap();
1095    debug_assert_eq!(prefix, 0b10);
1096
1097    // Build ^StateInit.
1098    let cell = CellBuilder::build_from(cs).unwrap();
1099
1100    // Build `just$1 (right$1 ^StateInit)`.
1101    let mut b = CellBuilder::new();
1102    b.store_small_uint(0b11, 2).unwrap();
1103    b.store_reference(cell).unwrap();
1104
1105    // Done
1106    b
1107}
1108
1109fn rewrite_body_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1110    // Skip prefix `left$0 ...`.
1111    let prefix = cs.load_bit().unwrap();
1112    debug_assert!(!prefix);
1113
1114    // Build ^X.
1115    let cell = CellBuilder::build_from(cs).unwrap();
1116
1117    // Build `right$1 ^X`.
1118    let mut b = CellBuilder::new();
1119    b.store_bit_one().unwrap();
1120    b.store_reference(cell).unwrap();
1121
1122    // Done
1123    b
1124}
1125
1126fn build_message(
1127    info: &RelaxedMsgInfo,
1128    state_init_cs: &CellSlice<'_>,
1129    body_cs: &CellSlice<'_>,
1130) -> Result<Lazy<OwnedMessage>, Error> {
1131    CellBuilder::build_from((info, state_init_cs, body_cs)).map(|cell| {
1132        // SAFETY: Tuple is always built as ordinary cell.
1133        unsafe { Lazy::from_raw_unchecked(cell) }
1134    })
1135}
1136
1137fn update_total_msg_stat(
1138    total_message_size: &mut StorageUsedShort,
1139    stats: CellTreeStats,
1140    root_bits: u16,
1141) {
1142    let bits_diff = VarUint56::new(stats.bit_count.saturating_add(root_bits as _));
1143    let cells_diff = VarUint56::new(stats.cell_count.saturating_add(1));
1144
1145    total_message_size.bits = total_message_size.bits.saturating_add(bits_diff);
1146    total_message_size.cells = total_message_size.cells.saturating_add(cells_diff);
1147}
1148
1149fn check_extra_balance(value: &ExtraCurrencyCollection, limit: usize) -> bool {
1150    for (i, entry) in value.as_dict().iter().enumerate() {
1151        if i > limit || entry.is_err() {
1152            return false;
1153        }
1154    }
1155    true
1156}
1157
1158fn normalize_extra_balance(
1159    value: ExtraCurrencyCollection,
1160    limit: usize,
1161) -> Result<ExtraCurrencyCollection, BalanceExtraError> {
1162    let mut result = value.clone();
1163    for (i, entry) in value.as_dict().iter().enumerate() {
1164        if i > limit {
1165            return Err(BalanceExtraError::OutOfLimit);
1166        }
1167        let (currency_id, other) = entry.map_err(BalanceExtraError::InvalidDict)?;
1168        if other.is_zero() {
1169            result
1170                .as_dict_mut()
1171                .remove(currency_id)
1172                .map_err(BalanceExtraError::InvalidDict)?;
1173        }
1174    }
1175    Ok(result)
1176}
1177
1178enum BalanceExtraError {
1179    OutOfLimit,
1180    InvalidDict(#[allow(unused)] Error),
1181}
1182
1183#[repr(i32)]
1184#[derive(Debug, thiserror::Error)]
1185enum ResultCode {
1186    #[error("invalid action list")]
1187    ActionListInvalid = 32,
1188    #[error("too many actions")]
1189    TooManyActions = 33,
1190    #[error("invalid or unsupported action")]
1191    ActionInvalid = 34,
1192    #[error("invalid source address")]
1193    InvalidSrcAddr = 35,
1194    #[error("invalid destination address")]
1195    InvalidDstAddr = 36,
1196    #[error("not enough balance (base currency)")]
1197    NotEnoughBalance = 37,
1198    #[error("not enough balance (extra currency)")]
1199    NotEnoughExtraBalance = 38,
1200    #[error("failed to fit message into cell")]
1201    FailedToFitMessage = 39,
1202    #[error("message exceeds limits")]
1203    MessageOutOfLimits = 40,
1204    #[error("library code not found")]
1205    NoLibCode = 41,
1206    #[error("failed to change libraries dict")]
1207    InvalidLibrariesDict = 42,
1208    #[error("too many library cells")]
1209    LibOutOfLimits = 43,
1210    #[error("too many extra currencies")]
1211    TooManyExtraCurrencies = 44,
1212    #[error("state exceeds limits")]
1213    StateOutOfLimits = 50,
1214}
1215
1216// TODO: Move into config parm 43.
1217const MAX_MSG_EXTRA_CURRENCIES: usize = 2;
1218
1219#[cfg(test)]
1220mod tests {
1221    use std::collections::BTreeMap;
1222
1223    use tycho_asm_macros::tvmasm;
1224    use tycho_types::merkle::MerkleProof;
1225    use tycho_types::models::{
1226        Anycast, IntAddr, MessageLayout, MsgInfo, RelaxedIntMsgInfo, RelaxedMessage, StdAddr,
1227        VarAddr,
1228    };
1229    use tycho_types::num::{Uint9, VarUint248};
1230
1231    use super::*;
1232    use crate::ExecutorParams;
1233    use crate::tests::{make_default_config, make_default_params};
1234
1235    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
1236    const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
1237    const OK_GAS: Tokens = Tokens::new(1_000_000);
1238
1239    fn stub_compute_phase(gas_fees: Tokens) -> ExecutedComputePhase {
1240        ExecutedComputePhase {
1241            success: true,
1242            msg_state_used: false,
1243            account_activated: false,
1244            gas_fees,
1245            gas_used: Default::default(),
1246            gas_limit: Default::default(),
1247            gas_credit: None,
1248            mode: 0,
1249            exit_code: 0,
1250            exit_arg: None,
1251            vm_steps: 0,
1252            vm_init_state_hash: Default::default(),
1253            vm_final_state_hash: Default::default(),
1254        }
1255    }
1256
1257    fn empty_action_phase() -> ActionPhase {
1258        ActionPhase {
1259            success: true,
1260            valid: true,
1261            no_funds: false,
1262            status_change: AccountStatusChange::Unchanged,
1263            total_fwd_fees: None,
1264            total_action_fees: None,
1265            result_code: 0,
1266            result_arg: None,
1267            total_actions: 0,
1268            special_actions: 0,
1269            skipped_actions: 0,
1270            messages_created: 0,
1271            action_list_hash: *Cell::empty_cell_ref().repr_hash(),
1272            total_message_size: Default::default(),
1273        }
1274    }
1275
1276    fn make_action_list<I: IntoIterator<Item: Store>>(actions: I) -> Cell {
1277        let mut root = Cell::default();
1278        for action in actions {
1279            root = CellBuilder::build_from((root, action)).unwrap();
1280        }
1281        root
1282    }
1283
1284    fn make_relaxed_message(
1285        info: impl Into<RelaxedMsgInfo>,
1286        init: Option<StateInit>,
1287        body: Option<CellBuilder>,
1288    ) -> Lazy<OwnedRelaxedMessage> {
1289        let body = match &body {
1290            None => Cell::empty_cell_ref().as_slice_allow_exotic(),
1291            Some(cell) => cell.as_full_slice(),
1292        };
1293        Lazy::new(&RelaxedMessage {
1294            info: info.into(),
1295            init,
1296            body,
1297            layout: None,
1298        })
1299        .unwrap()
1300        .cast_into()
1301    }
1302
1303    fn compute_full_stats(msg: &Lazy<OwnedMessage>, params: &ExecutorParams) -> StorageUsedShort {
1304        let msg = 'cell: {
1305            if params.strict_extra_currency {
1306                let mut parsed = msg.load().unwrap();
1307                if let MsgInfo::Int(int) = &mut parsed.info {
1308                    if !int.value.other.is_empty() {
1309                        int.value.other = ExtraCurrencyCollection::new();
1310                        break 'cell CellBuilder::build_from(parsed).unwrap();
1311                    }
1312                }
1313            }
1314            msg.inner().clone()
1315        };
1316
1317        let stats = {
1318            let mut stats = ExtStorageStat::with_limits(StorageStatLimits::UNLIMITED);
1319            assert!(stats.add_cell(msg.as_ref()));
1320            stats.stats()
1321        };
1322        StorageUsedShort {
1323            cells: VarUint56::new(stats.cell_count),
1324            bits: VarUint56::new(stats.bit_count),
1325        }
1326    }
1327
1328    fn original_balance(
1329        state: &ExecutorState<'_>,
1330        compute_phase: &ExecutedComputePhase,
1331    ) -> CurrencyCollection {
1332        state
1333            .balance
1334            .clone()
1335            .checked_add(&compute_phase.gas_fees.into())
1336            .unwrap()
1337    }
1338
1339    #[test]
1340    fn empty_actions() -> Result<()> {
1341        let params = make_default_params();
1342        let config = make_default_config();
1343        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1344
1345        let compute_phase = stub_compute_phase(OK_GAS);
1346        let prev_total_fees = state.total_fees;
1347        let prev_balance = state.balance.clone();
1348        let prev_end_lt = state.end_lt;
1349
1350        let ActionPhaseFull {
1351            action_phase,
1352            action_fine,
1353            state_exceeds_limits,
1354            bounce,
1355        } = state.action_phase(ActionPhaseContext {
1356            received_message: None,
1357            original_balance: original_balance(&state, &compute_phase),
1358            new_state: StateInit::default(),
1359            actions: Cell::empty_cell(),
1360            compute_phase: &compute_phase,
1361            inspector: None,
1362        })?;
1363
1364        assert_eq!(action_phase, empty_action_phase());
1365        assert_eq!(action_fine, Tokens::ZERO);
1366        assert!(!state_exceeds_limits);
1367        assert!(!bounce);
1368        assert_eq!(state.total_fees, prev_total_fees);
1369        assert_eq!(state.balance, prev_balance);
1370        assert_eq!(state.end_lt, prev_end_lt);
1371        Ok(())
1372    }
1373
1374    #[test]
1375    fn too_many_actions() -> Result<()> {
1376        let params = make_default_params();
1377        let config = make_default_config();
1378        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1379
1380        let compute_phase = stub_compute_phase(OK_GAS);
1381        let prev_total_fees = state.total_fees;
1382        let prev_balance = state.balance.clone();
1383        let prev_end_lt = state.end_lt;
1384
1385        let actions = make_action_list(
1386            std::iter::repeat_with(|| OutAction::SetCode {
1387                new_code: Cell::empty_cell(),
1388            })
1389            .take(300),
1390        );
1391
1392        let ActionPhaseFull {
1393            action_phase,
1394            action_fine,
1395            state_exceeds_limits,
1396            bounce,
1397        } = state.action_phase(ActionPhaseContext {
1398            received_message: None,
1399            original_balance: original_balance(&state, &compute_phase),
1400            new_state: StateInit::default(),
1401            actions: actions.clone(),
1402            compute_phase: &compute_phase,
1403            inspector: None,
1404        })?;
1405
1406        assert_eq!(action_phase, ActionPhase {
1407            success: false,
1408            valid: false,
1409            result_code: ResultCode::TooManyActions as i32,
1410            result_arg: Some(256),
1411            action_list_hash: *actions.repr_hash(),
1412            ..empty_action_phase()
1413        });
1414        assert_eq!(action_fine, Tokens::ZERO);
1415        assert!(!state_exceeds_limits);
1416        assert!(!bounce);
1417        assert_eq!(state.total_fees, prev_total_fees);
1418        assert_eq!(state.balance, prev_balance);
1419        assert_eq!(state.end_lt, prev_end_lt);
1420        Ok(())
1421    }
1422
1423    #[test]
1424    fn invalid_action_list() -> Result<()> {
1425        let params = make_default_params();
1426        let config = make_default_config();
1427        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1428
1429        let compute_phase = stub_compute_phase(OK_GAS);
1430        let prev_total_fees = state.total_fees;
1431        let prev_balance = state.balance.clone();
1432        let prev_end_lt = state.end_lt;
1433
1434        let actions = CellBuilder::build_from((
1435            CellBuilder::build_from(MerkleProof::default())?,
1436            OutAction::SetCode {
1437                new_code: Cell::default(),
1438            },
1439        ))?;
1440
1441        let ActionPhaseFull {
1442            action_phase,
1443            action_fine,
1444            state_exceeds_limits,
1445            bounce,
1446        } = state.action_phase(ActionPhaseContext {
1447            received_message: None,
1448            original_balance: original_balance(&state, &compute_phase),
1449            new_state: StateInit::default(),
1450            actions: actions.clone(),
1451            compute_phase: &compute_phase,
1452            inspector: None,
1453        })?;
1454
1455        assert_eq!(action_phase, ActionPhase {
1456            success: false,
1457            valid: false,
1458            result_code: ResultCode::ActionListInvalid as i32,
1459            result_arg: Some(1),
1460            action_list_hash: *actions.repr_hash(),
1461            ..empty_action_phase()
1462        });
1463        assert_eq!(action_fine, Tokens::ZERO);
1464        assert!(!state_exceeds_limits);
1465        assert!(!bounce);
1466        assert_eq!(state.total_fees, prev_total_fees);
1467        assert_eq!(state.balance, prev_balance);
1468        assert_eq!(state.end_lt, prev_end_lt);
1469        Ok(())
1470    }
1471
1472    #[test]
1473    fn invalid_action() -> Result<()> {
1474        let params = make_default_params();
1475        let config = make_default_config();
1476        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1477
1478        let compute_phase = stub_compute_phase(OK_GAS);
1479        let prev_total_fees = state.total_fees;
1480        let prev_balance = state.balance.clone();
1481        let prev_end_lt = state.end_lt;
1482
1483        let set_code_action = {
1484            let mut b = CellBuilder::new();
1485            OutAction::SetCode {
1486                new_code: Cell::empty_cell(),
1487            }
1488            .store_into(&mut b, Cell::empty_context())?;
1489            b
1490        };
1491        let invalid_action = {
1492            let mut b = CellBuilder::new();
1493            b.store_u32(0xdeafbeaf)?;
1494            b
1495        };
1496
1497        let actions = make_action_list([
1498            set_code_action.as_full_slice(),
1499            set_code_action.as_full_slice(),
1500            invalid_action.as_full_slice(),
1501            set_code_action.as_full_slice(),
1502            set_code_action.as_full_slice(),
1503        ]);
1504
1505        let ActionPhaseFull {
1506            action_phase,
1507            action_fine,
1508            state_exceeds_limits,
1509            bounce,
1510        } = state.action_phase(ActionPhaseContext {
1511            received_message: None,
1512            original_balance: original_balance(&state, &compute_phase),
1513            new_state: StateInit::default(),
1514            actions: actions.clone(),
1515            compute_phase: &compute_phase,
1516            inspector: None,
1517        })?;
1518
1519        assert_eq!(action_phase, ActionPhase {
1520            success: false,
1521            valid: false,
1522            result_code: ResultCode::ActionInvalid as i32,
1523            result_arg: Some(2),
1524            action_list_hash: *actions.repr_hash(),
1525            total_actions: 5,
1526            ..empty_action_phase()
1527        });
1528        assert_eq!(action_fine, Tokens::ZERO);
1529        assert!(!state_exceeds_limits);
1530        assert!(!bounce);
1531        assert_eq!(state.total_fees, prev_total_fees);
1532        assert_eq!(state.balance, prev_balance);
1533        assert_eq!(state.end_lt, prev_end_lt);
1534        Ok(())
1535    }
1536
1537    #[test]
1538    fn strict_reserve_extra_currency() -> Result<()> {
1539        let mut params = make_default_params();
1540        params.strict_extra_currency = true;
1541        let config = make_default_config();
1542        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1543        let prev_balance = state.balance.clone();
1544
1545        let compute_phase = stub_compute_phase(OK_GAS);
1546        let prev_total_fees = state.total_fees;
1547        let prev_end_lt = state.end_lt;
1548
1549        let actions = make_action_list([OutAction::ReserveCurrency {
1550            mode: ReserveCurrencyFlags::empty(),
1551            value: CurrencyCollection {
1552                tokens: Tokens::ZERO,
1553                other: BTreeMap::from_iter([(123u32, VarUint248::new(10))]).try_into()?,
1554            },
1555        }]);
1556
1557        let ActionPhaseFull {
1558            action_phase,
1559            action_fine,
1560            state_exceeds_limits,
1561            bounce,
1562        } = state.action_phase(ActionPhaseContext {
1563            received_message: None,
1564            original_balance: original_balance(&state, &compute_phase),
1565            new_state: StateInit::default(),
1566            actions: actions.clone(),
1567            compute_phase: &compute_phase,
1568            inspector: None,
1569        })?;
1570
1571        assert_eq!(action_phase, ActionPhase {
1572            success: false,
1573            valid: true,
1574            result_code: ResultCode::ActionInvalid as i32,
1575            result_arg: Some(0),
1576            action_list_hash: *actions.repr_hash(),
1577            total_actions: 1,
1578            ..empty_action_phase()
1579        });
1580        assert_eq!(action_fine, Tokens::ZERO);
1581        assert!(!state_exceeds_limits);
1582        assert!(!bounce);
1583        assert_eq!(state.total_fees, prev_total_fees);
1584        assert_eq!(state.balance, prev_balance);
1585        assert_eq!(state.end_lt, prev_end_lt);
1586        Ok(())
1587    }
1588
1589    #[test]
1590    fn send_single_message() -> Result<()> {
1591        let params = make_default_params();
1592        let config = make_default_config();
1593        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1594
1595        let compute_phase = stub_compute_phase(OK_GAS);
1596        let prev_total_fees = state.total_fees;
1597        let prev_balance = state.balance.clone();
1598        let prev_end_lt = state.end_lt;
1599
1600        let msg_value = Tokens::new(500_000_000);
1601
1602        let actions = make_action_list([OutAction::SendMsg {
1603            mode: SendMsgFlags::empty(),
1604            out_msg: make_relaxed_message(
1605                RelaxedIntMsgInfo {
1606                    dst: STUB_ADDR.into(),
1607                    value: msg_value.into(),
1608                    ..Default::default()
1609                },
1610                None,
1611                None,
1612            ),
1613        }]);
1614
1615        let ActionPhaseFull {
1616            action_phase,
1617            action_fine,
1618            state_exceeds_limits,
1619            bounce,
1620        } = state.action_phase(ActionPhaseContext {
1621            received_message: None,
1622            original_balance: original_balance(&state, &compute_phase),
1623            new_state: StateInit::default(),
1624            actions: actions.clone(),
1625            compute_phase: &compute_phase,
1626            inspector: None,
1627        })?;
1628
1629        assert_eq!(action_fine, Tokens::ZERO);
1630        assert!(!state_exceeds_limits);
1631        assert!(!bounce);
1632
1633        assert_eq!(state.out_msgs.len(), 1);
1634        assert_eq!(state.end_lt, prev_end_lt + 1);
1635        let last_msg = state.out_msgs.last().unwrap();
1636
1637        let msg_info = {
1638            let msg = last_msg.load()?;
1639            assert!(msg.init.is_none());
1640            assert_eq!(msg.body, Default::default());
1641            match msg.info {
1642                MsgInfo::Int(info) => info,
1643                e => panic!("unexpected msg info {e:?}"),
1644            }
1645        };
1646        assert_eq!(msg_info.src, STUB_ADDR.into());
1647        assert_eq!(msg_info.dst, STUB_ADDR.into());
1648        assert!(msg_info.ihr_disabled);
1649        assert!(!msg_info.bounce);
1650        assert!(!msg_info.bounced);
1651        assert_eq!(msg_info.created_at, params.block_unixtime);
1652        assert_eq!(msg_info.created_lt, prev_end_lt);
1653
1654        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1655        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1656
1657        assert_eq!(msg_info.value, (msg_value - expected_fwd_fees).into());
1658        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1659        assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1660
1661        assert_eq!(action_phase, ActionPhase {
1662            total_fwd_fees: Some(expected_fwd_fees),
1663            total_action_fees: Some(expected_first_frac),
1664            total_actions: 1,
1665            messages_created: 1,
1666            action_list_hash: *actions.repr_hash(),
1667            total_message_size: compute_full_stats(last_msg, &params),
1668            ..empty_action_phase()
1669        });
1670
1671        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1672        assert_eq!(state.balance.other, prev_balance.other);
1673        assert_eq!(state.balance.tokens, prev_balance.tokens - msg_value);
1674
1675        Ok(())
1676    }
1677
1678    #[test]
1679    fn send_all_balance() -> Result<()> {
1680        let params = make_default_params();
1681        let config = make_default_config();
1682        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1683
1684        let compute_phase = stub_compute_phase(OK_GAS);
1685        let prev_total_fees = state.total_fees;
1686        let prev_balance = state.balance.clone();
1687        let prev_end_lt = state.end_lt;
1688
1689        let actions = make_action_list([OutAction::SendMsg {
1690            mode: SendMsgFlags::ALL_BALANCE,
1691            out_msg: make_relaxed_message(
1692                RelaxedIntMsgInfo {
1693                    dst: STUB_ADDR.into(),
1694                    value: CurrencyCollection::ZERO,
1695                    ..Default::default()
1696                },
1697                None,
1698                None,
1699            ),
1700        }]);
1701
1702        let ActionPhaseFull {
1703            action_phase,
1704            action_fine,
1705            state_exceeds_limits,
1706            bounce,
1707        } = state.action_phase(ActionPhaseContext {
1708            received_message: None,
1709            original_balance: original_balance(&state, &compute_phase),
1710            new_state: StateInit::default(),
1711            actions: actions.clone(),
1712            compute_phase: &compute_phase,
1713            inspector: None,
1714        })?;
1715
1716        assert_eq!(action_fine, Tokens::ZERO);
1717        assert!(!state_exceeds_limits);
1718        assert!(!bounce);
1719
1720        assert_eq!(state.out_msgs.len(), 1);
1721        assert_eq!(state.end_lt, prev_end_lt + 1);
1722        let last_msg = state.out_msgs.last().unwrap();
1723
1724        let msg_info = {
1725            let msg = last_msg.load()?;
1726            assert!(msg.init.is_none());
1727            assert_eq!(msg.body, Default::default());
1728            match msg.info {
1729                MsgInfo::Int(info) => info,
1730                e => panic!("unexpected msg info {e:?}"),
1731            }
1732        };
1733        assert_eq!(msg_info.src, STUB_ADDR.into());
1734        assert_eq!(msg_info.dst, STUB_ADDR.into());
1735        assert!(msg_info.ihr_disabled);
1736        assert!(!msg_info.bounce);
1737        assert!(!msg_info.bounced);
1738        assert_eq!(msg_info.created_at, params.block_unixtime);
1739        assert_eq!(msg_info.created_lt, prev_end_lt);
1740
1741        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1742        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1743
1744        assert_eq!(
1745            msg_info.value,
1746            (prev_balance.tokens - expected_fwd_fees).into()
1747        );
1748        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1749        assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1750
1751        assert_eq!(action_phase, ActionPhase {
1752            total_fwd_fees: Some(expected_fwd_fees),
1753            total_action_fees: Some(expected_first_frac),
1754            total_actions: 1,
1755            messages_created: 1,
1756            action_list_hash: *actions.repr_hash(),
1757            total_message_size: compute_full_stats(last_msg, &params),
1758            ..empty_action_phase()
1759        });
1760
1761        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1762        assert_eq!(state.balance, CurrencyCollection::ZERO);
1763
1764        Ok(())
1765    }
1766
1767    #[test]
1768    fn strict_send_all_balance() -> Result<()> {
1769        let mut params = make_default_params();
1770        params.strict_extra_currency = true;
1771        let config = make_default_config();
1772
1773        let mut state =
1774            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, CurrencyCollection {
1775                tokens: OK_BALANCE,
1776                other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1777            });
1778
1779        let compute_phase = stub_compute_phase(OK_GAS);
1780        let prev_total_fees = state.total_fees;
1781        let prev_balance = state.balance.clone();
1782        let prev_end_lt = state.end_lt;
1783
1784        let sent_value = CurrencyCollection {
1785            tokens: Tokens::ZERO,
1786            other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1787        };
1788
1789        let actions = make_action_list([OutAction::SendMsg {
1790            mode: SendMsgFlags::ALL_BALANCE,
1791            out_msg: make_relaxed_message(
1792                RelaxedIntMsgInfo {
1793                    dst: STUB_ADDR.into(),
1794                    value: sent_value.clone(),
1795                    ..Default::default()
1796                },
1797                None,
1798                None,
1799            ),
1800        }]);
1801
1802        let ActionPhaseFull {
1803            action_phase,
1804            action_fine,
1805            state_exceeds_limits,
1806            bounce,
1807        } = state.action_phase(ActionPhaseContext {
1808            received_message: None,
1809            original_balance: original_balance(&state, &compute_phase),
1810            new_state: StateInit::default(),
1811            actions: actions.clone(),
1812            compute_phase: &compute_phase,
1813            inspector: None,
1814        })?;
1815
1816        assert_eq!(action_fine, Tokens::ZERO);
1817        assert!(!state_exceeds_limits);
1818        assert!(!bounce);
1819
1820        assert_eq!(state.out_msgs.len(), 1);
1821        assert_eq!(state.end_lt, prev_end_lt + 1);
1822        let last_msg = state.out_msgs.last().unwrap();
1823
1824        let msg_info = {
1825            let msg = last_msg.load()?;
1826            assert!(msg.init.is_none());
1827            assert_eq!(msg.body, Default::default());
1828            match msg.info {
1829                MsgInfo::Int(info) => info,
1830                e => panic!("unexpected msg info {e:?}"),
1831            }
1832        };
1833        assert_eq!(msg_info.src, STUB_ADDR.into());
1834        assert_eq!(msg_info.dst, STUB_ADDR.into());
1835        assert!(msg_info.ihr_disabled);
1836        assert!(!msg_info.bounce);
1837        assert!(!msg_info.bounced);
1838        assert_eq!(msg_info.created_at, params.block_unixtime);
1839        assert_eq!(msg_info.created_lt, prev_end_lt);
1840
1841        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1842        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1843
1844        assert_eq!(msg_info.value, CurrencyCollection {
1845            tokens: prev_balance.tokens - expected_fwd_fees,
1846            other: sent_value.other.clone(),
1847        });
1848        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1849        assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
1850
1851        assert_eq!(action_phase, ActionPhase {
1852            total_fwd_fees: Some(expected_fwd_fees),
1853            total_action_fees: Some(expected_first_frac),
1854            total_actions: 1,
1855            messages_created: 1,
1856            action_list_hash: *actions.repr_hash(),
1857            total_message_size: compute_full_stats(last_msg, &params),
1858            ..empty_action_phase()
1859        });
1860
1861        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1862        assert_eq!(state.balance.tokens, Tokens::ZERO);
1863        assert_eq!(
1864            state.balance.other,
1865            prev_balance.other.checked_sub(&sent_value.other)?
1866        );
1867
1868        Ok(())
1869    }
1870
1871    #[test]
1872    fn strict_send_all_balance_destroy() -> Result<()> {
1873        struct TestCase {
1874            balance: CurrencyCollection,
1875            to_send: CurrencyCollection,
1876            expected_end_status: AccountStatus,
1877        }
1878
1879        let mut params = make_default_params();
1880        params.strict_extra_currency = true;
1881        let config = make_default_config();
1882
1883        for TestCase {
1884            balance,
1885            to_send,
1886            expected_end_status,
1887        } in [
1888            // Simple send all + delete
1889            TestCase {
1890                balance: OK_BALANCE.into(),
1891                to_send: CurrencyCollection::ZERO,
1892                expected_end_status: AccountStatus::NotExists,
1893            },
1894            // Simple send all but account has some extra currencies
1895            TestCase {
1896                balance: CurrencyCollection {
1897                    tokens: OK_BALANCE,
1898                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1899                },
1900                to_send: CurrencyCollection::ZERO,
1901                expected_end_status: AccountStatus::Uninit,
1902            },
1903            // Simple send all but account has some extra currencies
1904            TestCase {
1905                balance: CurrencyCollection {
1906                    tokens: OK_BALANCE,
1907                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1908                },
1909                to_send: CurrencyCollection::ZERO,
1910                expected_end_status: AccountStatus::Uninit,
1911            },
1912            // Send all native + some extra but account still has some more extra currencies
1913            TestCase {
1914                balance: CurrencyCollection {
1915                    tokens: OK_BALANCE,
1916                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1917                },
1918                to_send: CurrencyCollection {
1919                    tokens: OK_BALANCE,
1920                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1921                },
1922                expected_end_status: AccountStatus::Uninit,
1923            },
1924            // Send all native and all extra
1925            TestCase {
1926                balance: CurrencyCollection {
1927                    tokens: OK_BALANCE,
1928                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1929                },
1930                to_send: CurrencyCollection {
1931                    tokens: OK_BALANCE,
1932                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1933                },
1934                expected_end_status: AccountStatus::NotExists,
1935            },
1936        ] {
1937            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, balance);
1938
1939            let compute_phase = stub_compute_phase(OK_GAS);
1940            let prev_total_fees = state.total_fees;
1941            let prev_balance = state.balance.clone();
1942            let prev_end_lt = state.end_lt;
1943
1944            let actions = make_action_list([OutAction::SendMsg {
1945                mode: SendMsgFlags::ALL_BALANCE | SendMsgFlags::DELETE_IF_EMPTY,
1946                out_msg: make_relaxed_message(
1947                    RelaxedIntMsgInfo {
1948                        dst: STUB_ADDR.into(),
1949                        value: to_send.clone(),
1950                        ..Default::default()
1951                    },
1952                    None,
1953                    None,
1954                ),
1955            }]);
1956
1957            let ActionPhaseFull {
1958                action_phase,
1959                action_fine,
1960                state_exceeds_limits,
1961                bounce,
1962            } = state.action_phase(ActionPhaseContext {
1963                received_message: None,
1964                original_balance: original_balance(&state, &compute_phase),
1965                new_state: StateInit::default(),
1966                actions: actions.clone(),
1967                compute_phase: &compute_phase,
1968                inspector: None,
1969            })?;
1970
1971            assert_eq!(action_fine, Tokens::ZERO);
1972            assert!(!state_exceeds_limits);
1973            assert!(!bounce);
1974
1975            assert_eq!(state.end_status, expected_end_status);
1976            assert_eq!(state.out_msgs.len(), 1);
1977            assert_eq!(state.end_lt, prev_end_lt + 1);
1978            let last_msg = state.out_msgs.last().unwrap();
1979
1980            let msg_info = {
1981                let msg = last_msg.load()?;
1982                assert!(msg.init.is_none());
1983                assert_eq!(msg.body, Default::default());
1984                match msg.info {
1985                    MsgInfo::Int(info) => info,
1986                    e => panic!("unexpected msg info {e:?}"),
1987                }
1988            };
1989            assert_eq!(msg_info.src, STUB_ADDR.into());
1990            assert_eq!(msg_info.dst, STUB_ADDR.into());
1991            assert!(msg_info.ihr_disabled);
1992            assert!(!msg_info.bounce);
1993            assert!(!msg_info.bounced);
1994            assert_eq!(msg_info.created_at, params.block_unixtime);
1995            assert_eq!(msg_info.created_lt, prev_end_lt);
1996
1997            let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1998            let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1999
2000            assert_eq!(msg_info.value, CurrencyCollection {
2001                tokens: prev_balance.tokens - expected_fwd_fees,
2002                other: to_send.other.clone(),
2003            });
2004            assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2005            assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2006
2007            assert_eq!(action_phase, ActionPhase {
2008                total_fwd_fees: Some(expected_fwd_fees),
2009                total_action_fees: Some(expected_first_frac),
2010                total_actions: 1,
2011                messages_created: 1,
2012                action_list_hash: *actions.repr_hash(),
2013                total_message_size: compute_full_stats(last_msg, &params),
2014                status_change: AccountStatusChange::Deleted,
2015                ..empty_action_phase()
2016            });
2017
2018            assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2019            assert_eq!(state.balance.tokens, Tokens::ZERO);
2020            assert_eq!(
2021                state.balance.other,
2022                prev_balance.other.checked_sub(&to_send.other)?
2023            );
2024        }
2025
2026        Ok(())
2027    }
2028
2029    #[test]
2030    fn send_all_not_reserved() -> Result<()> {
2031        let params = make_default_params();
2032        let config = make_default_config();
2033        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2034
2035        let compute_phase = stub_compute_phase(OK_GAS);
2036        let prev_total_fees = state.total_fees;
2037        let prev_end_lt = state.end_lt;
2038
2039        let expected_balance = CurrencyCollection::from(state.balance.tokens / 4);
2040        let actions = make_action_list([
2041            OutAction::ReserveCurrency {
2042                mode: ReserveCurrencyFlags::empty(),
2043                value: expected_balance.clone(),
2044            },
2045            OutAction::SendMsg {
2046                mode: SendMsgFlags::ALL_BALANCE,
2047                out_msg: make_relaxed_message(
2048                    RelaxedIntMsgInfo {
2049                        dst: STUB_ADDR.into(),
2050                        value: CurrencyCollection::ZERO,
2051                        ..Default::default()
2052                    },
2053                    None,
2054                    None,
2055                ),
2056            },
2057        ]);
2058
2059        let ActionPhaseFull {
2060            action_phase,
2061            action_fine,
2062            state_exceeds_limits,
2063            bounce,
2064        } = state.action_phase(ActionPhaseContext {
2065            received_message: None,
2066            original_balance: original_balance(&state, &compute_phase),
2067            new_state: StateInit::default(),
2068            actions: actions.clone(),
2069            compute_phase: &compute_phase,
2070            inspector: None,
2071        })?;
2072
2073        assert_eq!(state.out_msgs.len(), 1);
2074        assert_eq!(state.end_lt, prev_end_lt + 1);
2075        let last_msg = state.out_msgs.last().unwrap();
2076
2077        let msg_info = {
2078            let msg = last_msg.load()?;
2079            assert!(msg.init.is_none());
2080            assert_eq!(msg.body, Default::default());
2081            match msg.info {
2082                MsgInfo::Int(info) => info,
2083                e => panic!("unexpected msg info {e:?}"),
2084            }
2085        };
2086        assert_eq!(msg_info.src, STUB_ADDR.into());
2087        assert_eq!(msg_info.dst, STUB_ADDR.into());
2088        assert!(msg_info.ihr_disabled);
2089        assert!(!msg_info.bounce);
2090        assert!(!msg_info.bounced);
2091        assert_eq!(msg_info.created_at, params.block_unixtime);
2092        assert_eq!(msg_info.created_lt, prev_end_lt);
2093
2094        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2095        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2096
2097        assert_eq!(
2098            msg_info.value,
2099            (OK_BALANCE * 3 / 4 - expected_fwd_fees).into()
2100        );
2101        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2102        assert_eq!(msg_info.ihr_fee, Tokens::ZERO);
2103
2104        assert_eq!(action_phase, ActionPhase {
2105            total_fwd_fees: Some(expected_fwd_fees),
2106            total_action_fees: Some(expected_first_frac),
2107            total_actions: 2,
2108            messages_created: 1,
2109            special_actions: 1,
2110            action_list_hash: *actions.repr_hash(),
2111            total_message_size: compute_full_stats(last_msg, &params),
2112            ..empty_action_phase()
2113        });
2114        assert_eq!(action_fine, Tokens::ZERO);
2115        assert!(!state_exceeds_limits);
2116        assert!(!bounce);
2117
2118        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2119        assert_eq!(state.balance, expected_balance);
2120        Ok(())
2121    }
2122
2123    #[test]
2124    fn set_code() -> Result<()> {
2125        let params = make_default_params();
2126        let config = make_default_config();
2127
2128        let orig_data = CellBuilder::build_from(u32::MIN)?;
2129        let final_data = CellBuilder::build_from(u32::MAX)?;
2130
2131        let temp_code = Boc::decode(tvmasm!("NOP NOP"))?;
2132        let final_code = Boc::decode(tvmasm!("NOP"))?;
2133
2134        let mut state = ExecutorState::new_active(
2135            &params,
2136            &config,
2137            &STUB_ADDR,
2138            OK_BALANCE,
2139            orig_data,
2140            tvmasm!("ACCEPT"),
2141        );
2142
2143        let compute_phase = stub_compute_phase(OK_GAS);
2144        let prev_total_fees = state.total_fees;
2145        let prev_balance = state.balance.clone();
2146
2147        let actions = make_action_list([
2148            OutAction::SetCode {
2149                new_code: temp_code,
2150            },
2151            OutAction::SetCode {
2152                new_code: final_code.clone(),
2153            },
2154        ]);
2155
2156        let AccountState::Active(mut new_state) = state.state.clone() else {
2157            panic!("unexpected account state");
2158        };
2159        new_state.data = Some(final_data.clone());
2160
2161        let ActionPhaseFull {
2162            action_phase,
2163            action_fine,
2164            state_exceeds_limits,
2165            bounce,
2166        } = state.action_phase(ActionPhaseContext {
2167            received_message: None,
2168            original_balance: original_balance(&state, &compute_phase),
2169            new_state,
2170            actions: actions.clone(),
2171            compute_phase: &compute_phase,
2172            inspector: None,
2173        })?;
2174
2175        assert_eq!(action_phase, ActionPhase {
2176            total_actions: 2,
2177            special_actions: 2,
2178            action_list_hash: *actions.repr_hash(),
2179            ..empty_action_phase()
2180        });
2181        assert_eq!(action_fine, Tokens::ZERO);
2182        assert!(!state_exceeds_limits);
2183        assert!(!bounce);
2184        assert_eq!(state.end_status, AccountStatus::Active);
2185        assert_eq!(
2186            state.state,
2187            AccountState::Active(StateInit {
2188                code: Some(final_code),
2189                data: Some(final_data),
2190                ..Default::default()
2191            })
2192        );
2193        assert_eq!(state.total_fees, prev_total_fees);
2194        assert_eq!(state.balance, prev_balance);
2195        Ok(())
2196    }
2197
2198    #[test]
2199    fn invalid_dst_addr() -> Result<()> {
2200        let params = make_default_params();
2201        let config = make_default_config();
2202
2203        let targets = [
2204            // Unknown workchain.
2205            IntAddr::Std(StdAddr::new(123, HashBytes::ZERO)),
2206            // With anycast.
2207            IntAddr::Std({
2208                let mut addr = STUB_ADDR;
2209                let mut b = CellBuilder::new();
2210                b.store_u16(0xaabb)?;
2211                addr.anycast = Some(Box::new(Anycast::from_slice(&b.as_data_slice())?));
2212                addr
2213            }),
2214            // Var addr.
2215            IntAddr::Var(VarAddr {
2216                anycast: None,
2217                address_len: Uint9::new(80),
2218                workchain: 0,
2219                address: vec![0; 10],
2220            }),
2221        ];
2222
2223        for dst in targets {
2224            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2225
2226            let compute_phase = stub_compute_phase(OK_GAS);
2227            let prev_total_fees = state.total_fees;
2228            let prev_balance = state.balance.clone();
2229            let prev_end_lt = state.end_lt;
2230
2231            let actions = make_action_list([OutAction::SendMsg {
2232                mode: SendMsgFlags::ALL_BALANCE,
2233                out_msg: make_relaxed_message(
2234                    RelaxedIntMsgInfo {
2235                        dst,
2236                        ..Default::default()
2237                    },
2238                    None,
2239                    None,
2240                ),
2241            }]);
2242
2243            let ActionPhaseFull {
2244                action_phase,
2245                action_fine,
2246                state_exceeds_limits,
2247                bounce,
2248            } = state.action_phase(ActionPhaseContext {
2249                received_message: None,
2250                original_balance: original_balance(&state, &compute_phase),
2251                new_state: StateInit::default(),
2252                actions: actions.clone(),
2253                compute_phase: &compute_phase,
2254                inspector: None,
2255            })?;
2256
2257            assert_eq!(action_phase, ActionPhase {
2258                success: false,
2259                total_actions: 1,
2260                messages_created: 0,
2261                result_code: ResultCode::InvalidDstAddr as _,
2262                result_arg: Some(0),
2263                action_list_hash: *actions.repr_hash(),
2264                ..empty_action_phase()
2265            });
2266            assert_eq!(action_fine, Tokens::ZERO);
2267            assert!(!state_exceeds_limits);
2268            assert!(!bounce);
2269
2270            assert!(state.out_msgs.is_empty());
2271            assert_eq!(state.end_lt, prev_end_lt);
2272
2273            assert_eq!(state.total_fees, prev_total_fees);
2274            assert_eq!(state.balance, prev_balance);
2275        }
2276        Ok(())
2277    }
2278
2279    #[test]
2280    fn cant_pay_fwd_fee() -> Result<()> {
2281        let params = make_default_params();
2282        let config = make_default_config();
2283        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(50000));
2284
2285        let compute_phase = stub_compute_phase(OK_GAS);
2286        let prev_balance = state.balance.clone();
2287        let prev_total_fee = state.total_fees;
2288        let prev_end_lt = state.end_lt;
2289
2290        let actions = make_action_list([OutAction::SendMsg {
2291            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2292            out_msg: make_relaxed_message(
2293                RelaxedIntMsgInfo {
2294                    value: CurrencyCollection::ZERO,
2295                    dst: STUB_ADDR.into(),
2296                    ..Default::default()
2297                },
2298                None,
2299                Some({
2300                    let mut b = CellBuilder::new();
2301                    b.store_reference(Cell::empty_cell())?;
2302                    b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
2303                    b
2304                }),
2305            ),
2306        }]);
2307
2308        let ActionPhaseFull {
2309            action_phase,
2310            action_fine,
2311            state_exceeds_limits,
2312            bounce,
2313        } = state.action_phase(ActionPhaseContext {
2314            received_message: None,
2315            original_balance: original_balance(&state, &compute_phase),
2316            new_state: StateInit::default(),
2317            actions: actions.clone(),
2318            compute_phase: &compute_phase,
2319            inspector: None,
2320        })?;
2321
2322        assert_eq!(action_phase, ActionPhase {
2323            success: false,
2324            no_funds: true,
2325            result_code: ResultCode::NotEnoughBalance as _,
2326            result_arg: Some(0),
2327            total_actions: 1,
2328            total_action_fees: Some(prev_balance.tokens),
2329            action_list_hash: *actions.repr_hash(),
2330            ..empty_action_phase()
2331        });
2332        assert_eq!(action_fine, prev_balance.tokens);
2333        assert!(!state_exceeds_limits);
2334        assert!(!bounce);
2335
2336        assert_eq!(state.balance, CurrencyCollection::ZERO);
2337        assert_eq!(
2338            state.total_fees,
2339            prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2340        );
2341        assert!(state.out_msgs.is_empty());
2342        assert_eq!(state.end_lt, prev_end_lt);
2343        Ok(())
2344    }
2345
2346    #[test]
2347    fn rewrite_message() -> Result<()> {
2348        let params = make_default_params();
2349        let config = make_default_config();
2350        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2351
2352        let compute_phase = stub_compute_phase(OK_GAS);
2353        let prev_balance = state.balance.clone();
2354        let prev_total_fee = state.total_fees;
2355        let prev_end_lt = state.end_lt;
2356
2357        let msg_body = {
2358            let mut b = CellBuilder::new();
2359            b.store_zeros(600)?;
2360            b.store_reference(Cell::empty_cell())?;
2361            b
2362        };
2363
2364        let actions = make_action_list([OutAction::SendMsg {
2365            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2366            out_msg: make_relaxed_message(
2367                RelaxedIntMsgInfo {
2368                    value: CurrencyCollection::ZERO,
2369                    dst: STUB_ADDR.into(),
2370                    ..Default::default()
2371                },
2372                None,
2373                Some(msg_body.clone()),
2374            ),
2375        }]);
2376
2377        let ActionPhaseFull {
2378            action_phase,
2379            action_fine,
2380            state_exceeds_limits,
2381            bounce,
2382        } = state.action_phase(ActionPhaseContext {
2383            received_message: None,
2384            original_balance: original_balance(&state, &compute_phase),
2385            new_state: StateInit::default(),
2386            actions: actions.clone(),
2387            compute_phase: &compute_phase,
2388            inspector: None,
2389        })?;
2390
2391        assert_eq!(state.out_msgs.len(), 1);
2392        let last_msg = state.out_msgs.last().unwrap();
2393        let msg = last_msg.load()?;
2394        assert_eq!(
2395            msg.layout,
2396            Some(MessageLayout {
2397                init_to_cell: false,
2398                body_to_cell: true,
2399            })
2400        );
2401        assert_eq!(msg.body.1, msg_body.build()?);
2402
2403        let MsgInfo::Int(info) = msg.info else {
2404            panic!("expected an internal message");
2405        };
2406
2407        let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
2408            bit_count: 600,
2409            cell_count: 2,
2410        });
2411        let first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2412
2413        assert_eq!(action_phase, ActionPhase {
2414            total_actions: 1,
2415            messages_created: 1,
2416            total_fwd_fees: Some(expected_fwd_fees),
2417            total_action_fees: Some(first_frac),
2418            action_list_hash: *actions.repr_hash(),
2419            total_message_size: compute_full_stats(last_msg, &params),
2420            ..empty_action_phase()
2421        });
2422        assert_eq!(action_fine, Tokens::ZERO);
2423        assert!(!state_exceeds_limits);
2424        assert!(!bounce);
2425
2426        assert_eq!(state.end_lt, prev_end_lt + 1);
2427        assert_eq!(
2428            state.total_fees,
2429            prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2430        );
2431        assert_eq!(state.balance.other, prev_balance.other);
2432        assert_eq!(
2433            state.balance.tokens,
2434            prev_balance.tokens - info.value.tokens - expected_fwd_fees
2435        );
2436        Ok(())
2437    }
2438
2439    #[test]
2440    fn change_lib() -> Result<()> {
2441        struct TestCase {
2442            libraries: Dict<HashBytes, SimpleLib>,
2443            target: Dict<HashBytes, SimpleLib>,
2444            changes: Vec<(ChangeLibraryMode, LibRef)>,
2445            diff: Vec<PublicLibraryChange>,
2446        }
2447
2448        fn make_lib(id: u32) -> Cell {
2449            CellBuilder::build_from(id).unwrap()
2450        }
2451
2452        fn make_lib_hash(id: u32) -> HashBytes {
2453            *CellBuilder::build_from(id).unwrap().repr_hash()
2454        }
2455
2456        fn make_libs<I>(items: I) -> Dict<HashBytes, SimpleLib>
2457        where
2458            I: IntoIterator<Item = (u32, bool)>,
2459        {
2460            let mut items = items
2461                .into_iter()
2462                .map(|(id, public)| {
2463                    let root = make_lib(id);
2464                    (*root.repr_hash(), SimpleLib { root, public })
2465                })
2466                .collect::<Vec<_>>();
2467            items.sort_by(|(a, _), (b, _)| a.cmp(b));
2468            Dict::try_from_sorted_slice(&items).unwrap()
2469        }
2470
2471        let params = make_default_params();
2472        let config = make_default_config();
2473
2474        let test_cases = vec![
2475            // The simplest case with no libs.
2476            TestCase {
2477                libraries: Dict::new(),
2478                target: Dict::new(),
2479                changes: Vec::new(),
2480                diff: Vec::new(),
2481            },
2482            // Add private libs.
2483            TestCase {
2484                libraries: Dict::new(),
2485                target: make_libs([(123, false)]),
2486                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2487                diff: Vec::new(),
2488            },
2489            TestCase {
2490                libraries: Dict::new(),
2491                target: make_libs([(123, false), (234, false)]),
2492                changes: vec![
2493                    (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123))),
2494                    (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(234))),
2495                ],
2496                diff: Vec::new(),
2497            },
2498            TestCase {
2499                libraries: make_libs([(123, false)]),
2500                target: make_libs([(123, false)]),
2501                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2502                diff: Vec::new(),
2503            },
2504            TestCase {
2505                libraries: make_libs([(123, false)]),
2506                target: make_libs([(123, false)]),
2507                changes: vec![(
2508                    ChangeLibraryMode::ADD_PRIVATE,
2509                    LibRef::Hash(make_lib_hash(123)),
2510                )],
2511                diff: Vec::new(),
2512            },
2513            // Add public libs.
2514            TestCase {
2515                libraries: Dict::new(),
2516                target: make_libs([(123, true)]),
2517                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2518                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2519            },
2520            TestCase {
2521                libraries: Dict::new(),
2522                target: make_libs([(123, true), (234, true)]),
2523                changes: vec![
2524                    (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123))),
2525                    (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(234))),
2526                ],
2527                diff: vec![
2528                    PublicLibraryChange::Add(make_lib(123)),
2529                    PublicLibraryChange::Add(make_lib(234)),
2530                ],
2531            },
2532            TestCase {
2533                libraries: make_libs([(123, true)]),
2534                target: make_libs([(123, true)]),
2535                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2536                diff: Vec::new(),
2537            },
2538            TestCase {
2539                libraries: make_libs([(123, true)]),
2540                target: make_libs([(123, true)]),
2541                changes: vec![(
2542                    ChangeLibraryMode::ADD_PUBLIC,
2543                    LibRef::Hash(make_lib_hash(123)),
2544                )],
2545                diff: Vec::new(),
2546            },
2547            // Remove public libs.
2548            TestCase {
2549                libraries: Dict::new(),
2550                target: Dict::new(),
2551                // Non-existing by cell.
2552                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2553                diff: Vec::new(),
2554            },
2555            TestCase {
2556                libraries: Dict::new(),
2557                target: Dict::new(),
2558                // Non-existing by hash.
2559                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2560                diff: Vec::new(),
2561            },
2562            TestCase {
2563                libraries: make_libs([(123, false)]),
2564                target: Dict::new(),
2565                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2566                diff: Vec::new(),
2567            },
2568            TestCase {
2569                libraries: make_libs([(123, false)]),
2570                target: Dict::new(),
2571                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2572                diff: Vec::new(),
2573            },
2574            TestCase {
2575                libraries: make_libs([(123, true)]),
2576                target: Dict::new(),
2577                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2578                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2579            },
2580            // Update public libs.
2581            TestCase {
2582                libraries: make_libs([(123, false)]),
2583                target: make_libs([(123, true)]),
2584                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2585                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2586            },
2587            TestCase {
2588                libraries: make_libs([(123, false)]),
2589                target: make_libs([(123, true)]),
2590                changes: vec![(
2591                    ChangeLibraryMode::ADD_PUBLIC,
2592                    LibRef::Hash(make_lib_hash(123)),
2593                )],
2594                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2595            },
2596            TestCase {
2597                libraries: make_libs([(123, true)]),
2598                target: make_libs([(123, false)]),
2599                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2600                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2601            },
2602            TestCase {
2603                libraries: make_libs([(123, true)]),
2604                target: make_libs([(123, false)]),
2605                changes: vec![(
2606                    ChangeLibraryMode::ADD_PRIVATE,
2607                    LibRef::Hash(make_lib_hash(123)),
2608                )],
2609                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2610            },
2611        ];
2612
2613        for TestCase {
2614            libraries,
2615            target,
2616            changes,
2617            diff,
2618        } in test_cases
2619        {
2620            let mut state = ExecutorState::new_active(
2621                &params,
2622                &config,
2623                &StdAddr::new(-1, HashBytes::ZERO),
2624                OK_BALANCE,
2625                Cell::empty_cell(),
2626                tvmasm!("ACCEPT"),
2627            );
2628
2629            let target_state_init = match &mut state.state {
2630                AccountState::Active(state_init) => {
2631                    state_init.libraries = libraries;
2632                    StateInit {
2633                        libraries: target,
2634                        ..state_init.clone()
2635                    }
2636                }
2637                AccountState::Uninit | AccountState::Frozen(..) => panic!("invalid initial state"),
2638            };
2639
2640            let compute_phase = stub_compute_phase(OK_GAS);
2641            let prev_total_fees = state.total_fees;
2642            let prev_balance = state.balance.clone();
2643
2644            let change_count = changes.len();
2645            let actions = make_action_list(
2646                changes
2647                    .into_iter()
2648                    .map(|(mode, lib)| OutAction::ChangeLibrary { mode, lib }),
2649            );
2650
2651            let mut inspector = ExecutorInspector::default();
2652
2653            let ActionPhaseFull {
2654                action_phase,
2655                action_fine,
2656                state_exceeds_limits,
2657                bounce,
2658            } = state.action_phase(ActionPhaseContext {
2659                received_message: None,
2660                original_balance: original_balance(&state, &compute_phase),
2661                new_state: match state.state.clone() {
2662                    AccountState::Active(state_init) => state_init,
2663                    AccountState::Uninit | AccountState::Frozen(..) => Default::default(),
2664                },
2665                actions: actions.clone(),
2666                compute_phase: &compute_phase,
2667                inspector: Some(&mut inspector),
2668            })?;
2669
2670            assert_eq!(action_phase, ActionPhase {
2671                total_actions: change_count as u16,
2672                special_actions: change_count as u16,
2673                action_list_hash: *actions.repr_hash(),
2674                ..empty_action_phase()
2675            });
2676            assert_eq!(action_fine, Tokens::ZERO);
2677            assert!(!state_exceeds_limits);
2678            assert!(!bounce);
2679            assert_eq!(state.end_status, AccountStatus::Active);
2680            assert_eq!(state.state, AccountState::Active(target_state_init));
2681            assert_eq!(state.total_fees, prev_total_fees);
2682            assert_eq!(state.balance, prev_balance);
2683            assert_eq!(inspector.public_libs_diff, diff);
2684        }
2685        Ok(())
2686    }
2687}