Skip to main content

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                && cs_parsed.is_empty()
131            {
132                // Add this action if slices contained it exclusively.
133                parsed_list.push(Some(item));
134                continue;
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        // We should only skip if at least the mode is correct.
359        let skip_invalid = mode.contains(SendMsgFlags::IGNORE_ERROR);
360        let check_skip_invalid = |e: ResultCode, ctx: &mut ActionContext<'_>| {
361            if skip_invalid {
362                ctx.action_phase.skipped_actions += 1;
363                Ok(SendMsgResult::Sent)
364            } else {
365                ctx.action_phase.result_code = e as i32;
366                Err(ActionFailed)
367            }
368        };
369
370        if mode.bits() & !MASK != 0 || mode.contains(INVALID_MASK) {
371            // - Mode has some unknown bits;
372            // - Or "ALL_BALANCE" flag was used with "WITH_REMAINING_BALANCE".
373            return check_skip_invalid(ResultCode::ActionInvalid, ctx);
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            // NOTE: Only visit all libraries on the first attempt.
391            state_init_cs = match load_state_init_as_slice(&mut cs, rewrite.is_none()) {
392                Ok(cs) => cs,
393                // Invalid StateInit leads to a skipped action.
394                Err(LoadStateInitError::BadStateInit) => {
395                    return check_skip_invalid(ResultCode::ActionInvalid, ctx);
396                }
397                // Invalid message structure leads to a failed action.
398                Err(LoadStateInitError::Other) => return Err(ActionFailed),
399            };
400            body_cs = load_body_as_slice(&mut cs)?;
401
402            if !cs.is_empty() {
403                // Any remaining data in the message slice is treated as malicious data.
404                return Err(ActionFailed);
405            }
406        }
407
408        // Apply rewrite.
409        let rewritten_state_init_cb;
410        if rewrite.is_some() {
411            if state_init_cs.size_refs() >= 2 {
412                // Move state init to cell if it is more optimal.
413                rewritten_state_init_cb = rewrite_state_init_to_cell(state_init_cs);
414                state_init_cs = rewritten_state_init_cb.as_full_slice();
415            } else {
416                // Or try to move body to cell instead.
417                rewrite = Some(MessageRewrite::BodyToCell);
418            }
419        }
420
421        let rewritten_body_cs;
422        if let Some(MessageRewrite::BodyToCell) = rewrite
423            && body_cs.size_bits() > 1
424            && !body_cs.get_bit(0).unwrap()
425        {
426            // Try to move a non-empty plain body to cell.
427            rewritten_body_cs = rewrite_body_to_cell(body_cs);
428            body_cs = rewritten_body_cs.as_full_slice();
429        }
430
431        // Check info.
432        let mut use_mc_prices = self.address.is_masterchain();
433        match &mut relaxed_info {
434            // Check internal outbound message.
435            RelaxedMsgInfo::Int(info) => {
436                // Rewrite source address.
437                if !check_rewrite_src_addr(&self.address, &mut info.src) {
438                    return check_skip_invalid(ResultCode::InvalidSrcAddr, ctx);
439                };
440
441                // Rewrite destination address.
442                if !check_rewrite_dst_addr(&self.config.workchains, &mut info.dst) {
443                    return check_skip_invalid(ResultCode::InvalidDstAddr, ctx);
444                }
445                use_mc_prices |= info.dst.is_masterchain();
446
447                // Rewrite extra currencies.
448                if self.params.strict_extra_currency {
449                    match normalize_extra_balance(
450                        std::mem::take(&mut info.value.other),
451                        MAX_MSG_EXTRA_CURRENCIES,
452                    ) {
453                        Ok(other) => info.value.other = other,
454                        Err(BalanceExtraError::InvalidDict(_)) => {
455                            return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
456                        }
457                        Err(BalanceExtraError::OutOfLimit) => {
458                            return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
459                        }
460                    }
461                }
462
463                // Reset fees.
464                info.fwd_fee = Tokens::ZERO;
465
466                // Rewrite message timings.
467                info.created_at = self.params.block_unixtime;
468                info.created_lt = ctx.end_lt;
469
470                // Clear flags.
471                info.ihr_disabled = true;
472                info.bounced = false;
473            }
474            // Check external outbound message.
475            RelaxedMsgInfo::ExtOut(info) => {
476                if mode.bits() & !EXT_MSG_MASK != 0 {
477                    // Invalid mode for an outgoing external message.
478                    return check_skip_invalid(ResultCode::ActionInvalid, ctx);
479                }
480
481                // Rewrite source address.
482                if !check_rewrite_src_addr(&self.address, &mut info.src) {
483                    return check_skip_invalid(ResultCode::InvalidSrcAddr, ctx);
484                }
485
486                // Rewrite message timings.
487                info.created_at = self.params.block_unixtime;
488                info.created_lt = ctx.end_lt;
489            }
490        };
491
492        // Compute fine per cell. Account is required to pay it for every visited cell.
493        let prices = self.config.fwd_prices(use_mc_prices);
494        let mut max_cell_count = self.config.size_limits.max_msg_cells;
495        let fine_per_cell;
496        if self.is_special {
497            fine_per_cell = 0;
498        } else {
499            fine_per_cell = (prices.cell_price >> 16) / 4;
500
501            let mut funds = ctx.remaining_balance.tokens;
502            if let RelaxedMsgInfo::Int(info) = &relaxed_info
503                && !mode.contains(SendMsgFlags::ALL_BALANCE)
504                && !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY)
505            {
506                let mut new_funds = info.value.tokens;
507
508                if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
509                    && (|| {
510                        let msg_balance_remaining = match &ctx.received_message {
511                            Some(msg) => msg.balance_remaining.tokens,
512                            None => Tokens::ZERO,
513                        };
514                        new_funds.try_add_assign(msg_balance_remaining)?;
515                        new_funds.try_sub_assign(ctx.compute_phase.gas_fees)?;
516                        new_funds.try_sub_assign(*ctx.action_fine)?;
517
518                        Ok::<_, tycho_types::error::Error>(())
519                    })()
520                    .is_err()
521                {
522                    return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
523                }
524
525                funds = std::cmp::min(funds, new_funds);
526            }
527
528            if funds < Tokens::new(max_cell_count as u128 * fine_per_cell as u128) {
529                debug_assert_ne!(fine_per_cell, 0);
530                max_cell_count = (funds.into_inner() / fine_per_cell as u128)
531                    .try_into()
532                    .unwrap_or(u32::MAX);
533            }
534        }
535
536        let collect_fine = |cells: u32, ctx: &mut ActionContext<'_>| {
537            let mut fine = Tokens::new(
538                fine_per_cell.saturating_mul(std::cmp::min(max_cell_count, cells) as u64) as _,
539            );
540            fine = std::cmp::min(fine, ctx.remaining_balance.tokens);
541            ctx.action_fine.try_add_assign(fine)?;
542            ctx.remaining_balance.try_sub_assign_tokens(fine)
543        };
544
545        // Compute size of the message.
546        let stats = 'stats: {
547            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
548                bit_count: self.config.size_limits.max_msg_bits,
549                cell_count: max_cell_count,
550            });
551
552            'valid: {
553                for cell in state_init_cs.references() {
554                    if !stats.add_cell(cell) {
555                        break 'valid;
556                    }
557                }
558
559                for cell in body_cs.references() {
560                    if !stats.add_cell(cell) {
561                        break 'valid;
562                    }
563                }
564
565                // Add EC dict when `strict` behaviour is disabled.
566                if !self.params.strict_extra_currency
567                    && let RelaxedMsgInfo::Int(int) = &relaxed_info
568                    && let Some(cell) = int.value.other.as_dict().root()
569                    && !stats.add_cell(cell.as_ref())
570                {
571                    break 'valid;
572                }
573
574                break 'stats stats.stats();
575            }
576
577            collect_fine(stats.cells, ctx)?;
578            return check_skip_invalid(ResultCode::MessageOutOfLimits, ctx);
579        };
580
581        // Make sure that `check_skip_invalid` will collect fine.
582        let check_skip_invalid = move |e: ResultCode, ctx: &mut ActionContext<'_>| {
583            collect_fine(stats.cell_count as _, ctx)?;
584            check_skip_invalid(e, ctx)
585        };
586
587        // Compute forwarding fees.
588        let fwd_fee = if self.is_special {
589            Tokens::ZERO
590        } else {
591            prices.compute_fwd_fee(stats)
592        };
593
594        // Finalize message.
595        let msg;
596        let fees_collected;
597        match &mut relaxed_info {
598            RelaxedMsgInfo::Int(info) => {
599                // Rewrite message value and compute how much will be withdwarn.
600                let value_to_pay = match ctx.rewrite_message_value(&mut info.value, mode, fwd_fee) {
601                    Ok(total_value) => total_value,
602                    Err(_) => return check_skip_invalid(ResultCode::NotEnoughBalance, ctx),
603                };
604
605                // Check if remaining balance is enough to pay `total_value`.
606                if ctx.remaining_balance.tokens < value_to_pay {
607                    return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
608                }
609
610                // Check that the number of extra currencies is whithin the limit.
611                if self.params.strict_extra_currency
612                    && !check_extra_balance(&info.value.other, MAX_MSG_EXTRA_CURRENCIES)
613                {
614                    return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
615                }
616
617                // Check if account is allowed to send authority marks.
618                if self.params.authority_marks_enabled
619                    && !self.is_marks_authority
620                    && let Some(marks) = &self.config.authority_marks
621                    && marks.has_authority_marks_in(&info.value)?
622                {
623                    return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx);
624                }
625
626                // Try to withdraw extra currencies from the remaining balance.
627                let other = match ctx.remaining_balance.other.checked_sub(&info.value.other) {
628                    Ok(other) => other,
629                    Err(_) => return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx),
630                };
631
632                // Split forwarding fee.
633                fees_collected = prices.get_first_part(fwd_fee);
634                info.fwd_fee = fwd_fee - fees_collected;
635
636                // Finalize message.
637                msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
638                    Ok(msg) => msg,
639                    Err(_) => match MessageRewrite::next(rewrite) {
640                        Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
641                        None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
642                    },
643                };
644
645                // Clear message balance if it was used.
646                if let Some(msg) = &mut ctx.received_message
647                    && (mode.contains(SendMsgFlags::ALL_BALANCE)
648                        || mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE))
649                {
650                    if self.params.strict_extra_currency {
651                        // Only native balance was used.
652                        msg.balance_remaining.tokens = Tokens::ZERO;
653                    } else {
654                        // All balance was used.
655                        msg.balance_remaining = CurrencyCollection::ZERO;
656                    }
657                }
658
659                // Update the remaining balance.
660                ctx.remaining_balance.tokens -= value_to_pay;
661                ctx.remaining_balance.other = other;
662            }
663            RelaxedMsgInfo::ExtOut(_) => {
664                // Check if the remaining balance is enough to pay forwarding fees.
665                if ctx.remaining_balance.tokens < fwd_fee {
666                    return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
667                }
668
669                // Finalize message.
670                msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
671                    Ok(msg) => msg,
672                    Err(_) => match MessageRewrite::next(rewrite) {
673                        Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
674                        None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
675                    },
676                };
677
678                // Update the remaining balance.
679                ctx.remaining_balance.tokens -= fwd_fee;
680                fees_collected = fwd_fee;
681            }
682        }
683
684        update_total_msg_stat(
685            &mut ctx.action_phase.total_message_size,
686            stats,
687            msg.bit_len(),
688        );
689
690        ctx.action_phase.messages_created += 1;
691        ctx.end_lt += 1;
692
693        ctx.out_msgs.push(msg);
694
695        *ctx.action_phase.total_action_fees.get_or_insert_default() += fees_collected;
696        *ctx.action_phase.total_fwd_fees.get_or_insert_default() += fwd_fee;
697
698        if mode.contains(DELETE_MASK) {
699            ctx.delete_account = if self.params.strict_extra_currency {
700                // Delete when native balance was used.
701                debug_assert!(ctx.remaining_balance.tokens.is_zero());
702                ctx.reserved_balance.tokens.is_zero()
703            } else {
704                // Delete when full balance was used.
705                debug_assert!(ctx.remaining_balance.is_zero());
706                ctx.reserved_balance.is_zero()
707            };
708        }
709
710        Ok(SendMsgResult::Sent)
711    }
712
713    /// `SetCode` action.
714    fn do_set_code(&self, new_code: Cell, ctx: &mut ActionContext<'_>) -> Result<(), ActionFailed> {
715        // Update context.
716        ctx.new_state.code = Some(new_code);
717        ctx.action_phase.special_actions += 1;
718
719        // Done
720        Ok(())
721    }
722
723    /// `ReserveCurrency` action.
724    fn do_reserve_currency(
725        &self,
726        mode: ReserveCurrencyFlags,
727        mut reserve: CurrencyCollection,
728        ctx: &mut ActionContext<'_>,
729    ) -> Result<(), ActionFailed> {
730        const MASK: u8 = ReserveCurrencyFlags::all().bits();
731
732        // Check and apply mode flags.
733        if mode.contains(ReserveCurrencyFlags::BOUNCE_ON_ERROR) {
734            ctx.need_bounce_on_fail = true;
735        }
736
737        if mode.bits() & !MASK != 0 {
738            // Invalid mode.
739            return Err(ActionFailed);
740        }
741
742        if self.params.strict_extra_currency && !reserve.other.is_empty() {
743            // Cannot reserve extra currencies when strict behaviour is enabled.
744            return Err(ActionFailed);
745        }
746
747        if mode.contains(ReserveCurrencyFlags::WITH_ORIGINAL_BALANCE) {
748            if mode.contains(ReserveCurrencyFlags::REVERSE) {
749                if self.params.strict_extra_currency {
750                    reserve.tokens = ctx
751                        .original_balance
752                        .tokens
753                        .checked_sub(reserve.tokens)
754                        .ok_or(ActionFailed)?;
755                } else {
756                    reserve = ctx.original_balance.checked_sub(&reserve)?;
757                }
758            } else if self.params.strict_extra_currency {
759                reserve.try_add_assign_tokens(ctx.original_balance.tokens)?;
760            } else {
761                reserve.try_add_assign(ctx.original_balance)?;
762            }
763        } else if mode.contains(ReserveCurrencyFlags::REVERSE) {
764            // Invalid mode.
765            return Err(ActionFailed);
766        }
767
768        if mode.contains(ReserveCurrencyFlags::IGNORE_ERROR) {
769            // Clamp balance.
770            reserve = reserve.checked_clamp(&ctx.remaining_balance)?;
771        }
772
773        // Reserve balance.
774        let mut new_balance = CurrencyCollection {
775            tokens: match ctx.remaining_balance.tokens.checked_sub(reserve.tokens) {
776                Some(tokens) => tokens,
777                None => {
778                    ctx.action_phase.result_code = ResultCode::NotEnoughBalance as i32;
779                    return Err(ActionFailed);
780                }
781            },
782            other: match ctx.remaining_balance.other.checked_sub(&reserve.other) {
783                Ok(other) => other,
784                Err(_) => {
785                    ctx.action_phase.result_code = ResultCode::NotEnoughExtraBalance as i32;
786                    return Err(ActionFailed);
787                }
788            },
789        };
790
791        // Always normalize reserved balance.
792        reserve.other.normalize()?;
793
794        // Apply "ALL_BUT" flag. Leave only "new_balance", reserve everything else.
795        if mode.contains(ReserveCurrencyFlags::ALL_BUT) {
796            if self.params.strict_extra_currency {
797                std::mem::swap(&mut new_balance.tokens, &mut reserve.tokens);
798            } else {
799                std::mem::swap(&mut new_balance, &mut reserve);
800            }
801        }
802
803        // Update context.
804        ctx.remaining_balance = new_balance;
805        ctx.reserved_balance.try_add_assign(&reserve)?;
806        ctx.action_phase.special_actions += 1;
807
808        // Done
809        Ok(())
810    }
811
812    /// `ChangeLibrary` action.
813    fn do_change_library(
814        &self,
815        mode: ChangeLibraryMode,
816        mut lib: LibRef,
817        ctx: &mut ActionContext<'_>,
818    ) -> Result<(), ActionFailed> {
819        // Having both "ADD_PRIVATE" and "ADD_PUBLIC" flags is invalid.
820        const INVALID_MODE: ChangeLibraryMode = ChangeLibraryMode::from_bits_retain(
821            ChangeLibraryMode::ADD_PRIVATE.bits() | ChangeLibraryMode::ADD_PUBLIC.bits(),
822        );
823
824        // Check and apply mode flags.
825        if mode.contains(ChangeLibraryMode::BOUNCE_ON_ERROR) {
826            ctx.need_bounce_on_fail = true;
827        }
828
829        if mode.contains(INVALID_MODE) {
830            return Err(ActionFailed);
831        }
832
833        let hash = match &lib {
834            LibRef::Cell(cell) => cell.repr_hash(),
835            LibRef::Hash(hash) => hash,
836        };
837
838        let is_masterchain = self.address.is_masterchain();
839
840        let add_public = mode.contains(ChangeLibraryMode::ADD_PUBLIC);
841        if add_public || mode.contains(ChangeLibraryMode::ADD_PRIVATE) {
842            // Add new library.
843            let mut was_public = None;
844            if let Ok(Some(prev)) = ctx.new_state.libraries.get(hash) {
845                if prev.public == add_public {
846                    // Do nothing if library already exists with the same `public` flag.
847                    ctx.action_phase.special_actions += 1;
848                    return Ok(());
849                } else {
850                    // If library exists allow changing its `public` flag when `LibRef::Hash` was used.
851                    was_public = Some(prev.public);
852                    lib = LibRef::Cell(prev.root);
853                }
854            }
855
856            let LibRef::Cell(root) = lib else {
857                ctx.action_phase.result_code = ResultCode::NoLibCode as i32;
858                return Err(ActionFailed);
859            };
860
861            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
862                bit_count: u32::MAX,
863                cell_count: self.config.size_limits.max_library_cells,
864            });
865            if !stats.add_cell(root.as_ref()) {
866                ctx.action_phase.result_code = ResultCode::LibOutOfLimits as i32;
867                return Err(ActionFailed);
868            }
869
870            // Add library.
871            match ctx.new_state.libraries.set(*root.repr_hash(), SimpleLib {
872                public: add_public,
873                root: root.clone(),
874            }) {
875                // Track removed public libs by an inspector for masterchain accounts.
876                Ok(_) if is_masterchain => {
877                    // Track removed or added public libs by an inspector.
878                    if let Some(diff) = &mut ctx.public_libs_diff {
879                        match (was_public, add_public) {
880                            // Add new public libraries.
881                            (None, true) | (Some(false), true) => {
882                                diff.push(PublicLibraryChange::Add(root))
883                            }
884                            // Remove public libraries
885                            (Some(true), false) => {
886                                diff.push(PublicLibraryChange::Remove(*root.repr_hash()));
887                            }
888                            // Ignore unchanged or private libraries.
889                            _ => {}
890                        }
891                    }
892                }
893                Ok(_) => {}
894                Err(_) => {
895                    ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
896                    return Err(ActionFailed);
897                }
898            }
899        } else {
900            // Remove library.
901            match ctx.new_state.libraries.remove(hash) {
902                // Track removed public libs by an inspector for masterchain accounts.
903                Ok(Some(lib)) if is_masterchain && lib.public => {
904                    if let Some(diff) = &mut ctx.public_libs_diff {
905                        diff.push(PublicLibraryChange::Remove(*hash));
906                    }
907                }
908                Ok(_) => {}
909                Err(_) => {
910                    ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
911                    return Err(ActionFailed);
912                }
913            }
914        }
915
916        // Update context.
917        ctx.action_phase.special_actions += 1;
918
919        // Done
920        Ok(())
921    }
922}
923
924struct ActionContext<'a> {
925    need_bounce_on_fail: bool,
926    strict_extra_currency: bool,
927    received_message: Option<&'a mut ReceivedMessage>,
928    original_balance: &'a CurrencyCollection,
929    remaining_balance: CurrencyCollection,
930    reserved_balance: CurrencyCollection,
931    action_fine: &'a mut Tokens,
932    new_state: &'a mut StateInit,
933    end_lt: u64,
934    out_msgs: Vec<Lazy<OwnedMessage>>,
935    delete_account: bool,
936    public_libs_diff: Option<Vec<PublicLibraryChange>>,
937
938    compute_phase: &'a ExecutedComputePhase,
939    action_phase: &'a mut ActionPhase,
940}
941
942impl ActionContext<'_> {
943    fn apply_fine_on_error(
944        &mut self,
945        balance: &mut CurrencyCollection,
946        total_fees: &mut Tokens,
947        charge_action_fees: bool,
948    ) -> Result<(), Error> {
949        // Compute the resulting action fine (it must not be greater than the account balance).
950        if charge_action_fees {
951            self.action_fine
952                .try_add_assign(self.action_phase.total_action_fees.unwrap_or_default())?;
953        }
954
955        // Reset forwarding fee since no messages were actually sent.
956        // NOTE: This behaviour is not present in the reference implementation
957        //       but it seems to be more correct.
958        self.action_phase.total_fwd_fees = None;
959
960        // Charge the account balance for the action fine.
961        self.action_phase.total_action_fees = Some(*self.action_fine).filter(|t| !t.is_zero());
962
963        balance.tokens.try_sub_assign(*self.action_fine)?;
964        total_fees.try_add_assign(*self.action_fine)
965    }
966
967    fn rewrite_message_value(
968        &self,
969        value: &mut CurrencyCollection,
970        mut mode: SendMsgFlags,
971        fees_total: Tokens,
972    ) -> Result<Tokens, ActionFailed> {
973        // Update `value` based on flags.
974        if mode.contains(SendMsgFlags::ALL_BALANCE) {
975            // Attach all remaining balance to the message.
976            if self.strict_extra_currency {
977                // Attach only native balance when strict behaviour is enabled.
978                value.tokens = self.remaining_balance.tokens;
979            } else {
980                // Attach both native and extra currencies otherwise.
981                *value = self.remaining_balance.clone();
982            };
983            // Pay fees from the attached value.
984            mode.remove(SendMsgFlags::PAY_FEE_SEPARATELY);
985        } else if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE) {
986            if let Some(msg) = &self.received_message {
987                // Attach all remaining balance of the inbound message.
988                // (in addition to the original value).
989                if self.strict_extra_currency {
990                    // Attach only native balance when strict behaviour is enabled.
991                    value.try_add_assign_tokens(msg.balance_remaining.tokens)?;
992                } else {
993                    // Attach both native and extra currencies otherwise.
994                    value.try_add_assign(&msg.balance_remaining)?;
995                }
996            }
997
998            if !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
999                // Try to exclude fees from the attached value.
1000                value.try_sub_assign_tokens(*self.action_fine)?;
1001                value.try_sub_assign_tokens(self.compute_phase.gas_fees)?;
1002            }
1003        }
1004
1005        // Compute `value + fees`.
1006        let mut total = value.tokens;
1007        if mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
1008            total.try_add_assign(fees_total)?;
1009        } else {
1010            value.tokens.try_sub_assign(fees_total)?;
1011        }
1012
1013        // Done
1014        Ok(total)
1015    }
1016}
1017
1018struct ActionFailed;
1019
1020impl From<anyhow::Error> for ActionFailed {
1021    #[inline]
1022    fn from(_: anyhow::Error) -> Self {
1023        Self
1024    }
1025}
1026
1027impl From<Error> for ActionFailed {
1028    #[inline]
1029    fn from(_: Error) -> Self {
1030        Self
1031    }
1032}
1033
1034#[derive(Debug, Clone, Copy)]
1035enum SendMsgResult {
1036    Sent,
1037    Rewrite(MessageRewrite),
1038}
1039
1040#[derive(Debug, Clone, Copy)]
1041enum MessageRewrite {
1042    StateInitToCell,
1043    BodyToCell,
1044}
1045
1046impl MessageRewrite {
1047    pub fn next(rewrite: Option<Self>) -> Option<Self> {
1048        match rewrite {
1049            None => Some(Self::StateInitToCell),
1050            Some(Self::StateInitToCell) => Some(Self::BodyToCell),
1051            Some(Self::BodyToCell) => None,
1052        }
1053    }
1054}
1055
1056fn load_state_init_as_slice<'a>(
1057    cs: &mut CellSlice<'a>,
1058    check_libs: bool,
1059) -> Result<CellSlice<'a>, LoadStateInitError> {
1060    let mut res_cs = *cs;
1061
1062    // (Maybe (Either StateInit ^StateInit))
1063    if cs.load_bit()? {
1064        let libs = if cs.load_bit()? {
1065            // right$1 ^StateInit
1066            let state_root = cs.load_reference()?;
1067            if state_root.is_exotic() {
1068                // Only ordinary cells are allowed as state init.
1069                return Err(LoadStateInitError::BadStateInit);
1070            }
1071
1072            // Validate `StateInit` by reading.
1073            let mut cs = state_root.as_slice_allow_exotic();
1074            let Ok(state) = StateInit::load_from(&mut cs) else {
1075                return Err(LoadStateInitError::BadStateInit);
1076            };
1077
1078            // And ensure that nothing more was left.
1079            if !cs.is_empty() {
1080                return Err(LoadStateInitError::BadStateInit);
1081            }
1082
1083            state.libraries
1084        } else {
1085            // left$0 StateInit
1086
1087            // Validate `StateInit` by reading.
1088            match StateInit::load_from(cs) {
1089                Ok(state) => state.libraries,
1090                Err(_) => return Err(LoadStateInitError::BadStateInit),
1091            }
1092        };
1093
1094        if check_libs {
1095            for item in libs.iter() {
1096                let Ok((key, value)) = item else {
1097                    return Err(LoadStateInitError::BadStateInit);
1098                };
1099                if &key != value.root.repr_hash() {
1100                    return Err(LoadStateInitError::BadStateInit);
1101                }
1102            }
1103        }
1104    }
1105
1106    res_cs.skip_last(cs.size_bits(), cs.size_refs())?;
1107    Ok(res_cs)
1108}
1109
1110enum LoadStateInitError {
1111    BadStateInit,
1112    Other,
1113}
1114
1115impl From<Error> for LoadStateInitError {
1116    #[inline]
1117    fn from(_: Error) -> Self {
1118        Self::Other
1119    }
1120}
1121
1122fn load_body_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
1123    let res_cs = *cs;
1124
1125    if cs.load_bit()? {
1126        // right$1 ^X
1127        cs.skip_first(0, 1)?;
1128    } else {
1129        // left$0 X
1130        cs.load_remaining();
1131    }
1132
1133    Ok(res_cs)
1134}
1135
1136fn rewrite_state_init_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1137    // Skip prefix `just$1 (left$0 ...)`.
1138    let prefix = cs.load_small_uint(2).unwrap();
1139    debug_assert_eq!(prefix, 0b10);
1140
1141    // Build ^StateInit.
1142    let cell = CellBuilder::build_from(cs).unwrap();
1143
1144    // Build `just$1 (right$1 ^StateInit)`.
1145    let mut b = CellBuilder::new();
1146    b.store_small_uint(0b11, 2).unwrap();
1147    b.store_reference(cell).unwrap();
1148
1149    // Done
1150    b
1151}
1152
1153fn rewrite_body_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
1154    // Skip prefix `left$0 ...`.
1155    let prefix = cs.load_bit().unwrap();
1156    debug_assert!(!prefix);
1157
1158    // Build ^X.
1159    let cell = CellBuilder::build_from(cs).unwrap();
1160
1161    // Build `right$1 ^X`.
1162    let mut b = CellBuilder::new();
1163    b.store_bit_one().unwrap();
1164    b.store_reference(cell).unwrap();
1165
1166    // Done
1167    b
1168}
1169
1170fn build_message(
1171    info: &RelaxedMsgInfo,
1172    state_init_cs: &CellSlice<'_>,
1173    body_cs: &CellSlice<'_>,
1174) -> Result<Lazy<OwnedMessage>, Error> {
1175    CellBuilder::build_from((info, state_init_cs, body_cs)).map(|cell| {
1176        // SAFETY: Tuple is always built as ordinary cell.
1177        unsafe { Lazy::from_raw_unchecked(cell) }
1178    })
1179}
1180
1181fn update_total_msg_stat(
1182    total_message_size: &mut StorageUsedShort,
1183    stats: CellTreeStats,
1184    root_bits: u16,
1185) {
1186    let bits_diff = VarUint56::new(stats.bit_count.saturating_add(root_bits as _));
1187    let cells_diff = VarUint56::new(stats.cell_count.saturating_add(1));
1188
1189    total_message_size.bits = total_message_size.bits.saturating_add(bits_diff);
1190    total_message_size.cells = total_message_size.cells.saturating_add(cells_diff);
1191}
1192
1193fn check_extra_balance(value: &ExtraCurrencyCollection, limit: usize) -> bool {
1194    for (i, entry) in value.as_dict().iter().enumerate() {
1195        if i > limit || entry.is_err() {
1196            return false;
1197        }
1198    }
1199    true
1200}
1201
1202fn normalize_extra_balance(
1203    value: ExtraCurrencyCollection,
1204    limit: usize,
1205) -> Result<ExtraCurrencyCollection, BalanceExtraError> {
1206    let mut result = value.clone();
1207    for (i, entry) in value.as_dict().iter().enumerate() {
1208        if i > limit {
1209            return Err(BalanceExtraError::OutOfLimit);
1210        }
1211        let (currency_id, other) = entry.map_err(BalanceExtraError::InvalidDict)?;
1212        if other.is_zero() {
1213            result
1214                .as_dict_mut()
1215                .remove(currency_id)
1216                .map_err(BalanceExtraError::InvalidDict)?;
1217        }
1218    }
1219    Ok(result)
1220}
1221
1222enum BalanceExtraError {
1223    OutOfLimit,
1224    InvalidDict(#[allow(unused)] Error),
1225}
1226
1227#[repr(i32)]
1228#[derive(Debug, thiserror::Error)]
1229enum ResultCode {
1230    #[error("invalid action list")]
1231    ActionListInvalid = 32,
1232    #[error("too many actions")]
1233    TooManyActions = 33,
1234    #[error("invalid or unsupported action")]
1235    ActionInvalid = 34,
1236    #[error("invalid source address")]
1237    InvalidSrcAddr = 35,
1238    #[error("invalid destination address")]
1239    InvalidDstAddr = 36,
1240    #[error("not enough balance (base currency)")]
1241    NotEnoughBalance = 37,
1242    #[error("not enough balance (extra currency)")]
1243    NotEnoughExtraBalance = 38,
1244    #[error("failed to fit message into cell")]
1245    FailedToFitMessage = 39,
1246    #[error("message exceeds limits")]
1247    MessageOutOfLimits = 40,
1248    #[error("library code not found")]
1249    NoLibCode = 41,
1250    #[error("failed to change libraries dict")]
1251    InvalidLibrariesDict = 42,
1252    #[error("too many library cells")]
1253    LibOutOfLimits = 43,
1254    #[error("too many extra currencies")]
1255    TooManyExtraCurrencies = 44,
1256    #[error("state exceeds limits")]
1257    StateOutOfLimits = 50,
1258}
1259
1260// TODO: Move into config parm 43.
1261const MAX_MSG_EXTRA_CURRENCIES: usize = 2;
1262
1263#[cfg(test)]
1264mod tests {
1265    use std::collections::BTreeMap;
1266
1267    use tycho_asm_macros::tvmasm;
1268    use tycho_types::merkle::MerkleProof;
1269    use tycho_types::models::{
1270        Anycast, AuthorityMarksConfig, IntAddr, MessageExtraFlags, MessageLayout, MsgInfo,
1271        RelaxedIntMsgInfo, RelaxedMessage, StdAddr, VarAddr,
1272    };
1273    use tycho_types::num::{Uint9, VarUint248};
1274
1275    use super::*;
1276    use crate::ExecutorParams;
1277    use crate::tests::{make_custom_config, make_default_config, make_default_params};
1278
1279    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
1280    const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
1281    const OK_GAS: Tokens = Tokens::new(1_000_000);
1282
1283    fn stub_compute_phase(gas_fees: Tokens) -> ExecutedComputePhase {
1284        ExecutedComputePhase {
1285            success: true,
1286            msg_state_used: false,
1287            account_activated: false,
1288            gas_fees,
1289            gas_used: Default::default(),
1290            gas_limit: Default::default(),
1291            gas_credit: None,
1292            mode: 0,
1293            exit_code: 0,
1294            exit_arg: None,
1295            vm_steps: 0,
1296            vm_init_state_hash: Default::default(),
1297            vm_final_state_hash: Default::default(),
1298        }
1299    }
1300
1301    fn empty_action_phase() -> ActionPhase {
1302        ActionPhase {
1303            success: true,
1304            valid: true,
1305            no_funds: false,
1306            status_change: AccountStatusChange::Unchanged,
1307            total_fwd_fees: None,
1308            total_action_fees: None,
1309            result_code: 0,
1310            result_arg: None,
1311            total_actions: 0,
1312            special_actions: 0,
1313            skipped_actions: 0,
1314            messages_created: 0,
1315            action_list_hash: *Cell::empty_cell_ref().repr_hash(),
1316            total_message_size: Default::default(),
1317        }
1318    }
1319
1320    fn make_action_list<I: IntoIterator<Item: Store>>(actions: I) -> Cell {
1321        let mut root = Cell::default();
1322        for action in actions {
1323            root = CellBuilder::build_from((root, action)).unwrap();
1324        }
1325        root
1326    }
1327
1328    fn make_relaxed_message(
1329        info: impl Into<RelaxedMsgInfo>,
1330        init: Option<StateInit>,
1331        body: Option<CellBuilder>,
1332    ) -> Lazy<OwnedRelaxedMessage> {
1333        let body = match &body {
1334            None => Cell::empty_cell_ref().as_slice_allow_exotic(),
1335            Some(cell) => cell.as_full_slice(),
1336        };
1337        Lazy::new(&RelaxedMessage {
1338            info: info.into(),
1339            init,
1340            body,
1341            layout: None,
1342        })
1343        .unwrap()
1344        .cast_into()
1345    }
1346
1347    fn compute_full_stats(msg: &Lazy<OwnedMessage>, params: &ExecutorParams) -> StorageUsedShort {
1348        let msg = 'cell: {
1349            if params.strict_extra_currency {
1350                let mut parsed = msg.load().unwrap();
1351                if let MsgInfo::Int(int) = &mut parsed.info
1352                    && !int.value.other.is_empty()
1353                {
1354                    int.value.other = ExtraCurrencyCollection::new();
1355                    break 'cell CellBuilder::build_from(parsed).unwrap();
1356                }
1357            }
1358            msg.inner().clone()
1359        };
1360
1361        let stats = {
1362            let mut stats = ExtStorageStat::with_limits(StorageStatLimits::UNLIMITED);
1363            assert!(stats.add_cell(msg.as_ref()));
1364            stats.stats()
1365        };
1366        StorageUsedShort {
1367            cells: VarUint56::new(stats.cell_count),
1368            bits: VarUint56::new(stats.bit_count),
1369        }
1370    }
1371
1372    fn original_balance(
1373        state: &ExecutorState<'_>,
1374        compute_phase: &ExecutedComputePhase,
1375    ) -> CurrencyCollection {
1376        state
1377            .balance
1378            .clone()
1379            .checked_add(&compute_phase.gas_fees.into())
1380            .unwrap()
1381    }
1382
1383    #[test]
1384    fn empty_actions() -> Result<()> {
1385        let params = make_default_params();
1386        let config = make_default_config();
1387        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1388
1389        let compute_phase = stub_compute_phase(OK_GAS);
1390        let prev_total_fees = state.total_fees;
1391        let prev_balance = state.balance.clone();
1392        let prev_end_lt = state.end_lt;
1393
1394        let ActionPhaseFull {
1395            action_phase,
1396            action_fine,
1397            state_exceeds_limits,
1398            bounce,
1399        } = state.action_phase(ActionPhaseContext {
1400            received_message: None,
1401            original_balance: original_balance(&state, &compute_phase),
1402            new_state: StateInit::default(),
1403            actions: Cell::empty_cell(),
1404            compute_phase: &compute_phase,
1405            inspector: None,
1406        })?;
1407
1408        assert_eq!(action_phase, empty_action_phase());
1409        assert_eq!(action_fine, Tokens::ZERO);
1410        assert!(!state_exceeds_limits);
1411        assert!(!bounce);
1412        assert_eq!(state.total_fees, prev_total_fees);
1413        assert_eq!(state.balance, prev_balance);
1414        assert_eq!(state.end_lt, prev_end_lt);
1415        Ok(())
1416    }
1417
1418    #[test]
1419    fn too_many_actions() -> Result<()> {
1420        let params = make_default_params();
1421        let config = make_default_config();
1422        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1423
1424        let compute_phase = stub_compute_phase(OK_GAS);
1425        let prev_total_fees = state.total_fees;
1426        let prev_balance = state.balance.clone();
1427        let prev_end_lt = state.end_lt;
1428
1429        let actions = make_action_list(
1430            std::iter::repeat_with(|| OutAction::SetCode {
1431                new_code: Cell::empty_cell(),
1432            })
1433            .take(300),
1434        );
1435
1436        let ActionPhaseFull {
1437            action_phase,
1438            action_fine,
1439            state_exceeds_limits,
1440            bounce,
1441        } = state.action_phase(ActionPhaseContext {
1442            received_message: None,
1443            original_balance: original_balance(&state, &compute_phase),
1444            new_state: StateInit::default(),
1445            actions: actions.clone(),
1446            compute_phase: &compute_phase,
1447            inspector: None,
1448        })?;
1449
1450        assert_eq!(action_phase, ActionPhase {
1451            success: false,
1452            valid: false,
1453            result_code: ResultCode::TooManyActions as i32,
1454            result_arg: Some(256),
1455            action_list_hash: *actions.repr_hash(),
1456            ..empty_action_phase()
1457        });
1458        assert_eq!(action_fine, Tokens::ZERO);
1459        assert!(!state_exceeds_limits);
1460        assert!(!bounce);
1461        assert_eq!(state.total_fees, prev_total_fees);
1462        assert_eq!(state.balance, prev_balance);
1463        assert_eq!(state.end_lt, prev_end_lt);
1464        Ok(())
1465    }
1466
1467    #[test]
1468    fn invalid_action_list() -> Result<()> {
1469        let params = make_default_params();
1470        let config = make_default_config();
1471        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1472
1473        let compute_phase = stub_compute_phase(OK_GAS);
1474        let prev_total_fees = state.total_fees;
1475        let prev_balance = state.balance.clone();
1476        let prev_end_lt = state.end_lt;
1477
1478        let actions = CellBuilder::build_from((
1479            CellBuilder::build_from(MerkleProof::default())?,
1480            OutAction::SetCode {
1481                new_code: Cell::default(),
1482            },
1483        ))?;
1484
1485        let ActionPhaseFull {
1486            action_phase,
1487            action_fine,
1488            state_exceeds_limits,
1489            bounce,
1490        } = state.action_phase(ActionPhaseContext {
1491            received_message: None,
1492            original_balance: original_balance(&state, &compute_phase),
1493            new_state: StateInit::default(),
1494            actions: actions.clone(),
1495            compute_phase: &compute_phase,
1496            inspector: None,
1497        })?;
1498
1499        assert_eq!(action_phase, ActionPhase {
1500            success: false,
1501            valid: false,
1502            result_code: ResultCode::ActionListInvalid as i32,
1503            result_arg: Some(1),
1504            action_list_hash: *actions.repr_hash(),
1505            ..empty_action_phase()
1506        });
1507        assert_eq!(action_fine, Tokens::ZERO);
1508        assert!(!state_exceeds_limits);
1509        assert!(!bounce);
1510        assert_eq!(state.total_fees, prev_total_fees);
1511        assert_eq!(state.balance, prev_balance);
1512        assert_eq!(state.end_lt, prev_end_lt);
1513        Ok(())
1514    }
1515
1516    #[test]
1517    fn invalid_action() -> Result<()> {
1518        let params = make_default_params();
1519        let config = make_default_config();
1520        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1521
1522        let compute_phase = stub_compute_phase(OK_GAS);
1523        let prev_total_fees = state.total_fees;
1524        let prev_balance = state.balance.clone();
1525        let prev_end_lt = state.end_lt;
1526
1527        let set_code_action = {
1528            let mut b = CellBuilder::new();
1529            OutAction::SetCode {
1530                new_code: Cell::empty_cell(),
1531            }
1532            .store_into(&mut b, Cell::empty_context())?;
1533            b
1534        };
1535        let invalid_action = {
1536            let mut b = CellBuilder::new();
1537            b.store_u32(0xdeafbeaf)?;
1538            b
1539        };
1540
1541        let actions = make_action_list([
1542            set_code_action.as_full_slice(),
1543            set_code_action.as_full_slice(),
1544            invalid_action.as_full_slice(),
1545            set_code_action.as_full_slice(),
1546            set_code_action.as_full_slice(),
1547        ]);
1548
1549        let ActionPhaseFull {
1550            action_phase,
1551            action_fine,
1552            state_exceeds_limits,
1553            bounce,
1554        } = state.action_phase(ActionPhaseContext {
1555            received_message: None,
1556            original_balance: original_balance(&state, &compute_phase),
1557            new_state: StateInit::default(),
1558            actions: actions.clone(),
1559            compute_phase: &compute_phase,
1560            inspector: None,
1561        })?;
1562
1563        assert_eq!(action_phase, ActionPhase {
1564            success: false,
1565            valid: false,
1566            result_code: ResultCode::ActionInvalid as i32,
1567            result_arg: Some(2),
1568            action_list_hash: *actions.repr_hash(),
1569            total_actions: 5,
1570            ..empty_action_phase()
1571        });
1572        assert_eq!(action_fine, Tokens::ZERO);
1573        assert!(!state_exceeds_limits);
1574        assert!(!bounce);
1575        assert_eq!(state.total_fees, prev_total_fees);
1576        assert_eq!(state.balance, prev_balance);
1577        assert_eq!(state.end_lt, prev_end_lt);
1578        Ok(())
1579    }
1580
1581    #[test]
1582    fn strict_reserve_extra_currency() -> Result<()> {
1583        let mut params = make_default_params();
1584        params.strict_extra_currency = true;
1585        let config = make_default_config();
1586        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1587        let prev_balance = state.balance.clone();
1588
1589        let compute_phase = stub_compute_phase(OK_GAS);
1590        let prev_total_fees = state.total_fees;
1591        let prev_end_lt = state.end_lt;
1592
1593        let actions = make_action_list([OutAction::ReserveCurrency {
1594            mode: ReserveCurrencyFlags::empty(),
1595            value: CurrencyCollection {
1596                tokens: Tokens::ZERO,
1597                other: BTreeMap::from_iter([(123u32, VarUint248::new(10))]).try_into()?,
1598            },
1599        }]);
1600
1601        let ActionPhaseFull {
1602            action_phase,
1603            action_fine,
1604            state_exceeds_limits,
1605            bounce,
1606        } = state.action_phase(ActionPhaseContext {
1607            received_message: None,
1608            original_balance: original_balance(&state, &compute_phase),
1609            new_state: StateInit::default(),
1610            actions: actions.clone(),
1611            compute_phase: &compute_phase,
1612            inspector: None,
1613        })?;
1614
1615        assert_eq!(action_phase, ActionPhase {
1616            success: false,
1617            valid: true,
1618            result_code: ResultCode::ActionInvalid as i32,
1619            result_arg: Some(0),
1620            action_list_hash: *actions.repr_hash(),
1621            total_actions: 1,
1622            ..empty_action_phase()
1623        });
1624        assert_eq!(action_fine, Tokens::ZERO);
1625        assert!(!state_exceeds_limits);
1626        assert!(!bounce);
1627        assert_eq!(state.total_fees, prev_total_fees);
1628        assert_eq!(state.balance, prev_balance);
1629        assert_eq!(state.end_lt, prev_end_lt);
1630        Ok(())
1631    }
1632
1633    #[test]
1634    fn send_single_message() -> Result<()> {
1635        let params = make_default_params();
1636        let config = make_default_config();
1637        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1638
1639        let compute_phase = stub_compute_phase(OK_GAS);
1640        let prev_total_fees = state.total_fees;
1641        let prev_balance = state.balance.clone();
1642        let prev_end_lt = state.end_lt;
1643
1644        let msg_value = Tokens::new(500_000_000);
1645
1646        let actions = make_action_list([OutAction::SendMsg {
1647            mode: SendMsgFlags::empty(),
1648            out_msg: make_relaxed_message(
1649                RelaxedIntMsgInfo {
1650                    dst: STUB_ADDR.into(),
1651                    value: msg_value.into(),
1652                    ..Default::default()
1653                },
1654                None,
1655                None,
1656            ),
1657        }]);
1658
1659        let ActionPhaseFull {
1660            action_phase,
1661            action_fine,
1662            state_exceeds_limits,
1663            bounce,
1664        } = state.action_phase(ActionPhaseContext {
1665            received_message: None,
1666            original_balance: original_balance(&state, &compute_phase),
1667            new_state: StateInit::default(),
1668            actions: actions.clone(),
1669            compute_phase: &compute_phase,
1670            inspector: None,
1671        })?;
1672
1673        assert_eq!(action_fine, Tokens::ZERO);
1674        assert!(!state_exceeds_limits);
1675        assert!(!bounce);
1676
1677        assert_eq!(state.out_msgs.len(), 1);
1678        assert_eq!(state.end_lt, prev_end_lt + 1);
1679        let last_msg = state.out_msgs.last().unwrap();
1680
1681        let msg_info = {
1682            let msg = last_msg.load()?;
1683            assert!(msg.init.is_none());
1684            assert_eq!(msg.body, Default::default());
1685            match msg.info {
1686                MsgInfo::Int(info) => info,
1687                e => panic!("unexpected msg info {e:?}"),
1688            }
1689        };
1690        assert_eq!(msg_info.src, STUB_ADDR.into());
1691        assert_eq!(msg_info.dst, STUB_ADDR.into());
1692        assert!(msg_info.ihr_disabled);
1693        assert!(!msg_info.bounce);
1694        assert!(!msg_info.bounced);
1695        assert_eq!(msg_info.created_at, params.block_unixtime);
1696        assert_eq!(msg_info.created_lt, prev_end_lt);
1697
1698        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1699        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1700
1701        assert_eq!(msg_info.value, (msg_value - expected_fwd_fees).into());
1702        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1703        assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
1704
1705        assert_eq!(action_phase, ActionPhase {
1706            total_fwd_fees: Some(expected_fwd_fees),
1707            total_action_fees: Some(expected_first_frac),
1708            total_actions: 1,
1709            messages_created: 1,
1710            action_list_hash: *actions.repr_hash(),
1711            total_message_size: compute_full_stats(last_msg, &params),
1712            ..empty_action_phase()
1713        });
1714
1715        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1716        assert_eq!(state.balance.other, prev_balance.other);
1717        assert_eq!(state.balance.tokens, prev_balance.tokens - msg_value);
1718
1719        Ok(())
1720    }
1721
1722    #[test]
1723    fn send_all_balance() -> Result<()> {
1724        let params = make_default_params();
1725        let config = make_default_config();
1726        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
1727
1728        let compute_phase = stub_compute_phase(OK_GAS);
1729        let prev_total_fees = state.total_fees;
1730        let prev_balance = state.balance.clone();
1731        let prev_end_lt = state.end_lt;
1732
1733        let actions = make_action_list([OutAction::SendMsg {
1734            mode: SendMsgFlags::ALL_BALANCE,
1735            out_msg: make_relaxed_message(
1736                RelaxedIntMsgInfo {
1737                    dst: STUB_ADDR.into(),
1738                    value: CurrencyCollection::ZERO,
1739                    ..Default::default()
1740                },
1741                None,
1742                None,
1743            ),
1744        }]);
1745
1746        let ActionPhaseFull {
1747            action_phase,
1748            action_fine,
1749            state_exceeds_limits,
1750            bounce,
1751        } = state.action_phase(ActionPhaseContext {
1752            received_message: None,
1753            original_balance: original_balance(&state, &compute_phase),
1754            new_state: StateInit::default(),
1755            actions: actions.clone(),
1756            compute_phase: &compute_phase,
1757            inspector: None,
1758        })?;
1759
1760        assert_eq!(action_fine, Tokens::ZERO);
1761        assert!(!state_exceeds_limits);
1762        assert!(!bounce);
1763
1764        assert_eq!(state.out_msgs.len(), 1);
1765        assert_eq!(state.end_lt, prev_end_lt + 1);
1766        let last_msg = state.out_msgs.last().unwrap();
1767
1768        let msg_info = {
1769            let msg = last_msg.load()?;
1770            assert!(msg.init.is_none());
1771            assert_eq!(msg.body, Default::default());
1772            match msg.info {
1773                MsgInfo::Int(info) => info,
1774                e => panic!("unexpected msg info {e:?}"),
1775            }
1776        };
1777        assert_eq!(msg_info.src, STUB_ADDR.into());
1778        assert_eq!(msg_info.dst, STUB_ADDR.into());
1779        assert!(msg_info.ihr_disabled);
1780        assert!(!msg_info.bounce);
1781        assert!(!msg_info.bounced);
1782        assert_eq!(msg_info.created_at, params.block_unixtime);
1783        assert_eq!(msg_info.created_lt, prev_end_lt);
1784
1785        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1786        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1787
1788        assert_eq!(
1789            msg_info.value,
1790            (prev_balance.tokens - expected_fwd_fees).into()
1791        );
1792        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1793        assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
1794
1795        assert_eq!(action_phase, ActionPhase {
1796            total_fwd_fees: Some(expected_fwd_fees),
1797            total_action_fees: Some(expected_first_frac),
1798            total_actions: 1,
1799            messages_created: 1,
1800            action_list_hash: *actions.repr_hash(),
1801            total_message_size: compute_full_stats(last_msg, &params),
1802            ..empty_action_phase()
1803        });
1804
1805        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1806        assert_eq!(state.balance, CurrencyCollection::ZERO);
1807
1808        Ok(())
1809    }
1810
1811    #[test]
1812    fn strict_send_all_balance() -> Result<()> {
1813        let mut params = make_default_params();
1814        params.strict_extra_currency = true;
1815        let config = make_default_config();
1816
1817        let mut state =
1818            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, CurrencyCollection {
1819                tokens: OK_BALANCE,
1820                other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1821            });
1822
1823        let compute_phase = stub_compute_phase(OK_GAS);
1824        let prev_total_fees = state.total_fees;
1825        let prev_balance = state.balance.clone();
1826        let prev_end_lt = state.end_lt;
1827
1828        let sent_value = CurrencyCollection {
1829            tokens: Tokens::ZERO,
1830            other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1831        };
1832
1833        let actions = make_action_list([OutAction::SendMsg {
1834            mode: SendMsgFlags::ALL_BALANCE,
1835            out_msg: make_relaxed_message(
1836                RelaxedIntMsgInfo {
1837                    dst: STUB_ADDR.into(),
1838                    value: sent_value.clone(),
1839                    ..Default::default()
1840                },
1841                None,
1842                None,
1843            ),
1844        }]);
1845
1846        let ActionPhaseFull {
1847            action_phase,
1848            action_fine,
1849            state_exceeds_limits,
1850            bounce,
1851        } = state.action_phase(ActionPhaseContext {
1852            received_message: None,
1853            original_balance: original_balance(&state, &compute_phase),
1854            new_state: StateInit::default(),
1855            actions: actions.clone(),
1856            compute_phase: &compute_phase,
1857            inspector: None,
1858        })?;
1859
1860        assert_eq!(action_fine, Tokens::ZERO);
1861        assert!(!state_exceeds_limits);
1862        assert!(!bounce);
1863
1864        assert_eq!(state.out_msgs.len(), 1);
1865        assert_eq!(state.end_lt, prev_end_lt + 1);
1866        let last_msg = state.out_msgs.last().unwrap();
1867
1868        let msg_info = {
1869            let msg = last_msg.load()?;
1870            assert!(msg.init.is_none());
1871            assert_eq!(msg.body, Default::default());
1872            match msg.info {
1873                MsgInfo::Int(info) => info,
1874                e => panic!("unexpected msg info {e:?}"),
1875            }
1876        };
1877        assert_eq!(msg_info.src, STUB_ADDR.into());
1878        assert_eq!(msg_info.dst, STUB_ADDR.into());
1879        assert!(msg_info.ihr_disabled);
1880        assert!(!msg_info.bounce);
1881        assert!(!msg_info.bounced);
1882        assert_eq!(msg_info.created_at, params.block_unixtime);
1883        assert_eq!(msg_info.created_lt, prev_end_lt);
1884
1885        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
1886        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
1887
1888        assert_eq!(msg_info.value, CurrencyCollection {
1889            tokens: prev_balance.tokens - expected_fwd_fees,
1890            other: sent_value.other.clone(),
1891        });
1892        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
1893        assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
1894
1895        assert_eq!(action_phase, ActionPhase {
1896            total_fwd_fees: Some(expected_fwd_fees),
1897            total_action_fees: Some(expected_first_frac),
1898            total_actions: 1,
1899            messages_created: 1,
1900            action_list_hash: *actions.repr_hash(),
1901            total_message_size: compute_full_stats(last_msg, &params),
1902            ..empty_action_phase()
1903        });
1904
1905        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
1906        assert_eq!(state.balance.tokens, Tokens::ZERO);
1907        assert_eq!(
1908            state.balance.other,
1909            prev_balance.other.checked_sub(&sent_value.other)?
1910        );
1911
1912        Ok(())
1913    }
1914
1915    #[test]
1916    fn strict_send_all_balance_destroy() -> Result<()> {
1917        struct TestCase {
1918            balance: CurrencyCollection,
1919            to_send: CurrencyCollection,
1920            expected_end_status: AccountStatus,
1921        }
1922
1923        let mut params = make_default_params();
1924        params.strict_extra_currency = true;
1925        let config = make_default_config();
1926
1927        for TestCase {
1928            balance,
1929            to_send,
1930            expected_end_status,
1931        } in [
1932            // Simple send all + delete
1933            TestCase {
1934                balance: OK_BALANCE.into(),
1935                to_send: CurrencyCollection::ZERO,
1936                expected_end_status: AccountStatus::NotExists,
1937            },
1938            // Simple send all but account has some extra currencies
1939            TestCase {
1940                balance: CurrencyCollection {
1941                    tokens: OK_BALANCE,
1942                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1943                },
1944                to_send: CurrencyCollection::ZERO,
1945                expected_end_status: AccountStatus::Uninit,
1946            },
1947            // Simple send all but account has some extra currencies
1948            TestCase {
1949                balance: CurrencyCollection {
1950                    tokens: OK_BALANCE,
1951                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1952                },
1953                to_send: CurrencyCollection::ZERO,
1954                expected_end_status: AccountStatus::Uninit,
1955            },
1956            // Send all native + some extra but account still has some more extra currencies
1957            TestCase {
1958                balance: CurrencyCollection {
1959                    tokens: OK_BALANCE,
1960                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1961                },
1962                to_send: CurrencyCollection {
1963                    tokens: OK_BALANCE,
1964                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
1965                },
1966                expected_end_status: AccountStatus::Uninit,
1967            },
1968            // Send all native and all extra
1969            TestCase {
1970                balance: CurrencyCollection {
1971                    tokens: OK_BALANCE,
1972                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1973                },
1974                to_send: CurrencyCollection {
1975                    tokens: OK_BALANCE,
1976                    other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
1977                },
1978                expected_end_status: AccountStatus::NotExists,
1979            },
1980        ] {
1981            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, balance);
1982
1983            let compute_phase = stub_compute_phase(OK_GAS);
1984            let prev_total_fees = state.total_fees;
1985            let prev_balance = state.balance.clone();
1986            let prev_end_lt = state.end_lt;
1987
1988            let actions = make_action_list([OutAction::SendMsg {
1989                mode: SendMsgFlags::ALL_BALANCE | SendMsgFlags::DELETE_IF_EMPTY,
1990                out_msg: make_relaxed_message(
1991                    RelaxedIntMsgInfo {
1992                        dst: STUB_ADDR.into(),
1993                        value: to_send.clone(),
1994                        ..Default::default()
1995                    },
1996                    None,
1997                    None,
1998                ),
1999            }]);
2000
2001            let ActionPhaseFull {
2002                action_phase,
2003                action_fine,
2004                state_exceeds_limits,
2005                bounce,
2006            } = state.action_phase(ActionPhaseContext {
2007                received_message: None,
2008                original_balance: original_balance(&state, &compute_phase),
2009                new_state: StateInit::default(),
2010                actions: actions.clone(),
2011                compute_phase: &compute_phase,
2012                inspector: None,
2013            })?;
2014
2015            assert_eq!(action_fine, Tokens::ZERO);
2016            assert!(!state_exceeds_limits);
2017            assert!(!bounce);
2018
2019            assert_eq!(state.end_status, expected_end_status);
2020            assert_eq!(state.out_msgs.len(), 1);
2021            assert_eq!(state.end_lt, prev_end_lt + 1);
2022            let last_msg = state.out_msgs.last().unwrap();
2023
2024            let msg_info = {
2025                let msg = last_msg.load()?;
2026                assert!(msg.init.is_none());
2027                assert_eq!(msg.body, Default::default());
2028                match msg.info {
2029                    MsgInfo::Int(info) => info,
2030                    e => panic!("unexpected msg info {e:?}"),
2031                }
2032            };
2033            assert_eq!(msg_info.src, STUB_ADDR.into());
2034            assert_eq!(msg_info.dst, STUB_ADDR.into());
2035            assert!(msg_info.ihr_disabled);
2036            assert!(!msg_info.bounce);
2037            assert!(!msg_info.bounced);
2038            assert_eq!(msg_info.created_at, params.block_unixtime);
2039            assert_eq!(msg_info.created_lt, prev_end_lt);
2040
2041            let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2042            let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2043
2044            assert_eq!(msg_info.value, CurrencyCollection {
2045                tokens: prev_balance.tokens - expected_fwd_fees,
2046                other: to_send.other.clone(),
2047            });
2048            assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2049            assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
2050
2051            assert_eq!(action_phase, ActionPhase {
2052                total_fwd_fees: Some(expected_fwd_fees),
2053                total_action_fees: Some(expected_first_frac),
2054                total_actions: 1,
2055                messages_created: 1,
2056                action_list_hash: *actions.repr_hash(),
2057                total_message_size: compute_full_stats(last_msg, &params),
2058                status_change: AccountStatusChange::Deleted,
2059                ..empty_action_phase()
2060            });
2061
2062            assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2063            assert_eq!(state.balance.tokens, Tokens::ZERO);
2064            assert_eq!(
2065                state.balance.other,
2066                prev_balance.other.checked_sub(&to_send.other)?
2067            );
2068        }
2069
2070        Ok(())
2071    }
2072
2073    #[test]
2074    fn send_all_not_reserved() -> Result<()> {
2075        let params = make_default_params();
2076        let config = make_default_config();
2077        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2078
2079        let compute_phase = stub_compute_phase(OK_GAS);
2080        let prev_total_fees = state.total_fees;
2081        let prev_end_lt = state.end_lt;
2082
2083        let expected_balance = CurrencyCollection::from(state.balance.tokens / 4);
2084        let actions = make_action_list([
2085            OutAction::ReserveCurrency {
2086                mode: ReserveCurrencyFlags::empty(),
2087                value: expected_balance.clone(),
2088            },
2089            OutAction::SendMsg {
2090                mode: SendMsgFlags::ALL_BALANCE,
2091                out_msg: make_relaxed_message(
2092                    RelaxedIntMsgInfo {
2093                        dst: STUB_ADDR.into(),
2094                        value: CurrencyCollection::ZERO,
2095                        ..Default::default()
2096                    },
2097                    None,
2098                    None,
2099                ),
2100            },
2101        ]);
2102
2103        let ActionPhaseFull {
2104            action_phase,
2105            action_fine,
2106            state_exceeds_limits,
2107            bounce,
2108        } = state.action_phase(ActionPhaseContext {
2109            received_message: None,
2110            original_balance: original_balance(&state, &compute_phase),
2111            new_state: StateInit::default(),
2112            actions: actions.clone(),
2113            compute_phase: &compute_phase,
2114            inspector: None,
2115        })?;
2116
2117        assert_eq!(state.out_msgs.len(), 1);
2118        assert_eq!(state.end_lt, prev_end_lt + 1);
2119        let last_msg = state.out_msgs.last().unwrap();
2120
2121        let msg_info = {
2122            let msg = last_msg.load()?;
2123            assert!(msg.init.is_none());
2124            assert_eq!(msg.body, Default::default());
2125            match msg.info {
2126                MsgInfo::Int(info) => info,
2127                e => panic!("unexpected msg info {e:?}"),
2128            }
2129        };
2130        assert_eq!(msg_info.src, STUB_ADDR.into());
2131        assert_eq!(msg_info.dst, STUB_ADDR.into());
2132        assert!(msg_info.ihr_disabled);
2133        assert!(!msg_info.bounce);
2134        assert!(!msg_info.bounced);
2135        assert_eq!(msg_info.created_at, params.block_unixtime);
2136        assert_eq!(msg_info.created_lt, prev_end_lt);
2137
2138        let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
2139        let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2140
2141        assert_eq!(
2142            msg_info.value,
2143            (OK_BALANCE * 3 / 4 - expected_fwd_fees).into()
2144        );
2145        assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
2146        assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
2147
2148        assert_eq!(action_phase, ActionPhase {
2149            total_fwd_fees: Some(expected_fwd_fees),
2150            total_action_fees: Some(expected_first_frac),
2151            total_actions: 2,
2152            messages_created: 1,
2153            special_actions: 1,
2154            action_list_hash: *actions.repr_hash(),
2155            total_message_size: compute_full_stats(last_msg, &params),
2156            ..empty_action_phase()
2157        });
2158        assert_eq!(action_fine, Tokens::ZERO);
2159        assert!(!state_exceeds_limits);
2160        assert!(!bounce);
2161
2162        assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
2163        assert_eq!(state.balance, expected_balance);
2164        Ok(())
2165    }
2166
2167    #[test]
2168    fn cant_send_authority_marks_by_all_balance() -> Result<()> {
2169        let params = ExecutorParams {
2170            authority_marks_enabled: true,
2171            strict_extra_currency: false,
2172            ..make_default_params()
2173        };
2174
2175        let config = make_custom_config(|config| {
2176            config.set_authority_marks_config(&AuthorityMarksConfig {
2177                authority_addresses: Dict::new(),
2178                black_mark_id: 100,
2179                white_mark_id: 101,
2180            })?;
2181            Ok(())
2182        });
2183
2184        let mut state = ExecutorState::new_active(
2185            &params,
2186            &config,
2187            &STUB_ADDR,
2188            CurrencyCollection {
2189                tokens: OK_BALANCE,
2190                other: BTreeMap::from_iter([
2191                    (100u32, VarUint248::new(500)), // white and black marks + some extra currency
2192                    (101u32, VarUint248::new(500)),
2193                    (200u32, VarUint248::new(500)),
2194                ])
2195                .try_into()?,
2196            },
2197            Cell::default(),
2198            tvmasm!("ACCEPT"),
2199        );
2200
2201        let compute_phase = stub_compute_phase(OK_GAS);
2202
2203        let prev_end_lt = state.end_lt;
2204        let prev_balance = state.balance.clone();
2205
2206        let actions = make_action_list([OutAction::SendMsg {
2207            mode: SendMsgFlags::ALL_BALANCE,
2208            out_msg: make_relaxed_message(
2209                RelaxedIntMsgInfo {
2210                    dst: STUB_ADDR.into(),
2211                    value: CurrencyCollection {
2212                        tokens: Tokens::new(100_000_000),
2213                        other: ExtraCurrencyCollection::new(),
2214                    },
2215                    ..Default::default()
2216                },
2217                None,
2218                None,
2219            ),
2220        }]);
2221
2222        let ActionPhaseFull {
2223            action_phase,
2224            action_fine,
2225            state_exceeds_limits,
2226            bounce,
2227        } = state.action_phase(ActionPhaseContext {
2228            received_message: None,
2229            original_balance: original_balance(&state, &compute_phase),
2230            new_state: StateInit::default(),
2231            actions,
2232            compute_phase: &compute_phase,
2233            inspector: None,
2234        })?;
2235
2236        assert!(!action_phase.success);
2237        assert!(action_phase.valid);
2238        assert_eq!(
2239            action_phase.result_code,
2240            ResultCode::NotEnoughExtraBalance as i32
2241        );
2242        assert_eq!(action_phase.messages_created, 0);
2243        assert_eq!(action_phase.total_actions, 1);
2244        assert_eq!(action_fine, Tokens::ZERO);
2245        assert!(!state_exceeds_limits);
2246        assert!(!bounce);
2247
2248        assert_eq!(state.out_msgs.len(), 0);
2249        assert_eq!(state.end_lt, prev_end_lt);
2250
2251        assert_eq!(state.balance, prev_balance);
2252        Ok(())
2253    }
2254
2255    #[test]
2256    fn authority_addresses_can_send_marks() -> Result<()> {
2257        let params = ExecutorParams {
2258            authority_marks_enabled: true,
2259            ..make_default_params()
2260        };
2261        let config = make_custom_config(|config| {
2262            config.set_authority_marks_config(&AuthorityMarksConfig {
2263                authority_addresses: Dict::try_from_btree(&BTreeMap::from_iter([(
2264                    HashBytes::ZERO,
2265                    (),
2266                )]))?,
2267                black_mark_id: 100,
2268                white_mark_id: 101,
2269            })?;
2270            Ok(())
2271        });
2272
2273        let mut state = ExecutorState::new_active(
2274            &params,
2275            &config,
2276            &StdAddr::new(-1, HashBytes::ZERO),
2277            CurrencyCollection {
2278                tokens: OK_BALANCE,
2279                other: BTreeMap::from_iter([
2280                    (100u32, VarUint248::new(1000)), // more black barks than white
2281                    (101u32, VarUint248::new(100)),
2282                ])
2283                .try_into()?,
2284            },
2285            Cell::default(),
2286            tvmasm!("ACCEPT"),
2287        );
2288
2289        let compute_phase = stub_compute_phase(OK_GAS);
2290        let prev_balance = state.balance.clone();
2291
2292        // check sending of black mark token even if authority account has more black marks
2293        let actions_marks = make_action_list([OutAction::SendMsg {
2294            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2295            out_msg: make_relaxed_message(
2296                RelaxedIntMsgInfo {
2297                    dst: STUB_ADDR.into(),
2298                    value: CurrencyCollection {
2299                        tokens: Tokens::ZERO,
2300                        other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
2301                    },
2302                    ..Default::default()
2303                },
2304                None,
2305                None,
2306            ),
2307        }]);
2308
2309        let ActionPhaseFull {
2310            action_phase: action_phase_normal,
2311            action_fine: action_fine_normal,
2312            state_exceeds_limits: state_exceeds_limits_normal,
2313            bounce: bounce_normal,
2314        } = state.action_phase(ActionPhaseContext {
2315            received_message: None,
2316            original_balance: original_balance(&state, &compute_phase),
2317            new_state: StateInit::default(),
2318            actions: actions_marks.clone(),
2319            compute_phase: &compute_phase,
2320            inspector: None,
2321        })?;
2322
2323        assert!(action_phase_normal.success);
2324        assert!(action_phase_normal.valid);
2325        assert_eq!(action_phase_normal.result_code, 0);
2326        assert_eq!(action_phase_normal.messages_created, 1);
2327        assert_eq!(action_fine_normal, Tokens::ZERO);
2328
2329        assert_ne!(state.balance.tokens, prev_balance.tokens); //gas should be consumed
2330        assert_eq!(
2331            state.balance.other,
2332            BTreeMap::from_iter([
2333                (100u32, VarUint248::new(990)), // updated white and black marks
2334                (101u32, VarUint248::new(100)),
2335            ])
2336            .try_into()?
2337        );
2338        assert!(!state_exceeds_limits_normal);
2339        assert!(!bounce_normal);
2340
2341        assert_eq!(state.out_msgs.len(), 1);
2342
2343        Ok(())
2344    }
2345
2346    #[test]
2347    fn cant_send_authority_marks_directly() -> Result<()> {
2348        let params = ExecutorParams {
2349            authority_marks_enabled: true,
2350            ..make_default_params()
2351        };
2352
2353        let config = make_custom_config(|config| {
2354            config.set_authority_marks_config(&AuthorityMarksConfig {
2355                authority_addresses: Dict::new(),
2356                black_mark_id: 100,
2357                white_mark_id: 101,
2358            })?;
2359            Ok(())
2360        });
2361
2362        let mut state = ExecutorState::new_active(
2363            &params,
2364            &config,
2365            &STUB_ADDR,
2366            CurrencyCollection {
2367                tokens: OK_BALANCE,
2368                other: BTreeMap::from_iter([
2369                    (100u32, VarUint248::new(500)), // white and black marks
2370                    (101u32, VarUint248::new(500)),
2371                ])
2372                .try_into()?,
2373            },
2374            Cell::default(),
2375            tvmasm!("ACCEPT"),
2376        );
2377
2378        let compute_phase = stub_compute_phase(OK_GAS);
2379        let prev_balance = state.balance.clone();
2380        let prev_total_fees = state.total_fees;
2381        let prev_end_lt = state.end_lt;
2382
2383        // check send of black mark token. Should exit with NotEnoughExtraBalance error
2384        let actions_black = make_action_list([OutAction::SendMsg {
2385            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2386            out_msg: make_relaxed_message(
2387                RelaxedIntMsgInfo {
2388                    dst: STUB_ADDR.into(),
2389                    value: CurrencyCollection {
2390                        tokens: Tokens::ZERO,
2391                        other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
2392                    },
2393                    ..Default::default()
2394                },
2395                None,
2396                None,
2397            ),
2398        }]);
2399
2400        let ActionPhaseFull {
2401            action_phase: action_phase_black,
2402            action_fine: action_fine_black,
2403            state_exceeds_limits: state_exceeds_limits_black,
2404            bounce: bounce_black,
2405        } = state.action_phase(ActionPhaseContext {
2406            received_message: None,
2407            original_balance: original_balance(&state, &compute_phase),
2408            new_state: StateInit::default(),
2409            actions: actions_black.clone(),
2410            compute_phase: &compute_phase,
2411            inspector: None,
2412        })?;
2413
2414        assert_eq!(action_phase_black, ActionPhase {
2415            success: false,
2416            valid: true,
2417            no_funds: true,
2418            result_code: ResultCode::NotEnoughExtraBalance as i32,
2419            result_arg: Some(0),
2420            total_actions: 1,
2421            action_list_hash: *actions_black.repr_hash(),
2422            ..empty_action_phase()
2423        });
2424
2425        assert_eq!(action_fine_black, Tokens::ZERO);
2426        assert!(!state_exceeds_limits_black);
2427        assert!(!bounce_black);
2428
2429        assert_eq!(state.total_fees, prev_total_fees);
2430        assert_eq!(state.balance, prev_balance);
2431        assert_eq!(state.end_lt, prev_end_lt);
2432        assert!(state.out_msgs.is_empty());
2433
2434        // now let's send native token. should be successful
2435        let actions_normal = make_action_list([OutAction::SendMsg {
2436            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2437            out_msg: make_relaxed_message(
2438                RelaxedIntMsgInfo {
2439                    dst: STUB_ADDR.into(),
2440                    value: CurrencyCollection {
2441                        tokens: Tokens::ZERO,
2442                        other: ExtraCurrencyCollection::new(),
2443                    },
2444                    ..Default::default()
2445                },
2446                None,
2447                None,
2448            ),
2449        }]);
2450
2451        let ActionPhaseFull {
2452            action_phase: action_phase_normal,
2453            action_fine: action_fine_normal,
2454            state_exceeds_limits: state_exceeds_limits_normal,
2455            bounce: bounce_normal,
2456        } = state.action_phase(ActionPhaseContext {
2457            received_message: None,
2458            original_balance: original_balance(&state, &compute_phase),
2459            new_state: StateInit::default(),
2460            actions: actions_normal.clone(),
2461            compute_phase: &compute_phase,
2462            inspector: None,
2463        })?;
2464
2465        assert!(action_phase_normal.success);
2466        assert!(action_phase_normal.valid);
2467        assert_eq!(action_phase_normal.result_code, 0);
2468        assert_eq!(action_phase_normal.messages_created, 1);
2469        assert_eq!(action_fine_normal, Tokens::ZERO);
2470        assert!(!state_exceeds_limits_normal);
2471        assert!(!bounce_normal);
2472
2473        assert_eq!(state.out_msgs.len(), 1);
2474
2475        Ok(())
2476    }
2477
2478    #[test]
2479    fn set_code() -> Result<()> {
2480        let params = make_default_params();
2481        let config = make_default_config();
2482
2483        let orig_data = CellBuilder::build_from(u32::MIN)?;
2484        let final_data = CellBuilder::build_from(u32::MAX)?;
2485
2486        let temp_code = Boc::decode(tvmasm!("NOP NOP"))?;
2487        let final_code = Boc::decode(tvmasm!("NOP"))?;
2488
2489        let mut state = ExecutorState::new_active(
2490            &params,
2491            &config,
2492            &STUB_ADDR,
2493            OK_BALANCE,
2494            orig_data,
2495            tvmasm!("ACCEPT"),
2496        );
2497
2498        let compute_phase = stub_compute_phase(OK_GAS);
2499        let prev_total_fees = state.total_fees;
2500        let prev_balance = state.balance.clone();
2501
2502        let actions = make_action_list([
2503            OutAction::SetCode {
2504                new_code: temp_code,
2505            },
2506            OutAction::SetCode {
2507                new_code: final_code.clone(),
2508            },
2509        ]);
2510
2511        let AccountState::Active(mut new_state) = state.state.clone() else {
2512            panic!("unexpected account state");
2513        };
2514        new_state.data = Some(final_data.clone());
2515
2516        let ActionPhaseFull {
2517            action_phase,
2518            action_fine,
2519            state_exceeds_limits,
2520            bounce,
2521        } = state.action_phase(ActionPhaseContext {
2522            received_message: None,
2523            original_balance: original_balance(&state, &compute_phase),
2524            new_state,
2525            actions: actions.clone(),
2526            compute_phase: &compute_phase,
2527            inspector: None,
2528        })?;
2529
2530        assert_eq!(action_phase, ActionPhase {
2531            total_actions: 2,
2532            special_actions: 2,
2533            action_list_hash: *actions.repr_hash(),
2534            ..empty_action_phase()
2535        });
2536        assert_eq!(action_fine, Tokens::ZERO);
2537        assert!(!state_exceeds_limits);
2538        assert!(!bounce);
2539        assert_eq!(state.end_status, AccountStatus::Active);
2540        assert_eq!(
2541            state.state,
2542            AccountState::Active(StateInit {
2543                code: Some(final_code),
2544                data: Some(final_data),
2545                ..Default::default()
2546            })
2547        );
2548        assert_eq!(state.total_fees, prev_total_fees);
2549        assert_eq!(state.balance, prev_balance);
2550        Ok(())
2551    }
2552
2553    #[test]
2554    fn invalid_dst_addr() -> Result<()> {
2555        let params = make_default_params();
2556        let config = make_default_config();
2557
2558        let targets = [
2559            // Unknown workchain.
2560            IntAddr::Std(StdAddr::new(123, HashBytes::ZERO)),
2561            // With anycast.
2562            IntAddr::Std({
2563                let mut addr = STUB_ADDR;
2564                let mut b = CellBuilder::new();
2565                b.store_u16(0xaabb)?;
2566                addr.anycast = Some(Box::new(Anycast::from_slice(&b.as_data_slice())?));
2567                addr
2568            }),
2569            // Var addr.
2570            IntAddr::Var(VarAddr {
2571                anycast: None,
2572                address_len: Uint9::new(80),
2573                workchain: 0,
2574                address: vec![0; 10],
2575            }),
2576        ];
2577
2578        for dst in targets {
2579            let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2580
2581            let compute_phase = stub_compute_phase(OK_GAS);
2582            let prev_total_fees = state.total_fees;
2583            let prev_balance = state.balance.clone();
2584            let prev_end_lt = state.end_lt;
2585
2586            let actions = make_action_list([OutAction::SendMsg {
2587                mode: SendMsgFlags::ALL_BALANCE,
2588                out_msg: make_relaxed_message(
2589                    RelaxedIntMsgInfo {
2590                        dst,
2591                        ..Default::default()
2592                    },
2593                    None,
2594                    None,
2595                ),
2596            }]);
2597
2598            let ActionPhaseFull {
2599                action_phase,
2600                action_fine,
2601                state_exceeds_limits,
2602                bounce,
2603            } = state.action_phase(ActionPhaseContext {
2604                received_message: None,
2605                original_balance: original_balance(&state, &compute_phase),
2606                new_state: StateInit::default(),
2607                actions: actions.clone(),
2608                compute_phase: &compute_phase,
2609                inspector: None,
2610            })?;
2611
2612            assert_eq!(action_phase, ActionPhase {
2613                success: false,
2614                total_actions: 1,
2615                messages_created: 0,
2616                result_code: ResultCode::InvalidDstAddr as _,
2617                result_arg: Some(0),
2618                action_list_hash: *actions.repr_hash(),
2619                ..empty_action_phase()
2620            });
2621            assert_eq!(action_fine, Tokens::ZERO);
2622            assert!(!state_exceeds_limits);
2623            assert!(!bounce);
2624
2625            assert!(state.out_msgs.is_empty());
2626            assert_eq!(state.end_lt, prev_end_lt);
2627
2628            assert_eq!(state.total_fees, prev_total_fees);
2629            assert_eq!(state.balance, prev_balance);
2630        }
2631        Ok(())
2632    }
2633
2634    #[test]
2635    fn cant_pay_fwd_fee() -> Result<()> {
2636        let params = make_default_params();
2637        let config = make_default_config();
2638        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(50000));
2639
2640        let compute_phase = stub_compute_phase(OK_GAS);
2641        let prev_balance = state.balance.clone();
2642        let prev_total_fee = state.total_fees;
2643        let prev_end_lt = state.end_lt;
2644
2645        let actions = make_action_list([OutAction::SendMsg {
2646            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2647            out_msg: make_relaxed_message(
2648                RelaxedIntMsgInfo {
2649                    value: CurrencyCollection::ZERO,
2650                    dst: STUB_ADDR.into(),
2651                    ..Default::default()
2652                },
2653                None,
2654                Some({
2655                    let mut b = CellBuilder::new();
2656                    b.store_reference(Cell::empty_cell())?;
2657                    b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
2658                    b
2659                }),
2660            ),
2661        }]);
2662
2663        let ActionPhaseFull {
2664            action_phase,
2665            action_fine,
2666            state_exceeds_limits,
2667            bounce,
2668        } = state.action_phase(ActionPhaseContext {
2669            received_message: None,
2670            original_balance: original_balance(&state, &compute_phase),
2671            new_state: StateInit::default(),
2672            actions: actions.clone(),
2673            compute_phase: &compute_phase,
2674            inspector: None,
2675        })?;
2676
2677        assert_eq!(action_phase, ActionPhase {
2678            success: false,
2679            no_funds: true,
2680            result_code: ResultCode::NotEnoughBalance as _,
2681            result_arg: Some(0),
2682            total_actions: 1,
2683            total_action_fees: Some(prev_balance.tokens),
2684            action_list_hash: *actions.repr_hash(),
2685            ..empty_action_phase()
2686        });
2687        assert_eq!(action_fine, prev_balance.tokens);
2688        assert!(!state_exceeds_limits);
2689        assert!(!bounce);
2690
2691        assert_eq!(state.balance, CurrencyCollection::ZERO);
2692        assert_eq!(
2693            state.total_fees,
2694            prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2695        );
2696        assert!(state.out_msgs.is_empty());
2697        assert_eq!(state.end_lt, prev_end_lt);
2698        Ok(())
2699    }
2700
2701    #[test]
2702    fn rewrite_message() -> Result<()> {
2703        let params = make_default_params();
2704        let config = make_default_config();
2705        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
2706
2707        let compute_phase = stub_compute_phase(OK_GAS);
2708        let prev_balance = state.balance.clone();
2709        let prev_total_fee = state.total_fees;
2710        let prev_end_lt = state.end_lt;
2711
2712        let msg_body = {
2713            let mut b = CellBuilder::new();
2714            b.store_zeros(600)?;
2715            b.store_reference(Cell::empty_cell())?;
2716            b
2717        };
2718
2719        let actions = make_action_list([OutAction::SendMsg {
2720            mode: SendMsgFlags::PAY_FEE_SEPARATELY,
2721            out_msg: make_relaxed_message(
2722                RelaxedIntMsgInfo {
2723                    value: CurrencyCollection::ZERO,
2724                    dst: STUB_ADDR.into(),
2725                    ..Default::default()
2726                },
2727                None,
2728                Some(msg_body.clone()),
2729            ),
2730        }]);
2731
2732        let ActionPhaseFull {
2733            action_phase,
2734            action_fine,
2735            state_exceeds_limits,
2736            bounce,
2737        } = state.action_phase(ActionPhaseContext {
2738            received_message: None,
2739            original_balance: original_balance(&state, &compute_phase),
2740            new_state: StateInit::default(),
2741            actions: actions.clone(),
2742            compute_phase: &compute_phase,
2743            inspector: None,
2744        })?;
2745
2746        assert_eq!(state.out_msgs.len(), 1);
2747        let last_msg = state.out_msgs.last().unwrap();
2748        let msg = last_msg.load()?;
2749        assert_eq!(
2750            msg.layout,
2751            Some(MessageLayout {
2752                init_to_cell: false,
2753                body_to_cell: true,
2754            })
2755        );
2756        assert_eq!(msg.body.1, msg_body.build()?);
2757
2758        let MsgInfo::Int(info) = msg.info else {
2759            panic!("expected an internal message");
2760        };
2761
2762        let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
2763            bit_count: 600,
2764            cell_count: 2,
2765        });
2766        let first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
2767
2768        assert_eq!(action_phase, ActionPhase {
2769            total_actions: 1,
2770            messages_created: 1,
2771            total_fwd_fees: Some(expected_fwd_fees),
2772            total_action_fees: Some(first_frac),
2773            action_list_hash: *actions.repr_hash(),
2774            total_message_size: compute_full_stats(last_msg, &params),
2775            ..empty_action_phase()
2776        });
2777        assert_eq!(action_fine, Tokens::ZERO);
2778        assert!(!state_exceeds_limits);
2779        assert!(!bounce);
2780
2781        assert_eq!(state.end_lt, prev_end_lt + 1);
2782        assert_eq!(
2783            state.total_fees,
2784            prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
2785        );
2786        assert_eq!(state.balance.other, prev_balance.other);
2787        assert_eq!(
2788            state.balance.tokens,
2789            prev_balance.tokens - info.value.tokens - expected_fwd_fees
2790        );
2791        Ok(())
2792    }
2793
2794    #[test]
2795    fn change_lib() -> Result<()> {
2796        struct TestCase {
2797            libraries: Dict<HashBytes, SimpleLib>,
2798            target: Dict<HashBytes, SimpleLib>,
2799            changes: Vec<(ChangeLibraryMode, LibRef)>,
2800            diff: Vec<PublicLibraryChange>,
2801        }
2802
2803        fn make_lib(id: u32) -> Cell {
2804            CellBuilder::build_from(id).unwrap()
2805        }
2806
2807        fn make_lib_hash(id: u32) -> HashBytes {
2808            *CellBuilder::build_from(id).unwrap().repr_hash()
2809        }
2810
2811        fn make_libs<I>(items: I) -> Dict<HashBytes, SimpleLib>
2812        where
2813            I: IntoIterator<Item = (u32, bool)>,
2814        {
2815            let mut items = items
2816                .into_iter()
2817                .map(|(id, public)| {
2818                    let root = make_lib(id);
2819                    (*root.repr_hash(), SimpleLib { root, public })
2820                })
2821                .collect::<Vec<_>>();
2822            items.sort_by_key(|(a, _)| *a);
2823            Dict::try_from_sorted_slice(&items).unwrap()
2824        }
2825
2826        let params = make_default_params();
2827        let config = make_default_config();
2828
2829        let test_cases = vec![
2830            // The simplest case with no libs.
2831            TestCase {
2832                libraries: Dict::new(),
2833                target: Dict::new(),
2834                changes: Vec::new(),
2835                diff: Vec::new(),
2836            },
2837            // Add private libs.
2838            TestCase {
2839                libraries: Dict::new(),
2840                target: make_libs([(123, false)]),
2841                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2842                diff: Vec::new(),
2843            },
2844            TestCase {
2845                libraries: Dict::new(),
2846                target: make_libs([(123, false), (234, false)]),
2847                changes: vec![
2848                    (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123))),
2849                    (ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(234))),
2850                ],
2851                diff: Vec::new(),
2852            },
2853            TestCase {
2854                libraries: make_libs([(123, false)]),
2855                target: make_libs([(123, false)]),
2856                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2857                diff: Vec::new(),
2858            },
2859            TestCase {
2860                libraries: make_libs([(123, false)]),
2861                target: make_libs([(123, false)]),
2862                changes: vec![(
2863                    ChangeLibraryMode::ADD_PRIVATE,
2864                    LibRef::Hash(make_lib_hash(123)),
2865                )],
2866                diff: Vec::new(),
2867            },
2868            // Add public libs.
2869            TestCase {
2870                libraries: Dict::new(),
2871                target: make_libs([(123, true)]),
2872                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2873                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2874            },
2875            TestCase {
2876                libraries: Dict::new(),
2877                target: make_libs([(123, true), (234, true)]),
2878                changes: vec![
2879                    (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123))),
2880                    (ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(234))),
2881                ],
2882                diff: vec![
2883                    PublicLibraryChange::Add(make_lib(123)),
2884                    PublicLibraryChange::Add(make_lib(234)),
2885                ],
2886            },
2887            TestCase {
2888                libraries: make_libs([(123, true)]),
2889                target: make_libs([(123, true)]),
2890                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2891                diff: Vec::new(),
2892            },
2893            TestCase {
2894                libraries: make_libs([(123, true)]),
2895                target: make_libs([(123, true)]),
2896                changes: vec![(
2897                    ChangeLibraryMode::ADD_PUBLIC,
2898                    LibRef::Hash(make_lib_hash(123)),
2899                )],
2900                diff: Vec::new(),
2901            },
2902            // Remove public libs.
2903            TestCase {
2904                libraries: Dict::new(),
2905                target: Dict::new(),
2906                // Non-existing by cell.
2907                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2908                diff: Vec::new(),
2909            },
2910            TestCase {
2911                libraries: Dict::new(),
2912                target: Dict::new(),
2913                // Non-existing by hash.
2914                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2915                diff: Vec::new(),
2916            },
2917            TestCase {
2918                libraries: make_libs([(123, false)]),
2919                target: Dict::new(),
2920                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2921                diff: Vec::new(),
2922            },
2923            TestCase {
2924                libraries: make_libs([(123, false)]),
2925                target: Dict::new(),
2926                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
2927                diff: Vec::new(),
2928            },
2929            TestCase {
2930                libraries: make_libs([(123, true)]),
2931                target: Dict::new(),
2932                changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
2933                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2934            },
2935            // Update public libs.
2936            TestCase {
2937                libraries: make_libs([(123, false)]),
2938                target: make_libs([(123, true)]),
2939                changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
2940                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2941            },
2942            TestCase {
2943                libraries: make_libs([(123, false)]),
2944                target: make_libs([(123, true)]),
2945                changes: vec![(
2946                    ChangeLibraryMode::ADD_PUBLIC,
2947                    LibRef::Hash(make_lib_hash(123)),
2948                )],
2949                diff: vec![PublicLibraryChange::Add(make_lib(123))],
2950            },
2951            TestCase {
2952                libraries: make_libs([(123, true)]),
2953                target: make_libs([(123, false)]),
2954                changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
2955                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2956            },
2957            TestCase {
2958                libraries: make_libs([(123, true)]),
2959                target: make_libs([(123, false)]),
2960                changes: vec![(
2961                    ChangeLibraryMode::ADD_PRIVATE,
2962                    LibRef::Hash(make_lib_hash(123)),
2963                )],
2964                diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
2965            },
2966        ];
2967
2968        for TestCase {
2969            libraries,
2970            target,
2971            changes,
2972            diff,
2973        } in test_cases
2974        {
2975            let mut state = ExecutorState::new_active(
2976                &params,
2977                &config,
2978                &StdAddr::new(-1, HashBytes::ZERO),
2979                OK_BALANCE,
2980                Cell::empty_cell(),
2981                tvmasm!("ACCEPT"),
2982            );
2983
2984            let target_state_init = match &mut state.state {
2985                AccountState::Active(state_init) => {
2986                    state_init.libraries = libraries;
2987                    StateInit {
2988                        libraries: target,
2989                        ..state_init.clone()
2990                    }
2991                }
2992                AccountState::Uninit | AccountState::Frozen(..) => panic!("invalid initial state"),
2993            };
2994
2995            let compute_phase = stub_compute_phase(OK_GAS);
2996            let prev_total_fees = state.total_fees;
2997            let prev_balance = state.balance.clone();
2998
2999            let change_count = changes.len();
3000            let actions = make_action_list(
3001                changes
3002                    .into_iter()
3003                    .map(|(mode, lib)| OutAction::ChangeLibrary { mode, lib }),
3004            );
3005
3006            let mut inspector = ExecutorInspector::default();
3007
3008            let ActionPhaseFull {
3009                action_phase,
3010                action_fine,
3011                state_exceeds_limits,
3012                bounce,
3013            } = state.action_phase(ActionPhaseContext {
3014                received_message: None,
3015                original_balance: original_balance(&state, &compute_phase),
3016                new_state: match state.state.clone() {
3017                    AccountState::Active(state_init) => state_init,
3018                    AccountState::Uninit | AccountState::Frozen(..) => Default::default(),
3019                },
3020                actions: actions.clone(),
3021                compute_phase: &compute_phase,
3022                inspector: Some(&mut inspector),
3023            })?;
3024
3025            assert_eq!(action_phase, ActionPhase {
3026                total_actions: change_count as u16,
3027                special_actions: change_count as u16,
3028                action_list_hash: *actions.repr_hash(),
3029                ..empty_action_phase()
3030            });
3031            assert_eq!(action_fine, Tokens::ZERO);
3032            assert!(!state_exceeds_limits);
3033            assert!(!bounce);
3034            assert_eq!(state.end_status, AccountStatus::Active);
3035            assert_eq!(state.state, AccountState::Active(target_state_init));
3036            assert_eq!(state.total_fees, prev_total_fees);
3037            assert_eq!(state.balance, prev_balance);
3038            assert_eq!(inspector.public_libs_diff, diff);
3039        }
3040        Ok(())
3041    }
3042}