use anyhow::Result;
use tycho_types::cell::{CellTreeStats, Lazy};
use tycho_types::error::Error;
use tycho_types::models::{
AccountState, AccountStatus, AccountStatusChange, ActionPhase, ChangeLibraryMode,
CurrencyCollection, ExecutedComputePhase, ExtraCurrencyCollection, LibRef, OutAction,
OwnedMessage, OwnedRelaxedMessage, RelaxedMsgInfo, ReserveCurrencyFlags, SendMsgFlags,
SimpleLib, StateInit, StorageUsedShort,
};
use tycho_types::num::{Tokens, VarUint56};
use tycho_types::prelude::*;
use crate::phase::receive::ReceivedMessage;
use crate::util::{
ExtStorageStat, StateLimitsResult, StorageStatLimits, check_rewrite_dst_addr,
check_rewrite_src_addr, check_state_limits, check_state_limits_diff,
};
use crate::{ExecutorInspector, ExecutorState, PublicLibraryChange};
pub struct ActionPhaseContext<'a, 'e> {
pub received_message: Option<&'a mut ReceivedMessage>,
pub original_balance: CurrencyCollection,
pub new_state: StateInit,
pub actions: Cell,
pub compute_phase: &'a ExecutedComputePhase,
pub inspector: Option<&'a mut ExecutorInspector<'e>>,
}
#[derive(Debug)]
pub struct ActionPhaseFull {
pub action_phase: ActionPhase,
pub action_fine: Tokens,
pub state_exceeds_limits: bool,
pub bounce: bool,
}
impl ExecutorState<'_> {
pub fn action_phase(&mut self, mut ctx: ActionPhaseContext<'_, '_>) -> Result<ActionPhaseFull> {
const MAX_ACTIONS: u16 = 255;
let mut res = ActionPhaseFull {
action_phase: ActionPhase {
success: false,
valid: false,
no_funds: false,
status_change: AccountStatusChange::Unchanged,
total_fwd_fees: None,
total_action_fees: None,
result_code: -1,
result_arg: None,
total_actions: 0,
special_actions: 0,
skipped_actions: 0,
messages_created: 0,
action_list_hash: *ctx.actions.repr_hash(),
total_message_size: StorageUsedShort::ZERO,
},
action_fine: Tokens::ZERO,
state_exceeds_limits: false,
bounce: false,
};
let mut action_idx = 0u16;
let mut list = Vec::new();
let mut actions = ctx.actions.as_ref();
loop {
if actions.is_exotic() {
res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
res.action_phase.result_arg = Some(action_idx as _);
res.action_phase.valid = false;
return Ok(res);
}
let mut cs = actions.as_slice_allow_exotic();
if cs.is_empty() {
break;
}
list.push(actions);
actions = match cs.load_reference() {
Ok(child) => child,
Err(_) => {
res.action_phase.result_code = ResultCode::ActionListInvalid as i32;
res.action_phase.result_arg = Some(action_idx as _);
res.action_phase.valid = false;
return Ok(res);
}
};
action_idx += 1;
if action_idx > MAX_ACTIONS {
res.action_phase.result_code = ResultCode::TooManyActions as i32;
res.action_phase.result_arg = Some(action_idx as _);
res.action_phase.valid = false;
return Ok(res);
}
}
res.action_phase.total_actions = action_idx;
let mut parsed_list = Vec::with_capacity(list.len());
for (action_idx, item) in list.into_iter().rev().enumerate() {
let mut cs = item.as_slice_allow_exotic();
cs.load_reference().ok();
let mut cs_parsed = cs;
if let Ok(item) = OutAction::load_from(&mut cs_parsed)
&& cs_parsed.is_empty()
{
parsed_list.push(Some(item));
continue;
}
if cs.size_bits() >= 40 && cs.load_u32()? == OutAction::TAG_SEND_MSG {
let mode = SendMsgFlags::from_bits_retain(cs.load_u8()?);
if mode.contains(SendMsgFlags::IGNORE_ERROR) {
res.action_phase.skipped_actions += 1;
parsed_list.push(None);
continue;
} else if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
res.bounce = true;
}
}
res.action_phase.result_code = ResultCode::ActionInvalid as i32;
res.action_phase.result_arg = Some(action_idx as _);
res.action_phase.valid = false;
return Ok(res);
}
res.action_phase.valid = true;
let mut action_ctx = ActionContext {
need_bounce_on_fail: false,
strict_extra_currency: self.params.strict_extra_currency,
received_message: ctx.received_message,
original_balance: &ctx.original_balance,
remaining_balance: self.balance.clone(),
reserved_balance: CurrencyCollection::ZERO,
action_fine: &mut res.action_fine,
new_state: &mut ctx.new_state,
end_lt: self.end_lt,
out_msgs: Vec::new(),
delete_account: false,
public_libs_diff: ctx.inspector.is_some().then(Vec::new),
compute_phase: ctx.compute_phase,
action_phase: &mut res.action_phase,
};
for (action_idx, action) in parsed_list.into_iter().enumerate() {
let Some(action) = action else {
continue;
};
action_ctx.need_bounce_on_fail = false;
action_ctx.action_phase.result_code = -1;
action_ctx.action_phase.result_arg = Some(action_idx as _);
let action = match action {
OutAction::SendMsg { mode, out_msg } => {
let mut rewrite = None;
loop {
match self.do_send_message(mode, &out_msg, &mut action_ctx, rewrite) {
Ok(SendMsgResult::Sent) => break Ok(()),
Ok(SendMsgResult::Rewrite(r)) => rewrite = Some(r),
Err(e) => break Err(e),
}
}
}
OutAction::SetCode { new_code } => self.do_set_code(new_code, &mut action_ctx),
OutAction::ReserveCurrency { mode, value } => {
self.do_reserve_currency(mode, value, &mut action_ctx)
}
OutAction::ChangeLibrary { mode, lib } => {
self.do_change_library(mode, lib, &mut action_ctx)
}
};
if let Err(ActionFailed) = action {
let result_code = &mut action_ctx.action_phase.result_code;
if *result_code == -1 {
*result_code = ResultCode::ActionInvalid as i32;
}
if *result_code == ResultCode::NotEnoughBalance as i32
|| *result_code == ResultCode::NotEnoughExtraBalance as i32
{
action_ctx.action_phase.no_funds = true;
}
action_ctx.apply_fine_on_error(
&mut self.balance,
&mut self.total_fees,
self.params.charge_action_fees_on_fail,
)?;
res.bounce |= action_ctx.need_bounce_on_fail;
return Ok(res);
}
}
if !self.is_special {
let limits = &self.config.size_limits;
let is_masterchain = self.address.is_masterchain();
let check = match &self.state {
AccountState::Active(current_state) => check_state_limits_diff(
current_state,
action_ctx.new_state,
limits,
is_masterchain,
&mut self.cached_storage_stat,
),
AccountState::Uninit | AccountState::Frozen(_) => check_state_limits(
action_ctx.new_state.code.as_ref(),
action_ctx.new_state.data.as_ref(),
&action_ctx.new_state.libraries,
limits,
is_masterchain,
&mut self.cached_storage_stat,
),
};
if matches!(check, StateLimitsResult::Exceeds) {
action_ctx.apply_fine_on_error(
&mut self.balance,
&mut self.total_fees,
self.params.charge_action_fees_on_fail,
)?;
res.bounce |= action_ctx.need_bounce_on_fail;
res.action_phase.result_code = ResultCode::StateOutOfLimits as i32;
res.state_exceeds_limits = true;
return Ok(res);
}
}
if !action_ctx.action_fine.is_zero() {
action_ctx
.action_phase
.total_action_fees
.get_or_insert_default()
.try_add_assign(*action_ctx.action_fine)?;
}
action_ctx
.remaining_balance
.try_add_assign(&action_ctx.reserved_balance)?;
action_ctx.action_phase.result_code = 0;
action_ctx.action_phase.result_arg = None;
action_ctx.action_phase.success = true;
if action_ctx.delete_account {
if self.params.strict_extra_currency {
debug_assert!(action_ctx.remaining_balance.tokens.is_zero());
} else {
debug_assert!(action_ctx.remaining_balance.is_zero());
}
action_ctx.action_phase.status_change = AccountStatusChange::Deleted;
self.end_status = if action_ctx.remaining_balance.is_zero() {
AccountStatus::NotExists
} else {
AccountStatus::Uninit
};
self.cached_storage_stat = None;
}
if let Some(fees) = action_ctx.action_phase.total_action_fees {
self.total_fees.try_add_assign(fees)?;
}
self.balance = action_ctx.remaining_balance;
if let Some(inspector) = ctx.inspector {
inspector.public_libs_diff = action_ctx.public_libs_diff.unwrap_or_default();
}
self.out_msgs = action_ctx.out_msgs;
self.end_lt = action_ctx.end_lt;
self.state = AccountState::Active(ctx.new_state);
Ok(res)
}
fn do_send_message(
&self,
mode: SendMsgFlags,
out_msg: &Lazy<OwnedRelaxedMessage>,
ctx: &mut ActionContext<'_>,
mut rewrite: Option<MessageRewrite>,
) -> Result<SendMsgResult, ActionFailed> {
const MASK: u8 = SendMsgFlags::all().bits();
const INVALID_MASK: SendMsgFlags =
SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::WITH_REMAINING_BALANCE);
const EXT_MSG_MASK: u8 = SendMsgFlags::PAY_FEE_SEPARATELY
.union(SendMsgFlags::IGNORE_ERROR)
.union(SendMsgFlags::BOUNCE_ON_ERROR)
.bits();
const DELETE_MASK: SendMsgFlags =
SendMsgFlags::ALL_BALANCE.union(SendMsgFlags::DELETE_IF_EMPTY);
if mode.contains(SendMsgFlags::BOUNCE_ON_ERROR) {
ctx.need_bounce_on_fail = true;
}
let skip_invalid = mode.contains(SendMsgFlags::IGNORE_ERROR);
let check_skip_invalid = |e: ResultCode, ctx: &mut ActionContext<'_>| {
if skip_invalid {
ctx.action_phase.skipped_actions += 1;
Ok(SendMsgResult::Sent)
} else {
ctx.action_phase.result_code = e as i32;
Err(ActionFailed)
}
};
if mode.bits() & !MASK != 0 || mode.contains(INVALID_MASK) {
return check_skip_invalid(ResultCode::ActionInvalid, ctx);
}
if out_msg.is_exotic() {
return Err(ActionFailed);
}
let mut relaxed_info;
let mut state_init_cs;
let mut body_cs;
{
let mut cs = out_msg.as_slice_allow_exotic();
relaxed_info = RelaxedMsgInfo::load_from(&mut cs)?;
state_init_cs = match load_state_init_as_slice(&mut cs, rewrite.is_none()) {
Ok(cs) => cs,
Err(LoadStateInitError::BadStateInit) => {
return check_skip_invalid(ResultCode::ActionInvalid, ctx);
}
Err(LoadStateInitError::Other) => return Err(ActionFailed),
};
body_cs = load_body_as_slice(&mut cs)?;
if !cs.is_empty() {
return Err(ActionFailed);
}
}
let rewritten_state_init_cb;
if rewrite.is_some() {
if state_init_cs.size_refs() >= 2 {
rewritten_state_init_cb = rewrite_state_init_to_cell(state_init_cs);
state_init_cs = rewritten_state_init_cb.as_full_slice();
} else {
rewrite = Some(MessageRewrite::BodyToCell);
}
}
let rewritten_body_cs;
if let Some(MessageRewrite::BodyToCell) = rewrite
&& body_cs.size_bits() > 1
&& !body_cs.get_bit(0).unwrap()
{
rewritten_body_cs = rewrite_body_to_cell(body_cs);
body_cs = rewritten_body_cs.as_full_slice();
}
let mut use_mc_prices = self.address.is_masterchain();
match &mut relaxed_info {
RelaxedMsgInfo::Int(info) => {
if !check_rewrite_src_addr(&self.address, &mut info.src) {
return check_skip_invalid(ResultCode::InvalidSrcAddr, ctx);
};
if !check_rewrite_dst_addr(&self.config.workchains, &mut info.dst) {
return check_skip_invalid(ResultCode::InvalidDstAddr, ctx);
}
use_mc_prices |= info.dst.is_masterchain();
if self.params.strict_extra_currency {
match normalize_extra_balance(
std::mem::take(&mut info.value.other),
MAX_MSG_EXTRA_CURRENCIES,
) {
Ok(other) => info.value.other = other,
Err(BalanceExtraError::InvalidDict(_)) => {
return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
}
Err(BalanceExtraError::OutOfLimit) => {
return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
}
}
}
info.fwd_fee = Tokens::ZERO;
info.created_at = self.params.block_unixtime;
info.created_lt = ctx.end_lt;
info.ihr_disabled = true;
info.bounced = false;
}
RelaxedMsgInfo::ExtOut(info) => {
if mode.bits() & !EXT_MSG_MASK != 0 {
return check_skip_invalid(ResultCode::ActionInvalid, ctx);
}
if !check_rewrite_src_addr(&self.address, &mut info.src) {
return check_skip_invalid(ResultCode::InvalidSrcAddr, ctx);
}
info.created_at = self.params.block_unixtime;
info.created_lt = ctx.end_lt;
}
};
let prices = self.config.fwd_prices(use_mc_prices);
let mut max_cell_count = self.config.size_limits.max_msg_cells;
let fine_per_cell;
if self.is_special {
fine_per_cell = 0;
} else {
fine_per_cell = (prices.cell_price >> 16) / 4;
let mut funds = ctx.remaining_balance.tokens;
if let RelaxedMsgInfo::Int(info) = &relaxed_info
&& !mode.contains(SendMsgFlags::ALL_BALANCE)
&& !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY)
{
let mut new_funds = info.value.tokens;
if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE)
&& (|| {
let msg_balance_remaining = match &ctx.received_message {
Some(msg) => msg.balance_remaining.tokens,
None => Tokens::ZERO,
};
new_funds.try_add_assign(msg_balance_remaining)?;
new_funds.try_sub_assign(ctx.compute_phase.gas_fees)?;
new_funds.try_sub_assign(*ctx.action_fine)?;
Ok::<_, tycho_types::error::Error>(())
})()
.is_err()
{
return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
}
funds = std::cmp::min(funds, new_funds);
}
if funds < Tokens::new(max_cell_count as u128 * fine_per_cell as u128) {
debug_assert_ne!(fine_per_cell, 0);
max_cell_count = (funds.into_inner() / fine_per_cell as u128)
.try_into()
.unwrap_or(u32::MAX);
}
}
let collect_fine = |cells: u32, ctx: &mut ActionContext<'_>| {
let mut fine = Tokens::new(
fine_per_cell.saturating_mul(std::cmp::min(max_cell_count, cells) as u64) as _,
);
fine = std::cmp::min(fine, ctx.remaining_balance.tokens);
ctx.action_fine.try_add_assign(fine)?;
ctx.remaining_balance.try_sub_assign_tokens(fine)
};
let stats = 'stats: {
let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
bit_count: self.config.size_limits.max_msg_bits,
cell_count: max_cell_count,
});
'valid: {
for cell in state_init_cs.references() {
if !stats.add_cell(cell) {
break 'valid;
}
}
for cell in body_cs.references() {
if !stats.add_cell(cell) {
break 'valid;
}
}
if !self.params.strict_extra_currency
&& let RelaxedMsgInfo::Int(int) = &relaxed_info
&& let Some(cell) = int.value.other.as_dict().root()
&& !stats.add_cell(cell.as_ref())
{
break 'valid;
}
break 'stats stats.stats();
}
collect_fine(stats.cells, ctx)?;
return check_skip_invalid(ResultCode::MessageOutOfLimits, ctx);
};
let check_skip_invalid = move |e: ResultCode, ctx: &mut ActionContext<'_>| {
collect_fine(stats.cell_count as _, ctx)?;
check_skip_invalid(e, ctx)
};
let fwd_fee = if self.is_special {
Tokens::ZERO
} else {
prices.compute_fwd_fee(stats)
};
let msg;
let fees_collected;
match &mut relaxed_info {
RelaxedMsgInfo::Int(info) => {
let value_to_pay = match ctx.rewrite_message_value(&mut info.value, mode, fwd_fee) {
Ok(total_value) => total_value,
Err(_) => return check_skip_invalid(ResultCode::NotEnoughBalance, ctx),
};
if ctx.remaining_balance.tokens < value_to_pay {
return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
}
if self.params.strict_extra_currency
&& !check_extra_balance(&info.value.other, MAX_MSG_EXTRA_CURRENCIES)
{
return check_skip_invalid(ResultCode::TooManyExtraCurrencies, ctx);
}
if self.params.authority_marks_enabled
&& !self.is_marks_authority
&& let Some(marks) = &self.config.authority_marks
&& marks.has_authority_marks_in(&info.value)?
{
return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx);
}
let other = match ctx.remaining_balance.other.checked_sub(&info.value.other) {
Ok(other) => other,
Err(_) => return check_skip_invalid(ResultCode::NotEnoughExtraBalance, ctx),
};
fees_collected = prices.get_first_part(fwd_fee);
info.fwd_fee = fwd_fee - fees_collected;
msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
Ok(msg) => msg,
Err(_) => match MessageRewrite::next(rewrite) {
Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
},
};
if let Some(msg) = &mut ctx.received_message
&& (mode.contains(SendMsgFlags::ALL_BALANCE)
|| mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE))
{
if self.params.strict_extra_currency {
msg.balance_remaining.tokens = Tokens::ZERO;
} else {
msg.balance_remaining = CurrencyCollection::ZERO;
}
}
ctx.remaining_balance.tokens -= value_to_pay;
ctx.remaining_balance.other = other;
}
RelaxedMsgInfo::ExtOut(_) => {
if ctx.remaining_balance.tokens < fwd_fee {
return check_skip_invalid(ResultCode::NotEnoughBalance, ctx);
}
msg = match build_message(&relaxed_info, &state_init_cs, &body_cs) {
Ok(msg) => msg,
Err(_) => match MessageRewrite::next(rewrite) {
Some(rewrite) => return Ok(SendMsgResult::Rewrite(rewrite)),
None => return check_skip_invalid(ResultCode::FailedToFitMessage, ctx),
},
};
ctx.remaining_balance.tokens -= fwd_fee;
fees_collected = fwd_fee;
}
}
update_total_msg_stat(
&mut ctx.action_phase.total_message_size,
stats,
msg.bit_len(),
);
ctx.action_phase.messages_created += 1;
ctx.end_lt += 1;
ctx.out_msgs.push(msg);
*ctx.action_phase.total_action_fees.get_or_insert_default() += fees_collected;
*ctx.action_phase.total_fwd_fees.get_or_insert_default() += fwd_fee;
if mode.contains(DELETE_MASK) {
ctx.delete_account = if self.params.strict_extra_currency {
debug_assert!(ctx.remaining_balance.tokens.is_zero());
ctx.reserved_balance.tokens.is_zero()
} else {
debug_assert!(ctx.remaining_balance.is_zero());
ctx.reserved_balance.is_zero()
};
}
Ok(SendMsgResult::Sent)
}
fn do_set_code(&self, new_code: Cell, ctx: &mut ActionContext<'_>) -> Result<(), ActionFailed> {
ctx.new_state.code = Some(new_code);
ctx.action_phase.special_actions += 1;
Ok(())
}
fn do_reserve_currency(
&self,
mode: ReserveCurrencyFlags,
mut reserve: CurrencyCollection,
ctx: &mut ActionContext<'_>,
) -> Result<(), ActionFailed> {
const MASK: u8 = ReserveCurrencyFlags::all().bits();
if mode.contains(ReserveCurrencyFlags::BOUNCE_ON_ERROR) {
ctx.need_bounce_on_fail = true;
}
if mode.bits() & !MASK != 0 {
return Err(ActionFailed);
}
if self.params.strict_extra_currency && !reserve.other.is_empty() {
return Err(ActionFailed);
}
if mode.contains(ReserveCurrencyFlags::WITH_ORIGINAL_BALANCE) {
if mode.contains(ReserveCurrencyFlags::REVERSE) {
if self.params.strict_extra_currency {
reserve.tokens = ctx
.original_balance
.tokens
.checked_sub(reserve.tokens)
.ok_or(ActionFailed)?;
} else {
reserve = ctx.original_balance.checked_sub(&reserve)?;
}
} else if self.params.strict_extra_currency {
reserve.try_add_assign_tokens(ctx.original_balance.tokens)?;
} else {
reserve.try_add_assign(ctx.original_balance)?;
}
} else if mode.contains(ReserveCurrencyFlags::REVERSE) {
return Err(ActionFailed);
}
if mode.contains(ReserveCurrencyFlags::IGNORE_ERROR) {
reserve = reserve.checked_clamp(&ctx.remaining_balance)?;
}
let mut new_balance = CurrencyCollection {
tokens: match ctx.remaining_balance.tokens.checked_sub(reserve.tokens) {
Some(tokens) => tokens,
None => {
ctx.action_phase.result_code = ResultCode::NotEnoughBalance as i32;
return Err(ActionFailed);
}
},
other: match ctx.remaining_balance.other.checked_sub(&reserve.other) {
Ok(other) => other,
Err(_) => {
ctx.action_phase.result_code = ResultCode::NotEnoughExtraBalance as i32;
return Err(ActionFailed);
}
},
};
reserve.other.normalize()?;
if mode.contains(ReserveCurrencyFlags::ALL_BUT) {
if self.params.strict_extra_currency {
std::mem::swap(&mut new_balance.tokens, &mut reserve.tokens);
} else {
std::mem::swap(&mut new_balance, &mut reserve);
}
}
ctx.remaining_balance = new_balance;
ctx.reserved_balance.try_add_assign(&reserve)?;
ctx.action_phase.special_actions += 1;
Ok(())
}
fn do_change_library(
&self,
mode: ChangeLibraryMode,
mut lib: LibRef,
ctx: &mut ActionContext<'_>,
) -> Result<(), ActionFailed> {
const INVALID_MODE: ChangeLibraryMode = ChangeLibraryMode::from_bits_retain(
ChangeLibraryMode::ADD_PRIVATE.bits() | ChangeLibraryMode::ADD_PUBLIC.bits(),
);
if mode.contains(ChangeLibraryMode::BOUNCE_ON_ERROR) {
ctx.need_bounce_on_fail = true;
}
if mode.contains(INVALID_MODE) {
return Err(ActionFailed);
}
let hash = match &lib {
LibRef::Cell(cell) => cell.repr_hash(),
LibRef::Hash(hash) => hash,
};
let is_masterchain = self.address.is_masterchain();
let add_public = mode.contains(ChangeLibraryMode::ADD_PUBLIC);
if add_public || mode.contains(ChangeLibraryMode::ADD_PRIVATE) {
let mut was_public = None;
if let Ok(Some(prev)) = ctx.new_state.libraries.get(hash) {
if prev.public == add_public {
ctx.action_phase.special_actions += 1;
return Ok(());
} else {
was_public = Some(prev.public);
lib = LibRef::Cell(prev.root);
}
}
let LibRef::Cell(root) = lib else {
ctx.action_phase.result_code = ResultCode::NoLibCode as i32;
return Err(ActionFailed);
};
let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
bit_count: u32::MAX,
cell_count: self.config.size_limits.max_library_cells,
});
if !stats.add_cell(root.as_ref()) {
ctx.action_phase.result_code = ResultCode::LibOutOfLimits as i32;
return Err(ActionFailed);
}
match ctx.new_state.libraries.set(*root.repr_hash(), SimpleLib {
public: add_public,
root: root.clone(),
}) {
Ok(_) if is_masterchain => {
if let Some(diff) = &mut ctx.public_libs_diff {
match (was_public, add_public) {
(None, true) | (Some(false), true) => {
diff.push(PublicLibraryChange::Add(root))
}
(Some(true), false) => {
diff.push(PublicLibraryChange::Remove(*root.repr_hash()));
}
_ => {}
}
}
}
Ok(_) => {}
Err(_) => {
ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
return Err(ActionFailed);
}
}
} else {
match ctx.new_state.libraries.remove(hash) {
Ok(Some(lib)) if is_masterchain && lib.public => {
if let Some(diff) = &mut ctx.public_libs_diff {
diff.push(PublicLibraryChange::Remove(*hash));
}
}
Ok(_) => {}
Err(_) => {
ctx.action_phase.result_code = ResultCode::InvalidLibrariesDict as i32;
return Err(ActionFailed);
}
}
}
ctx.action_phase.special_actions += 1;
Ok(())
}
}
struct ActionContext<'a> {
need_bounce_on_fail: bool,
strict_extra_currency: bool,
received_message: Option<&'a mut ReceivedMessage>,
original_balance: &'a CurrencyCollection,
remaining_balance: CurrencyCollection,
reserved_balance: CurrencyCollection,
action_fine: &'a mut Tokens,
new_state: &'a mut StateInit,
end_lt: u64,
out_msgs: Vec<Lazy<OwnedMessage>>,
delete_account: bool,
public_libs_diff: Option<Vec<PublicLibraryChange>>,
compute_phase: &'a ExecutedComputePhase,
action_phase: &'a mut ActionPhase,
}
impl ActionContext<'_> {
fn apply_fine_on_error(
&mut self,
balance: &mut CurrencyCollection,
total_fees: &mut Tokens,
charge_action_fees: bool,
) -> Result<(), Error> {
if charge_action_fees {
self.action_fine
.try_add_assign(self.action_phase.total_action_fees.unwrap_or_default())?;
}
self.action_phase.total_fwd_fees = None;
self.action_phase.total_action_fees = Some(*self.action_fine).filter(|t| !t.is_zero());
balance.tokens.try_sub_assign(*self.action_fine)?;
total_fees.try_add_assign(*self.action_fine)
}
fn rewrite_message_value(
&self,
value: &mut CurrencyCollection,
mut mode: SendMsgFlags,
fees_total: Tokens,
) -> Result<Tokens, ActionFailed> {
if mode.contains(SendMsgFlags::ALL_BALANCE) {
if self.strict_extra_currency {
value.tokens = self.remaining_balance.tokens;
} else {
*value = self.remaining_balance.clone();
};
mode.remove(SendMsgFlags::PAY_FEE_SEPARATELY);
} else if mode.contains(SendMsgFlags::WITH_REMAINING_BALANCE) {
if let Some(msg) = &self.received_message {
if self.strict_extra_currency {
value.try_add_assign_tokens(msg.balance_remaining.tokens)?;
} else {
value.try_add_assign(&msg.balance_remaining)?;
}
}
if !mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
value.try_sub_assign_tokens(*self.action_fine)?;
value.try_sub_assign_tokens(self.compute_phase.gas_fees)?;
}
}
let mut total = value.tokens;
if mode.contains(SendMsgFlags::PAY_FEE_SEPARATELY) {
total.try_add_assign(fees_total)?;
} else {
value.tokens.try_sub_assign(fees_total)?;
}
Ok(total)
}
}
struct ActionFailed;
impl From<anyhow::Error> for ActionFailed {
#[inline]
fn from(_: anyhow::Error) -> Self {
Self
}
}
impl From<Error> for ActionFailed {
#[inline]
fn from(_: Error) -> Self {
Self
}
}
#[derive(Debug, Clone, Copy)]
enum SendMsgResult {
Sent,
Rewrite(MessageRewrite),
}
#[derive(Debug, Clone, Copy)]
enum MessageRewrite {
StateInitToCell,
BodyToCell,
}
impl MessageRewrite {
pub fn next(rewrite: Option<Self>) -> Option<Self> {
match rewrite {
None => Some(Self::StateInitToCell),
Some(Self::StateInitToCell) => Some(Self::BodyToCell),
Some(Self::BodyToCell) => None,
}
}
}
fn load_state_init_as_slice<'a>(
cs: &mut CellSlice<'a>,
check_libs: bool,
) -> Result<CellSlice<'a>, LoadStateInitError> {
let mut res_cs = *cs;
if cs.load_bit()? {
let libs = if cs.load_bit()? {
let state_root = cs.load_reference()?;
if state_root.is_exotic() {
return Err(LoadStateInitError::BadStateInit);
}
let mut cs = state_root.as_slice_allow_exotic();
let Ok(state) = StateInit::load_from(&mut cs) else {
return Err(LoadStateInitError::BadStateInit);
};
if !cs.is_empty() {
return Err(LoadStateInitError::BadStateInit);
}
state.libraries
} else {
match StateInit::load_from(cs) {
Ok(state) => state.libraries,
Err(_) => return Err(LoadStateInitError::BadStateInit),
}
};
if check_libs {
for item in libs.iter() {
let Ok((key, value)) = item else {
return Err(LoadStateInitError::BadStateInit);
};
if &key != value.root.repr_hash() {
return Err(LoadStateInitError::BadStateInit);
}
}
}
}
res_cs.skip_last(cs.size_bits(), cs.size_refs())?;
Ok(res_cs)
}
enum LoadStateInitError {
BadStateInit,
Other,
}
impl From<Error> for LoadStateInitError {
#[inline]
fn from(_: Error) -> Self {
Self::Other
}
}
fn load_body_as_slice<'a>(cs: &mut CellSlice<'a>) -> Result<CellSlice<'a>, Error> {
let res_cs = *cs;
if cs.load_bit()? {
cs.skip_first(0, 1)?;
} else {
cs.load_remaining();
}
Ok(res_cs)
}
fn rewrite_state_init_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
let prefix = cs.load_small_uint(2).unwrap();
debug_assert_eq!(prefix, 0b10);
let cell = CellBuilder::build_from(cs).unwrap();
let mut b = CellBuilder::new();
b.store_small_uint(0b11, 2).unwrap();
b.store_reference(cell).unwrap();
b
}
fn rewrite_body_to_cell(mut cs: CellSlice<'_>) -> CellBuilder {
let prefix = cs.load_bit().unwrap();
debug_assert!(!prefix);
let cell = CellBuilder::build_from(cs).unwrap();
let mut b = CellBuilder::new();
b.store_bit_one().unwrap();
b.store_reference(cell).unwrap();
b
}
fn build_message(
info: &RelaxedMsgInfo,
state_init_cs: &CellSlice<'_>,
body_cs: &CellSlice<'_>,
) -> Result<Lazy<OwnedMessage>, Error> {
CellBuilder::build_from((info, state_init_cs, body_cs)).map(|cell| {
unsafe { Lazy::from_raw_unchecked(cell) }
})
}
fn update_total_msg_stat(
total_message_size: &mut StorageUsedShort,
stats: CellTreeStats,
root_bits: u16,
) {
let bits_diff = VarUint56::new(stats.bit_count.saturating_add(root_bits as _));
let cells_diff = VarUint56::new(stats.cell_count.saturating_add(1));
total_message_size.bits = total_message_size.bits.saturating_add(bits_diff);
total_message_size.cells = total_message_size.cells.saturating_add(cells_diff);
}
fn check_extra_balance(value: &ExtraCurrencyCollection, limit: usize) -> bool {
for (i, entry) in value.as_dict().iter().enumerate() {
if i > limit || entry.is_err() {
return false;
}
}
true
}
fn normalize_extra_balance(
value: ExtraCurrencyCollection,
limit: usize,
) -> Result<ExtraCurrencyCollection, BalanceExtraError> {
let mut result = value.clone();
for (i, entry) in value.as_dict().iter().enumerate() {
if i > limit {
return Err(BalanceExtraError::OutOfLimit);
}
let (currency_id, other) = entry.map_err(BalanceExtraError::InvalidDict)?;
if other.is_zero() {
result
.as_dict_mut()
.remove(currency_id)
.map_err(BalanceExtraError::InvalidDict)?;
}
}
Ok(result)
}
enum BalanceExtraError {
OutOfLimit,
InvalidDict(#[allow(unused)] Error),
}
#[repr(i32)]
#[derive(Debug, thiserror::Error)]
enum ResultCode {
#[error("invalid action list")]
ActionListInvalid = 32,
#[error("too many actions")]
TooManyActions = 33,
#[error("invalid or unsupported action")]
ActionInvalid = 34,
#[error("invalid source address")]
InvalidSrcAddr = 35,
#[error("invalid destination address")]
InvalidDstAddr = 36,
#[error("not enough balance (base currency)")]
NotEnoughBalance = 37,
#[error("not enough balance (extra currency)")]
NotEnoughExtraBalance = 38,
#[error("failed to fit message into cell")]
FailedToFitMessage = 39,
#[error("message exceeds limits")]
MessageOutOfLimits = 40,
#[error("library code not found")]
NoLibCode = 41,
#[error("failed to change libraries dict")]
InvalidLibrariesDict = 42,
#[error("too many library cells")]
LibOutOfLimits = 43,
#[error("too many extra currencies")]
TooManyExtraCurrencies = 44,
#[error("state exceeds limits")]
StateOutOfLimits = 50,
}
const MAX_MSG_EXTRA_CURRENCIES: usize = 2;
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use tycho_asm_macros::tvmasm;
use tycho_types::merkle::MerkleProof;
use tycho_types::models::{
Anycast, AuthorityMarksConfig, IntAddr, MessageExtraFlags, MessageLayout, MsgInfo,
RelaxedIntMsgInfo, RelaxedMessage, StdAddr, VarAddr,
};
use tycho_types::num::{Uint9, VarUint248};
use super::*;
use crate::ExecutorParams;
use crate::tests::{make_custom_config, make_default_config, make_default_params};
const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
const OK_GAS: Tokens = Tokens::new(1_000_000);
fn stub_compute_phase(gas_fees: Tokens) -> ExecutedComputePhase {
ExecutedComputePhase {
success: true,
msg_state_used: false,
account_activated: false,
gas_fees,
gas_used: Default::default(),
gas_limit: Default::default(),
gas_credit: None,
mode: 0,
exit_code: 0,
exit_arg: None,
vm_steps: 0,
vm_init_state_hash: Default::default(),
vm_final_state_hash: Default::default(),
}
}
fn empty_action_phase() -> ActionPhase {
ActionPhase {
success: true,
valid: true,
no_funds: false,
status_change: AccountStatusChange::Unchanged,
total_fwd_fees: None,
total_action_fees: None,
result_code: 0,
result_arg: None,
total_actions: 0,
special_actions: 0,
skipped_actions: 0,
messages_created: 0,
action_list_hash: *Cell::empty_cell_ref().repr_hash(),
total_message_size: Default::default(),
}
}
fn make_action_list<I: IntoIterator<Item: Store>>(actions: I) -> Cell {
let mut root = Cell::default();
for action in actions {
root = CellBuilder::build_from((root, action)).unwrap();
}
root
}
fn make_relaxed_message(
info: impl Into<RelaxedMsgInfo>,
init: Option<StateInit>,
body: Option<CellBuilder>,
) -> Lazy<OwnedRelaxedMessage> {
let body = match &body {
None => Cell::empty_cell_ref().as_slice_allow_exotic(),
Some(cell) => cell.as_full_slice(),
};
Lazy::new(&RelaxedMessage {
info: info.into(),
init,
body,
layout: None,
})
.unwrap()
.cast_into()
}
fn compute_full_stats(msg: &Lazy<OwnedMessage>, params: &ExecutorParams) -> StorageUsedShort {
let msg = 'cell: {
if params.strict_extra_currency {
let mut parsed = msg.load().unwrap();
if let MsgInfo::Int(int) = &mut parsed.info
&& !int.value.other.is_empty()
{
int.value.other = ExtraCurrencyCollection::new();
break 'cell CellBuilder::build_from(parsed).unwrap();
}
}
msg.inner().clone()
};
let stats = {
let mut stats = ExtStorageStat::with_limits(StorageStatLimits::UNLIMITED);
assert!(stats.add_cell(msg.as_ref()));
stats.stats()
};
StorageUsedShort {
cells: VarUint56::new(stats.cell_count),
bits: VarUint56::new(stats.bit_count),
}
}
fn original_balance(
state: &ExecutorState<'_>,
compute_phase: &ExecutedComputePhase,
) -> CurrencyCollection {
state
.balance
.clone()
.checked_add(&compute_phase.gas_fees.into())
.unwrap()
}
#[test]
fn empty_actions() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: Cell::empty_cell(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, empty_action_phase());
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn too_many_actions() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let actions = make_action_list(
std::iter::repeat_with(|| OutAction::SetCode {
new_code: Cell::empty_cell(),
})
.take(300),
);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
valid: false,
result_code: ResultCode::TooManyActions as i32,
result_arg: Some(256),
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn invalid_action_list() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let actions = CellBuilder::build_from((
CellBuilder::build_from(MerkleProof::default())?,
OutAction::SetCode {
new_code: Cell::default(),
},
))?;
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
valid: false,
result_code: ResultCode::ActionListInvalid as i32,
result_arg: Some(1),
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn invalid_action() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let set_code_action = {
let mut b = CellBuilder::new();
OutAction::SetCode {
new_code: Cell::empty_cell(),
}
.store_into(&mut b, Cell::empty_context())?;
b
};
let invalid_action = {
let mut b = CellBuilder::new();
b.store_u32(0xdeafbeaf)?;
b
};
let actions = make_action_list([
set_code_action.as_full_slice(),
set_code_action.as_full_slice(),
invalid_action.as_full_slice(),
set_code_action.as_full_slice(),
set_code_action.as_full_slice(),
]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
valid: false,
result_code: ResultCode::ActionInvalid as i32,
result_arg: Some(2),
action_list_hash: *actions.repr_hash(),
total_actions: 5,
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn strict_reserve_extra_currency() -> Result<()> {
let mut params = make_default_params();
params.strict_extra_currency = true;
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let prev_balance = state.balance.clone();
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_end_lt = state.end_lt;
let actions = make_action_list([OutAction::ReserveCurrency {
mode: ReserveCurrencyFlags::empty(),
value: CurrencyCollection {
tokens: Tokens::ZERO,
other: BTreeMap::from_iter([(123u32, VarUint248::new(10))]).try_into()?,
},
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
valid: true,
result_code: ResultCode::ActionInvalid as i32,
result_arg: Some(0),
action_list_hash: *actions.repr_hash(),
total_actions: 1,
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn send_single_message() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let msg_value = Tokens::new(500_000_000);
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::empty(),
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: msg_value.into(),
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.out_msgs.len(), 1);
assert_eq!(state.end_lt, prev_end_lt + 1);
let last_msg = state.out_msgs.last().unwrap();
let msg_info = {
let msg = last_msg.load()?;
assert!(msg.init.is_none());
assert_eq!(msg.body, Default::default());
match msg.info {
MsgInfo::Int(info) => info,
e => panic!("unexpected msg info {e:?}"),
}
};
assert_eq!(msg_info.src, STUB_ADDR.into());
assert_eq!(msg_info.dst, STUB_ADDR.into());
assert!(msg_info.ihr_disabled);
assert!(!msg_info.bounce);
assert!(!msg_info.bounced);
assert_eq!(msg_info.created_at, params.block_unixtime);
assert_eq!(msg_info.created_lt, prev_end_lt);
let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(msg_info.value, (msg_value - expected_fwd_fees).into());
assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
assert_eq!(action_phase, ActionPhase {
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(expected_first_frac),
total_actions: 1,
messages_created: 1,
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
..empty_action_phase()
});
assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - msg_value);
Ok(())
}
#[test]
fn send_all_balance() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection::ZERO,
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.out_msgs.len(), 1);
assert_eq!(state.end_lt, prev_end_lt + 1);
let last_msg = state.out_msgs.last().unwrap();
let msg_info = {
let msg = last_msg.load()?;
assert!(msg.init.is_none());
assert_eq!(msg.body, Default::default());
match msg.info {
MsgInfo::Int(info) => info,
e => panic!("unexpected msg info {e:?}"),
}
};
assert_eq!(msg_info.src, STUB_ADDR.into());
assert_eq!(msg_info.dst, STUB_ADDR.into());
assert!(msg_info.ihr_disabled);
assert!(!msg_info.bounce);
assert!(!msg_info.bounced);
assert_eq!(msg_info.created_at, params.block_unixtime);
assert_eq!(msg_info.created_lt, prev_end_lt);
let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(
msg_info.value,
(prev_balance.tokens - expected_fwd_fees).into()
);
assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
assert_eq!(action_phase, ActionPhase {
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(expected_first_frac),
total_actions: 1,
messages_created: 1,
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
..empty_action_phase()
});
assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
assert_eq!(state.balance, CurrencyCollection::ZERO);
Ok(())
}
#[test]
fn strict_send_all_balance() -> Result<()> {
let mut params = make_default_params();
params.strict_extra_currency = true;
let config = make_default_config();
let mut state =
ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
});
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let sent_value = CurrencyCollection {
tokens: Tokens::ZERO,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
};
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: sent_value.clone(),
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.out_msgs.len(), 1);
assert_eq!(state.end_lt, prev_end_lt + 1);
let last_msg = state.out_msgs.last().unwrap();
let msg_info = {
let msg = last_msg.load()?;
assert!(msg.init.is_none());
assert_eq!(msg.body, Default::default());
match msg.info {
MsgInfo::Int(info) => info,
e => panic!("unexpected msg info {e:?}"),
}
};
assert_eq!(msg_info.src, STUB_ADDR.into());
assert_eq!(msg_info.dst, STUB_ADDR.into());
assert!(msg_info.ihr_disabled);
assert!(!msg_info.bounce);
assert!(!msg_info.bounced);
assert_eq!(msg_info.created_at, params.block_unixtime);
assert_eq!(msg_info.created_lt, prev_end_lt);
let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(msg_info.value, CurrencyCollection {
tokens: prev_balance.tokens - expected_fwd_fees,
other: sent_value.other.clone(),
});
assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
assert_eq!(action_phase, ActionPhase {
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(expected_first_frac),
total_actions: 1,
messages_created: 1,
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
..empty_action_phase()
});
assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
assert_eq!(state.balance.tokens, Tokens::ZERO);
assert_eq!(
state.balance.other,
prev_balance.other.checked_sub(&sent_value.other)?
);
Ok(())
}
#[test]
fn strict_send_all_balance_destroy() -> Result<()> {
struct TestCase {
balance: CurrencyCollection,
to_send: CurrencyCollection,
expected_end_status: AccountStatus,
}
let mut params = make_default_params();
params.strict_extra_currency = true;
let config = make_default_config();
for TestCase {
balance,
to_send,
expected_end_status,
} in [
TestCase {
balance: OK_BALANCE.into(),
to_send: CurrencyCollection::ZERO,
expected_end_status: AccountStatus::NotExists,
},
TestCase {
balance: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
},
to_send: CurrencyCollection::ZERO,
expected_end_status: AccountStatus::Uninit,
},
TestCase {
balance: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
},
to_send: CurrencyCollection::ZERO,
expected_end_status: AccountStatus::Uninit,
},
TestCase {
balance: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
},
to_send: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1))]).try_into()?,
},
expected_end_status: AccountStatus::Uninit,
},
TestCase {
balance: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
},
to_send: CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([(0u32, VarUint248::new(1000))]).try_into()?,
},
expected_end_status: AccountStatus::NotExists,
},
] {
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE | SendMsgFlags::DELETE_IF_EMPTY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: to_send.clone(),
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.end_status, expected_end_status);
assert_eq!(state.out_msgs.len(), 1);
assert_eq!(state.end_lt, prev_end_lt + 1);
let last_msg = state.out_msgs.last().unwrap();
let msg_info = {
let msg = last_msg.load()?;
assert!(msg.init.is_none());
assert_eq!(msg.body, Default::default());
match msg.info {
MsgInfo::Int(info) => info,
e => panic!("unexpected msg info {e:?}"),
}
};
assert_eq!(msg_info.src, STUB_ADDR.into());
assert_eq!(msg_info.dst, STUB_ADDR.into());
assert!(msg_info.ihr_disabled);
assert!(!msg_info.bounce);
assert!(!msg_info.bounced);
assert_eq!(msg_info.created_at, params.block_unixtime);
assert_eq!(msg_info.created_lt, prev_end_lt);
let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(msg_info.value, CurrencyCollection {
tokens: prev_balance.tokens - expected_fwd_fees,
other: to_send.other.clone(),
});
assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
assert_eq!(action_phase, ActionPhase {
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(expected_first_frac),
total_actions: 1,
messages_created: 1,
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
status_change: AccountStatusChange::Deleted,
..empty_action_phase()
});
assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
assert_eq!(state.balance.tokens, Tokens::ZERO);
assert_eq!(
state.balance.other,
prev_balance.other.checked_sub(&to_send.other)?
);
}
Ok(())
}
#[test]
fn send_all_not_reserved() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_end_lt = state.end_lt;
let expected_balance = CurrencyCollection::from(state.balance.tokens / 4);
let actions = make_action_list([
OutAction::ReserveCurrency {
mode: ReserveCurrencyFlags::empty(),
value: expected_balance.clone(),
},
OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection::ZERO,
..Default::default()
},
None,
None,
),
},
]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(state.out_msgs.len(), 1);
assert_eq!(state.end_lt, prev_end_lt + 1);
let last_msg = state.out_msgs.last().unwrap();
let msg_info = {
let msg = last_msg.load()?;
assert!(msg.init.is_none());
assert_eq!(msg.body, Default::default());
match msg.info {
MsgInfo::Int(info) => info,
e => panic!("unexpected msg info {e:?}"),
}
};
assert_eq!(msg_info.src, STUB_ADDR.into());
assert_eq!(msg_info.dst, STUB_ADDR.into());
assert!(msg_info.ihr_disabled);
assert!(!msg_info.bounce);
assert!(!msg_info.bounced);
assert_eq!(msg_info.created_at, params.block_unixtime);
assert_eq!(msg_info.created_lt, prev_end_lt);
let expected_fwd_fees = Tokens::new(config.fwd_prices.lump_price as _);
let expected_first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(
msg_info.value,
(OK_BALANCE * 3 / 4 - expected_fwd_fees).into()
);
assert_eq!(msg_info.fwd_fee, expected_fwd_fees - expected_first_frac);
assert_eq!(msg_info.extra_flags, MessageExtraFlags::default());
assert_eq!(action_phase, ActionPhase {
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(expected_first_frac),
total_actions: 2,
messages_created: 1,
special_actions: 1,
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.total_fees, prev_total_fees + expected_first_frac);
assert_eq!(state.balance, expected_balance);
Ok(())
}
#[test]
fn cant_send_authority_marks_by_all_balance() -> Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
strict_extra_currency: false,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::new(),
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(500)), (101u32, VarUint248::new(500)),
(200u32, VarUint248::new(500)),
])
.try_into()?,
},
Cell::default(),
tvmasm!("ACCEPT"),
);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_end_lt = state.end_lt;
let prev_balance = state.balance.clone();
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection {
tokens: Tokens::new(100_000_000),
other: ExtraCurrencyCollection::new(),
},
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions,
compute_phase: &compute_phase,
inspector: None,
})?;
assert!(!action_phase.success);
assert!(action_phase.valid);
assert_eq!(
action_phase.result_code,
ResultCode::NotEnoughExtraBalance as i32
);
assert_eq!(action_phase.messages_created, 0);
assert_eq!(action_phase.total_actions, 1);
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.out_msgs.len(), 0);
assert_eq!(state.end_lt, prev_end_lt);
assert_eq!(state.balance, prev_balance);
Ok(())
}
#[test]
fn authority_addresses_can_send_marks() -> Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::try_from_btree(&BTreeMap::from_iter([(
HashBytes::ZERO,
(),
)]))?,
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&StdAddr::new(-1, HashBytes::ZERO),
CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(1000)), (101u32, VarUint248::new(100)),
])
.try_into()?,
},
Cell::default(),
tvmasm!("ACCEPT"),
);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_balance = state.balance.clone();
let actions_marks = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::PAY_FEE_SEPARATELY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection {
tokens: Tokens::ZERO,
other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
},
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase: action_phase_normal,
action_fine: action_fine_normal,
state_exceeds_limits: state_exceeds_limits_normal,
bounce: bounce_normal,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions_marks.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert!(action_phase_normal.success);
assert!(action_phase_normal.valid);
assert_eq!(action_phase_normal.result_code, 0);
assert_eq!(action_phase_normal.messages_created, 1);
assert_eq!(action_fine_normal, Tokens::ZERO);
assert_ne!(state.balance.tokens, prev_balance.tokens); assert_eq!(
state.balance.other,
BTreeMap::from_iter([
(100u32, VarUint248::new(990)), (101u32, VarUint248::new(100)),
])
.try_into()?
);
assert!(!state_exceeds_limits_normal);
assert!(!bounce_normal);
assert_eq!(state.out_msgs.len(), 1);
Ok(())
}
#[test]
fn cant_send_authority_marks_directly() -> Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::new(),
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(500)), (101u32, VarUint248::new(500)),
])
.try_into()?,
},
Cell::default(),
tvmasm!("ACCEPT"),
);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let prev_end_lt = state.end_lt;
let actions_black = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::PAY_FEE_SEPARATELY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection {
tokens: Tokens::ZERO,
other: BTreeMap::from_iter([(100u32, VarUint248::new(10))]).try_into()?,
},
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase: action_phase_black,
action_fine: action_fine_black,
state_exceeds_limits: state_exceeds_limits_black,
bounce: bounce_black,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions_black.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase_black, ActionPhase {
success: false,
valid: true,
no_funds: true,
result_code: ResultCode::NotEnoughExtraBalance as i32,
result_arg: Some(0),
total_actions: 1,
action_list_hash: *actions_black.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine_black, Tokens::ZERO);
assert!(!state_exceeds_limits_black);
assert!(!bounce_black);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(state.end_lt, prev_end_lt);
assert!(state.out_msgs.is_empty());
let actions_normal = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::PAY_FEE_SEPARATELY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst: STUB_ADDR.into(),
value: CurrencyCollection {
tokens: Tokens::ZERO,
other: ExtraCurrencyCollection::new(),
},
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase: action_phase_normal,
action_fine: action_fine_normal,
state_exceeds_limits: state_exceeds_limits_normal,
bounce: bounce_normal,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions_normal.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert!(action_phase_normal.success);
assert!(action_phase_normal.valid);
assert_eq!(action_phase_normal.result_code, 0);
assert_eq!(action_phase_normal.messages_created, 1);
assert_eq!(action_fine_normal, Tokens::ZERO);
assert!(!state_exceeds_limits_normal);
assert!(!bounce_normal);
assert_eq!(state.out_msgs.len(), 1);
Ok(())
}
#[test]
fn set_code() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let orig_data = CellBuilder::build_from(u32::MIN)?;
let final_data = CellBuilder::build_from(u32::MAX)?;
let temp_code = Boc::decode(tvmasm!("NOP NOP"))?;
let final_code = Boc::decode(tvmasm!("NOP"))?;
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
orig_data,
tvmasm!("ACCEPT"),
);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let actions = make_action_list([
OutAction::SetCode {
new_code: temp_code,
},
OutAction::SetCode {
new_code: final_code.clone(),
},
]);
let AccountState::Active(mut new_state) = state.state.clone() else {
panic!("unexpected account state");
};
new_state.data = Some(final_data.clone());
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state,
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
total_actions: 2,
special_actions: 2,
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(
state.state,
AccountState::Active(StateInit {
code: Some(final_code),
data: Some(final_data),
..Default::default()
})
);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
Ok(())
}
#[test]
fn invalid_dst_addr() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let targets = [
IntAddr::Std(StdAddr::new(123, HashBytes::ZERO)),
IntAddr::Std({
let mut addr = STUB_ADDR;
let mut b = CellBuilder::new();
b.store_u16(0xaabb)?;
addr.anycast = Some(Box::new(Anycast::from_slice(&b.as_data_slice())?));
addr
}),
IntAddr::Var(VarAddr {
anycast: None,
address_len: Uint9::new(80),
workchain: 0,
address: vec![0; 10],
}),
];
for dst in targets {
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let prev_end_lt = state.end_lt;
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::ALL_BALANCE,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
dst,
..Default::default()
},
None,
None,
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
total_actions: 1,
messages_created: 0,
result_code: ResultCode::InvalidDstAddr as _,
result_arg: Some(0),
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert!(state.out_msgs.is_empty());
assert_eq!(state.end_lt, prev_end_lt);
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
}
Ok(())
}
#[test]
fn cant_pay_fwd_fee() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(50000));
let compute_phase = stub_compute_phase(OK_GAS);
let prev_balance = state.balance.clone();
let prev_total_fee = state.total_fees;
let prev_end_lt = state.end_lt;
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::PAY_FEE_SEPARATELY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
value: CurrencyCollection::ZERO,
dst: STUB_ADDR.into(),
..Default::default()
},
None,
Some({
let mut b = CellBuilder::new();
b.store_reference(Cell::empty_cell())?;
b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
b
}),
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(action_phase, ActionPhase {
success: false,
no_funds: true,
result_code: ResultCode::NotEnoughBalance as _,
result_arg: Some(0),
total_actions: 1,
total_action_fees: Some(prev_balance.tokens),
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, prev_balance.tokens);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.balance, CurrencyCollection::ZERO);
assert_eq!(
state.total_fees,
prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
);
assert!(state.out_msgs.is_empty());
assert_eq!(state.end_lt, prev_end_lt);
Ok(())
}
#[test]
fn rewrite_message() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let compute_phase = stub_compute_phase(OK_GAS);
let prev_balance = state.balance.clone();
let prev_total_fee = state.total_fees;
let prev_end_lt = state.end_lt;
let msg_body = {
let mut b = CellBuilder::new();
b.store_zeros(600)?;
b.store_reference(Cell::empty_cell())?;
b
};
let actions = make_action_list([OutAction::SendMsg {
mode: SendMsgFlags::PAY_FEE_SEPARATELY,
out_msg: make_relaxed_message(
RelaxedIntMsgInfo {
value: CurrencyCollection::ZERO,
dst: STUB_ADDR.into(),
..Default::default()
},
None,
Some(msg_body.clone()),
),
}]);
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: StateInit::default(),
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: None,
})?;
assert_eq!(state.out_msgs.len(), 1);
let last_msg = state.out_msgs.last().unwrap();
let msg = last_msg.load()?;
assert_eq!(
msg.layout,
Some(MessageLayout {
init_to_cell: false,
body_to_cell: true,
})
);
assert_eq!(msg.body.1, msg_body.build()?);
let MsgInfo::Int(info) = msg.info else {
panic!("expected an internal message");
};
let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
bit_count: 600,
cell_count: 2,
});
let first_frac = config.fwd_prices.get_first_part(expected_fwd_fees);
assert_eq!(action_phase, ActionPhase {
total_actions: 1,
messages_created: 1,
total_fwd_fees: Some(expected_fwd_fees),
total_action_fees: Some(first_frac),
action_list_hash: *actions.repr_hash(),
total_message_size: compute_full_stats(last_msg, ¶ms),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.end_lt, prev_end_lt + 1);
assert_eq!(
state.total_fees,
prev_total_fee + action_phase.total_action_fees.unwrap_or_default()
);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(
state.balance.tokens,
prev_balance.tokens - info.value.tokens - expected_fwd_fees
);
Ok(())
}
#[test]
fn change_lib() -> Result<()> {
struct TestCase {
libraries: Dict<HashBytes, SimpleLib>,
target: Dict<HashBytes, SimpleLib>,
changes: Vec<(ChangeLibraryMode, LibRef)>,
diff: Vec<PublicLibraryChange>,
}
fn make_lib(id: u32) -> Cell {
CellBuilder::build_from(id).unwrap()
}
fn make_lib_hash(id: u32) -> HashBytes {
*CellBuilder::build_from(id).unwrap().repr_hash()
}
fn make_libs<I>(items: I) -> Dict<HashBytes, SimpleLib>
where
I: IntoIterator<Item = (u32, bool)>,
{
let mut items = items
.into_iter()
.map(|(id, public)| {
let root = make_lib(id);
(*root.repr_hash(), SimpleLib { root, public })
})
.collect::<Vec<_>>();
items.sort_by_key(|(a, _)| *a);
Dict::try_from_sorted_slice(&items).unwrap()
}
let params = make_default_params();
let config = make_default_config();
let test_cases = vec![
TestCase {
libraries: Dict::new(),
target: Dict::new(),
changes: Vec::new(),
diff: Vec::new(),
},
TestCase {
libraries: Dict::new(),
target: make_libs([(123, false)]),
changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
diff: Vec::new(),
},
TestCase {
libraries: Dict::new(),
target: make_libs([(123, false), (234, false)]),
changes: vec![
(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123))),
(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(234))),
],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, false)]),
target: make_libs([(123, false)]),
changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, false)]),
target: make_libs([(123, false)]),
changes: vec![(
ChangeLibraryMode::ADD_PRIVATE,
LibRef::Hash(make_lib_hash(123)),
)],
diff: Vec::new(),
},
TestCase {
libraries: Dict::new(),
target: make_libs([(123, true)]),
changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
diff: vec![PublicLibraryChange::Add(make_lib(123))],
},
TestCase {
libraries: Dict::new(),
target: make_libs([(123, true), (234, true)]),
changes: vec![
(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123))),
(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(234))),
],
diff: vec![
PublicLibraryChange::Add(make_lib(123)),
PublicLibraryChange::Add(make_lib(234)),
],
},
TestCase {
libraries: make_libs([(123, true)]),
target: make_libs([(123, true)]),
changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, true)]),
target: make_libs([(123, true)]),
changes: vec![(
ChangeLibraryMode::ADD_PUBLIC,
LibRef::Hash(make_lib_hash(123)),
)],
diff: Vec::new(),
},
TestCase {
libraries: Dict::new(),
target: Dict::new(),
changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
diff: Vec::new(),
},
TestCase {
libraries: Dict::new(),
target: Dict::new(),
changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, false)]),
target: Dict::new(),
changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, false)]),
target: Dict::new(),
changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Hash(make_lib_hash(123)))],
diff: Vec::new(),
},
TestCase {
libraries: make_libs([(123, true)]),
target: Dict::new(),
changes: vec![(ChangeLibraryMode::REMOVE, LibRef::Cell(make_lib(123)))],
diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
},
TestCase {
libraries: make_libs([(123, false)]),
target: make_libs([(123, true)]),
changes: vec![(ChangeLibraryMode::ADD_PUBLIC, LibRef::Cell(make_lib(123)))],
diff: vec![PublicLibraryChange::Add(make_lib(123))],
},
TestCase {
libraries: make_libs([(123, false)]),
target: make_libs([(123, true)]),
changes: vec![(
ChangeLibraryMode::ADD_PUBLIC,
LibRef::Hash(make_lib_hash(123)),
)],
diff: vec![PublicLibraryChange::Add(make_lib(123))],
},
TestCase {
libraries: make_libs([(123, true)]),
target: make_libs([(123, false)]),
changes: vec![(ChangeLibraryMode::ADD_PRIVATE, LibRef::Cell(make_lib(123)))],
diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
},
TestCase {
libraries: make_libs([(123, true)]),
target: make_libs([(123, false)]),
changes: vec![(
ChangeLibraryMode::ADD_PRIVATE,
LibRef::Hash(make_lib_hash(123)),
)],
diff: vec![PublicLibraryChange::Remove(make_lib_hash(123))],
},
];
for TestCase {
libraries,
target,
changes,
diff,
} in test_cases
{
let mut state = ExecutorState::new_active(
¶ms,
&config,
&StdAddr::new(-1, HashBytes::ZERO),
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("ACCEPT"),
);
let target_state_init = match &mut state.state {
AccountState::Active(state_init) => {
state_init.libraries = libraries;
StateInit {
libraries: target,
..state_init.clone()
}
}
AccountState::Uninit | AccountState::Frozen(..) => panic!("invalid initial state"),
};
let compute_phase = stub_compute_phase(OK_GAS);
let prev_total_fees = state.total_fees;
let prev_balance = state.balance.clone();
let change_count = changes.len();
let actions = make_action_list(
changes
.into_iter()
.map(|(mode, lib)| OutAction::ChangeLibrary { mode, lib }),
);
let mut inspector = ExecutorInspector::default();
let ActionPhaseFull {
action_phase,
action_fine,
state_exceeds_limits,
bounce,
} = state.action_phase(ActionPhaseContext {
received_message: None,
original_balance: original_balance(&state, &compute_phase),
new_state: match state.state.clone() {
AccountState::Active(state_init) => state_init,
AccountState::Uninit | AccountState::Frozen(..) => Default::default(),
},
actions: actions.clone(),
compute_phase: &compute_phase,
inspector: Some(&mut inspector),
})?;
assert_eq!(action_phase, ActionPhase {
total_actions: change_count as u16,
special_actions: change_count as u16,
action_list_hash: *actions.repr_hash(),
..empty_action_phase()
});
assert_eq!(action_fine, Tokens::ZERO);
assert!(!state_exceeds_limits);
assert!(!bounce);
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(state.state, AccountState::Active(target_state_init));
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
assert_eq!(inspector.public_libs_diff, diff);
}
Ok(())
}
}