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