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