Skip to main content

tycho_executor/phase/
action.rs

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