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