Transaction Executor Functional Specification
Evgeniy Shishkin <evgeniy.shishkin@gmail.com>

Table of Contents

1 Introduction

Transaction Executor is a crucial part of EverScale blockchain node. It applies incoming messages to accounts, sealing the end result of this operation into a block in the form of a transaction object.

The Transaction Executor algorithms determine several critical aspects of smart-contracts behavior, such as:

  • How a balance of an account is affected after the message gets processed
  • What outbound messages will be generated as a result
  • Should the account be frozen or deleted?
  • What fees should be charged from the accounts balance

To be able to rigorously reason about a smart-contract behavior, it is important to construct the accurate model of this module, explain the main concepts, define its properties. In other words, make the groundwork for you, the reader, to foster the integration of this logic into the reasoning framework of your choice.

In the current work, we made the best-effort attempt to write such specification.

1.1 Document Structure

The document consists of two logical parts, intermixed with each other: the explanation part and the specification part.

The explanation part is done by providing extensive comments for data structures used through-out the Transaction Executor. The data structures are presented as Rust code snippets, taken from the original Node code. Sometimes, we intentionally omit details that are not relevant to the Transaction Executor, requiring much wider context to be explained.

The specification part is presented in two flavors. When the precision is required, we describe the behavior by providing the pseudo-code implementing some algorithm. For more general properties, we formulate them in a form of semi-formal statements about the system behavior.

By comparison to the program implementation, the specification pseudo-code overapproximates the implementation by throwing away non-relevant parts of the logic, for example: sophisticated error handling, non-interesting parts of the state being removed, introducing reasonable assumptions that greatly simplifies the logic, etc.

In other words, the pseudo-code shows how the system behaves for its significant parts, putting away everything else.

1.2 Pseudo-code Semantics

A few words about the pseudo-code language we use to define the behavior of the Transaction Executor parts.

The language used is Python-like, with nearly intuitive semantics.

We would like to highlight the the following:

  • For basic values like Ints and Bools, the assignment operator copies the value.
  • For complex datatypes (objects), the assignment operator copies a reference to the object instead of creating a new instance.
  • The call \(obj.clone()\) creates a deep copy of the object \(obj\)
  • The input arguments are passed by reference, so, mutating them within the function would mutate them for the caller also.
  • Default values for structure fields are:

    • 0 for Ints
    • False for Bools
    • None for Option
    • For Enum types, the default value is the first item in the enumeration

    Sometimes, the default value is irrelevant and not specified explicitly.

  • In few places, we use idiomatic Python values swap: \[a, b = b, a\] This construct exchanges the values of \(a\) and \(b\).
  • The object method syntax is used in few places, for example: \[obj.method(p1,p2,...) = method(obj,p1,p2,...)\]
  • Types and namespaces begin with uppercase letter, for example: TransactionExecutor.TrExecutorError() denotes the object TrExecutorError residing in the namespace TransactionExecutor
  • We use is operator to do type test. For example, to test that message is internal, we use the following construct:

    if in_msg.header is IntMsgInfo:
       return ExecuteInternalMessage(in_msg, ...)
    
  • We omit details of some global objects, and just assume they exist. For example, the virtual machine is created using some abstract TVM object. The same goes for system error enumerations. It is done this way not to overload the pseudo-code with easily recoverable details.

1.3 Live Specification

We decided to publish the specification online. This should provide us the opportunity to update the document on the regular basis, if some nuances or errors get discovered.

We encourage the community members to submit pull requests or issues using the following repository:

https://bitbucket.org/unboxedtype/executorspec/

2 EverScale Platform Architecture

The main actors of the EverScale blockchain are smart-contracts. Smart-contracts are programs that operate user valuable assets on their behalf. Valuable assets are usually cryptocurrency tokens or some digital goods, like NFTs.

Smart-contract execution is triggered by a message sent from some other party. If the message was delivered from the outside world (i.e. from the user program), it is said to be external. Otherwise, the message is considered internal.

Smart-contracts may also generate log records called events. Those records are used as information signals for an external observers. They foster communication between smart-contracts and off-chain programs.

The platform overall architecture is depicted on Fig. 1

Sorry, your browser does not support SVG.

Figure 1: EverScale blockchain high-level architecture

2.1 Platform Implementation

EverScale blockchain is a database operated by a peer-to-peer network of computing nodes. The database store users code and data in a form of programmable units called smart-contracts. Smart-contacts may communicate with other contracts and outside world by sending messages.

The computing node is called blockchain node in our context.

Among other things, blockchain nodes are responsible for storing the smart-contract state, delivering messages from users and smart-contracts, executing the smart-contacts code when needed.

Transaction Executor module is a part of the blockchain node responsible for proper execution of a smart-contract code upon receiving a message addressed to that smart-contract. The result of this execution is an updated smart-contract state and the transaction record that gets sealed into the block candidate.

We now go into details on the internals of this module.

3 Transaction Executor Module

In this section, we go into the technical details of Transaction Executor module. The source code of the module is available here.

3.1 Remark

In our opinion, the name of this module was chosen quite unfortunate. In its current form, it feels like the object being executed is a transaction. This is not true. Transaction is an outcome of executing a message on a smart-contract state using the Transaction Executor logic. Hence, it is the message that is being executed, not the transaction. Nevertheless, we stick with the original name not to confuse developers too much.

3.2 Inputs and Outputs

The principal architecture of the module is depicted on Fig. 2

Sorry, your browser does not support SVG.

Figure 2: Transaction Executor

We now describe each input/output entity in detail, together with the logic of the computation.

3.3 Multichain Architecture

EverScale has a native support for multiple blockchains running in parallel.

Each blockchain might be established by introducing a separate chain called workchain. Each workchain has a unique integer identifier in a range -127 … 127, the values -1 and 0 are already taken. Smart-contracts from different workchains may interact with each other by message passing.

At the moment, the system implements only two workchains - Masterchain (id -1) and Workchain (id 0).

excl2.png Currently, the creation of new workchains is not supported

3.4 Multicurrency payments

The native coin of EverScale blockchain is called Ever. However, EverScale has an ability to work with other types of coins. While system payments like gas and storage fees are made only in Evers, the other value transfers may contain coins of other currencies. This contrasts with most of other blockchains where there is only a single native cryptocurrency, and other currencies may be made only using artificial token smart-contracts.

Currently, this feature is not used widely.

excl2.png In this document, we limit our specification effort only for the case of a single currency - Evers. This choice significantly simplifies the business logic of the execution handlers.

3.5 Hashing Algorithm

Transaction Executor uses hashing in several places to compactly store data structures fingerprints. It is done in two steps. First, the data structure gets converted into a tree-like form. Then, a special hashing algorithm is applied to that tree. The basic hash function used is SHA256 from Sha2 Rust package.

The exact hashing algorithm, as well as tree-like representation is not interesting for our purposes, so we do not consider it here. For details, check this.

3.6 Accounts

Within a single workchain, the most basic information storage entity is an account. There, an account is uniquely determined by its account identifier. Within the whole blockchain - the collection of workchains - the account is uniquely determined by its address: it is when we put together the workchain identifier and its account identifier. See Fig. 3.

The account record stores account address, account balance and maybe a smart-contract state. The latter might be missing, and yet the account is able to receive basic messages.

Sorry, your browser does not support SVG.

Figure 3: Simplified representation of an EverScale Account.

Account could be created either by a user, or by a smart-contract. It is being done by sending a message carrying special payload to an address of the account.

In a process of its operation, an account might become frozen or deleted. This is usually a result of account having a debt, or executing a special instruction. We depict the life-cycle of an account on Fig. 4.

Sorry, your browser does not support SVG.

Figure 4: Account Lifecycle

In the beginning, the account remains in an Nonexistent state - it stores no funds and no data. Only after you send some value to the account, it switches to the Uninit state. In that state it is not yet very useful: it may only receive coins from the incoming messages.

The account might be initialized with a message carrying StateInit data blob containing smart-contract code and data. In this case, with enough coins on the balance, the account becomes initialized with this smart-contract, and it is then switched into Active state.

Due to storage fees, the account balance might become negative leading to Frozen state and even being deleted afterwards. The other option for deletion is if the smart-contract itself execute a special action. After the deletion, the account state is switched back to Nonexistent state.

3.6.1 Account Structure Definition

The account structure is defined as follows:

struct AccountStuff {
    addr: MsgAddressInt,
    storage_stat: StorageInfo,
    storage: AccountStorage,
}

type Account = Option<AccountStuff>
Table 1: AccountStuff structure fields
Field Description
addr Account address
storage_stat Account storage use statistics
storage Account smart-contract storage

3.6.2 Account Address

The location of an account in EverScale blockchain is represented as a two-value structure: the workchain number and the account identifier. This structure is called an account address. Hereinafter, we just call it address for greater convenience.

Addresses are defined as follows:

pub enum MsgAddressInt {
    AddrStd(MsgAddrStd),
    AddrVar(MsgAddrVar),
}

pub struct MsgAddrStd {
    pub anycast: Option<AnycastInfo>,
    pub workchain_id: i8,
    pub address: AccountId,
}

pub type AccountId = SliceData;

The address may be encoded by one of the two structures: MsgAddrStd or MsgAddrVar. The latter is used to locate accounts in huge blockchains, when the standard 8-bit workchain_id is not enough, and not supported currently.

  • Type SliceData denotes a binary blob encoded in a tree data structure.
  • Type i8 is an 8-bit signed integer.

Field Description
anycast Multi-shard contracts routing information
workchain_id Workchain identifier
address Account identifier within the workchain



excl2.png Anycast-addresses are planned to be removed shortly.

3.6.3 Account Storage

Any account in the EverScale blockchain is being charged for occupying space, on a regular basis. The fee depends on the size of data being stored, the current prices and when the last charge took place. In some circumstances, an account may also have a debt, called due payment. Most of this information is stored in the StorageInfo structure.

pub struct StorageInfo {
    used: StorageUsed,
    last_paid: u32,
    due_payment: Option<Grams>,
}

pub struct StorageUsed {
    cells: VarUInteger7,
    bits: VarUInteger7,
    public_cells: VarUInteger7,
}
Table 2: StorageInfo structure fields
Field Description
used Blockchain storage use statistics
last_paid Time of the latest payment, in Unix Epoch
due_payment Debt of the account



  • Type Grams denotes a set of natural numbers \(\{0, ..., 2^{256}\}\), equipped with \(\oplus\) and \(\ominus\) operators, such that: \[a \oplus b = (a + b) \,\, \boldsymbol{mod} \,\, 2^{256}\] \[a \ominus b = \boldsymbol{max}(a - b, 0)\] Here, \(+\) and \(-\) operators are standard addition and subtraction operators in a set of integers \(\mathbb{Z}\)
  • Amount of storage used by the account is encoded with StorageUsed struct.



Table 3: StorageUsed structure fields
Fields Description
cells Number of cells occupied by the account
bits Number of bits occupied by the account
public_cells Field is not used

To store the data in a tree-like form, it is encoded as a series of interlinked cells. This data structure also consume some space and it is accounted in the cells field. The bits field refer to data size being encoded in the cells.

3.6.4 Account Data

The full account record is represented by several nested data structures:

  • Account storage
  • Account state
  • Smart-contract storage called StateInit
3.6.4.1 Account Storage structure

The most outer record is the account storage. It contains the account balance and the account state. The account state may contain the smart-contract code and data, described by the structure called StateInit.

pub struct AccountStorage {
    last_trans_lt: u64,
    balance: CurrencyCollection,
    state: AccountState,
}
Table 4: AccountStorage fields
Field Description
last_trans_lt Last transaction logical time
balance Amount of cryptocurrency tokens available for the account
state Current account state
3.6.4.2 Account State

The account state defines the mode of operation for the account, during the message being executed for that account. The Transaction Executor logic vary greatly depending of what is the current account state is.

The account state may have additional data fields. See the enumeration below.

enum AccountState {
    AccountUninit,
    AccountActive{init_code_hash: Option<UInt256>,
                  state_init: StateInit},
    AccountFrozen{init_code_hash: Option<UInt256>,
                  state_init_hash: UInt256},
}

The life cycle of an account is depicted on Fig. 4.

Let us clarify the fields of enumeration items.

For the AccountActive, the value state_init defines the byte-code and data of the associated smart-contract. The field init_code_hash defines the hash of the field state_init.code that was used at the moment of the account initialization, or at the moment of the account freeze.

In EverScale, it is possible to change the smart-contract's code on the fly using the SetCode action. However, the value init_code_hash stays unaffected.

The same holds for AccountFrozen. The value of state_init_hash defines the hash of the smart-contract state_init.code at the moment of a freeze.

3.6.4.3 Smart-Contract Storage (StateInit)

The byte-code and data of a contract are stored within a structure called StateInit. Its name may seem quite confusing. It could have been named just State. The Init part comes from the fact that this structure is also used for the initialization of an account when it is uninitialized.

pub struct StateInit {
    pub split_depth: Option<Number5>,
    pub special: Option<TickTock>,
    pub code: Option<Cell>,
    pub data: Option<Cell>,
    pub library: StateInitLib,
}
  • split_depth field was initially devoted to large multi-shard smart-contracts, but currently it is not used.
  • special fields signals the fact that the smart-contract is related to the blockchain system functioning. This is related to very small amount or contracts residing in the Masterchain, i.e. Elector, Config, Giver, etc. There is a special logic of executing messages destined to those contracts.
  • code and data fields encodes the current byte-code and data of a contract. Here, data denote values of contract's variables.
  • library used to encode the code libraries the contract may refer to from its code. This mechanism is deprecated.

3.7 Messages

In EverScale, smart-contracts communicate between each other and with non-blockchain applications by means of an asynchronous message passing.

Technically, a message is a data structure encoding one of the following:

  • desired function call at a destination smart-contract, optionally attaching some coins.
  • event log record to signal external observers about some significant state being reached

We distinguish 3 types 1 of messages:

  • External - messages sent by non-blockchain applications
  • Internal - messages sent by smart-contracts between each other
  • Event - a log record signaling some special state for external observers
pub struct Message {
    header: CommonMsgInfo,
    init: Option<StateInit>,
    body: Option<SliceData>,
    body_to_ref: Option<bool>,
    init_to_ref: Option<bool>,
}

The last two fields body_to_ref and init_to_ref are used only for serialization purpose, hence not considered in this document.

3.7.1 Message Header

Any message has a message header: a data-structure defining, among other things, the message type and source and destination addresses.

The message header defines its type. It is described by the following enumeration:

pub enum CommonMsgInfo{
    IntMsgInfo(InternalMessageHeader),
    ExtInMsgInfo(ExternalInboundMessageHeader),
    ExtOutMsgInfo(ExtOutMessageHeader)
}

3.7.2 Internal Message

Within EverScale blockchain, smart-contracts communicate with each other by exchanging messages. Messages sent by smart-contracts are called internal. They are opposed to external messages that are sent by off-chain applications to smart-contracts.

The message header of an internal message is defined as follows:

pub struct InternalMessageHeader {
    pub ihr_disabled: bool,
    pub bounce: bool,
    pub bounced: bool,
    pub src: MsgAddressIntOrNone,
    pub dst: MsgAddressInt,
    pub value: CurrencyCollection,
    pub ihr_fee: Grams,
    pub fwd_fee: Grams,
    pub created_lt: u64,
    pub created_at: UnixTime32,
}
Table 5: InternalMessageHeader fields
Field Description
ihr_disabled IHR routing protocol disabled. Always True.
bounce Should the answer message be generated in case of an error
bounced Is this message was auto-generated by error handling
src Message source address
dst Message destination address
value Amount of coins attached to the message
ihr_fee IHR fee amount. Always zero.
fwd_fee Message delivery fee amount
created_lt Message creation logic time
created_at Message creation time in Epoch

Some clarifications:

  • bounce flag regulates how the message processing error should be handled by Transaction Executor
  • bounced flag is set when the message itself was auto-generated as a result of an error. If the message with bounced flag leads to an error itself, the next bounced message will not be generated.
  • value is measured in Nano Evers (\(10^{-9}\))
  • created_lt is a monotonically increasing counter. Thanks to this field, each new generated message is unique, even if the message payload is the same. The message creation logic time is also used to guarantee order of delivery. We do not dive deep into this question, because it is protocol-level details.

3.7.3 External Message

External messages are created outside of the blockchain and get sent through specially distinguished validator nodes called DApp Servers 2

External message header is defined as follows:

pub struct ExternalInboundMessageHeader {
    pub src: MsgAddressExt,
    pub dst: MsgAddressInt,
    pub import_fee: Grams,
}
  • Fields src and dst are source and destination addresses.
  • Field import_fee should have been the value paid to the validator for processing an external message. But in the current node, this field is not used. Hence, the fee is not paid. We reported this issue to the developers.
  • The source address for an external message is always set to AddrNone.
pub enum MsgAddressExt {
    AddrNone,
    AddrExtern(MsgAddrExt),
}

The second variant AddrExtern is not supported currently.

3.7.4 Events

Event can be considered as a log record. It is used to signal external observers of reaching some significant state in a smart-contract.

Usually, observers are external non-blockchain applications that constantly monitor blockchain state 3. Other smart-contracts are not able to catch events.

pub struct ExtOutMessageHeader {
    pub src: MsgAddressIntOrNone,
    pub dst: MsgAddressExt,
    pub created_lt: u64,
    pub created_at: UnixTime32,
}
pub enum MsgAddressIntOrNone {
    None,
    Some(MsgAddressInt)
}
  • Transaction Executor automatically assigns the source address src to be equal to the smart-contract address emitting the event.
  • The destination address dst may contain any identifier. It is included for easier integration with off-chain applications, i.e. applications can monitor emitted events based on their destination address, and consume only those events destined to their custom identifier.
  • Fields created_lt, created_at defines the logical creation time and epoch creation time.

3.8 Parameters

Besides incoming message and account, Transaction Executor has to have some external information regarding the current blockchain and non-blockchain state to support the TVM capabilities. For example, it has to know the current time to provide it for smart-contracts. It has to have some random seed to support the random number generator facility. All of this is passed using the ExecuteParams structure.

pub struct ExecuteParams {
    pub state_libs: HashmapE,
    pub block_unixtime: u32,
    pub block_lt: u64,
    pub last_tr_lt: Arc<AtomicU64>,
    pub seed_block: UInt256,
    pub debug: bool
}
Table 6: ExecuteParams fields
Field Description
state_libs A set of references to external libraries. This mechanism is not supported currently.
block_unixtime Current time in Unix Epoch
block_lt Block logical time
last_tr_lt The last transaction logical time
seed_block Random number generator seed
debug Should the TVM output debug information during its execution

3.9 Transaction

Transaction is an object that describes the successful execution of a message on the account. If a message execution results in an error, such execution does not lead to a transaction creation. After transaction is created, it gets sealed into the block. And after the block is negotiated with fellow validators, it find its way into the Masterchain. From that point, it stays there forever 4

Transaction is an output of the Transaction Executor, so we have to examine it more closely.

pub struct Transaction {
    pub account_addr: AccountId,
    pub lt: u64,
    pub prev_trans_hash: UInt256,
    pub prev_trans_lt: u64,
    pub now: u32,
    pub outmsg_cnt: i16,
    pub orig_status: AccountStatus,
    pub end_status: AccountStatus,
    pub in_msg: Option<ChildCell<Message>>,
    pub out_msgs: OutMessages,
    pub total_fees: CurrencyCollection,
    pub state_update: ChildCell<HashUpdate>,
    pub description: ChildCell<TransactionDescr>,
}
Table 7: Transaction structure fields
Field Description
account_addr Account identifier
lt Transaction creation logical time
prev_trans_hash Previous transaction hash value
prev_trans_lt Previous transaction logical time
now Current time in Unix Epoch
outmsg_cnt Number of generated outbound messages
orig_status Account state upon receiving the message
end_status Account state after executing the message
in_msg Processed message
out_msgs Set of generated outbound messages
total_fees Total fee amount for all the processing
state_update Hash footprint of the account state change
description Transaction Descriptor
   

3.10 Transaction Executor

Transaction Executor module is responsible for applying the incoming message to the destination account, using the supplied parameters. In case of success, Transaction Executor outputs the newly created transaction and the updated account.

The main entry point is the function execute_with_libs_and_params() within transaction_executor.rs module. Other entry points were either flagged as deprecated, or reduce to calling this function after some minor parameters mangling.

The message execution is being done in several phases.

A phase is a logical step during the message execution. It may finish successfully or with an error. In case of an error, the next phase may not be executed. Phases are done mostly in a fixed order, but there are some nuances.

Let us warn you that the phase is not just an implementation detail of the Transaction Executor internals that may be easily discarded. Message execution phases are a part of EverScale smart-contracts programming architecture. It is assumed that you have a good grasp on it, to be able to do proper troubleshooting in case something is not working as expected. Without this knowledge, it may be challenging to debug the problem.

This document aims to support programmers in their strive for this knowledge.

Sorry, your browser does not support SVG.

Figure 5: Transaction Executor Message Processing General Scheme

3.10.1 Transaction Executor Types

There are several type of messages in EverScale. Besides already mentioned ordinary messages, there are also a special type of messages that is a part of a wider protocol. For example, TickTock messages, SplitMerge messages, etc.

For each type of messages, there exists a separate Transaction Executor. In this work, we consider only the OrdinaryTransactionExecutor, that is defined in ordinary_transaction.rs.

3.10.2 Main Entry Point

fn execute_with_libs_and_params(
    &self,
    in_msg: Option<&Message>,
    account_root: &mut Cell,
    params: ExecuteParams,
) -> Result<Transaction>
Table 8: execute_with_libs_and_params() function parameters
Parameter Description
self Reference to the object calling the function
in_msg Incoming message 3.7
account_root Account record serialized in a form of Cells 3.6.1
params Transaction Executor parameters 3.8

As a result, the function returns either Ok(Transaction) object or Err value. Please note that besides returning the Transaction, there is a side-effect of mutating the account_root object. This justifies our generalization that it returns two objects: the transaction and the updated account record.

3.10.3 BlockchainConfig parameters

Besides ExecuteParams, the Transaction Executor relies on BlockchainConfig parameters. They are passed implicitly, at the Transaction Executor creation time.

BlockchainConfig is a set of globally defined parameters regulating different nuances of blockchain work. For example, prices for smart-contract execution, storage and a set of system contract addresses. The latter is needed to let Transaction Executor apply special logic for them.

Those parameters are global to the network, and negotiated between all the validators in advance. They are stored in a special system smart-contract, in the Masterchain.

pub struct BlockchainConfig {
    gas_prices_mc: GasLimitsPrices,
    gas_prices_wc: GasLimitsPrices,
    fwd_prices_mc: MsgForwardPrices,
    fwd_prices_wc: MsgForwardPrices,
    storage_prices: AccStoragePrices,
    special_contracts: FundamentalSmcAddresses,
    capabilities: u64,
    global_version: u32,
    raw_config: ConfigParams,
}
Field Description
gas_prices_mc Fees for Masterchain smart-contract execution
gas_prices_wc Fees for Workchain smart-contract execution
fwd_prices_mc Fees for delivering messages in Masterchain
fwd_prices_wc Fees for delivering messages in Workchain
storage_prices Fees for information storage
special_contracts Set of system smart-contract addresses
capabilities Set of operation-mode flags
global_version Minimum blocks version number allowed to be included in the chain
raw_config Dictionary with blockchain settings

3.10.4 Code Execution Fee

As in most of blockchain, in EverScale the execution of a smart-contract costs money. Usually, this fee is deduced from the coins attached to the message initiating the call, but there are nuances.

The fee amount to be deducted from the balance is calculated based on values found in gas_price_mc, gas_price_wc structures. They are defined as follows:

pub struct GasLimitsPrices {
    pub gas_price: u64,
    pub gas_limit: u64,
    pub special_gas_limit: u64,
    pub gas_credit: u64,
    pub block_gas_limit: u64,
    pub freeze_due_limit: u64,
    pub delete_due_limit: u64,
    pub flat_gas_limit: u64,
    pub flat_gas_price: u64,
    pub max_gas_threshold: u128,
}
Field Description
gas_price Price of 1 unit of gas, expressed in Nano Evers
gas_limit Maximum gas amount for execution of a single message for an ordinary account
special_gas_limit Maximum gas amount for execution of a single message for a system account
gas_credit Gas credited for an account to execute the external message
block_gas_limit Maximum gas amount of the whole block
freeze_due_limit Value of an account debt leading to account freeze
delete_due_limit Value of an account debt leading to account removal
flat_gas_limit  
flat_gas_price  
max_gas_threshold  

3.10.5 Message Passing Fee

Validators do the work of message delivery. To compensate their efforts, account pays for the message passing. The message passing fee depends on BlockchainConfig parameters fwd_prices and the message size. The fwd_prices_mc and fwd_prices_wc have the following definition:

pub struct MsgForwardPrices {
    pub lump_price: u64,
    pub bit_price: u64,
    pub cell_price: u64,
    pub ihr_price_factor: u32,
    pub first_frac: u16,
    pub next_frac: u16,
}

The fee amount is calculated using the expression:

\[msg\_fwd\_fees = lump\_price + bit\_price \times msg.bits + cell\_price \times msg.cells\]

Here, msg.bits - bit-length of the message body, msg.cells is a total amount of cells that this message consists of.

3.10.6 Data Storage Fee

In EverScale, account is charged a fee for storing the data. The fee amount is calculated using the formula:

\[fee = (cells * prices.cell\_price + bits * prices.bit\_price) * \Delta\]

\(\Delta\) - the time interval between now and the latest payment moment, measured in seconds. Here, we assume that storage prices stay constant during the \(\Delta\) interval. The storage fee gets charged on each message processing 4.4.

For greater flexibility, the storage prices may be changed depending on the current supply/demand situation. It is done by negotiating new blockchain config parameters \(prices.cell\_price\) and \(prices.bit\_price\) among validators. After validators accept it, new parameters are written in the Masterchain config smart-contract.

After the change, previous price parameters do no get lost. The whole history of storage price changes is stored in the config. It is done to provide precise calculation of the storage fee that take into account all the price changes during the interval of calculation.

3.10.6.1 Data Storage Fee Calculation

Here we describe the storage fee calculation expression in its generality.

Lets assume we have the following list of prices equipped with a timestamp of a moment when the price change took place:

\[ T = \{ t_0, t_1, ..., t_N \} \] \[Pr = \langle (pr_0, t_0), (pr_1, t_1), ..., (pr_N, t_N) \rangle \]

Here, \(pr_0\) is reserved for the initial prices set in the genesis block of the blockchain, and \(t_0\) is a timestamp of those initial prices being set.

Let \(now\) denote the current timestamp, i.e. the moment of time when we want to calculate the storage fee, measured in Unix Epoch. Its value is always greater or equal than the most recent price change timestamp.

Let \(last\_paid\) denote the timestamp of the latest storage payment. If the payment didn't take place, \(last\_paid = 0\).

To simplify the calculation formula, let us introduce a new list \(Pr'\), such that:

\[Pr' = \langle (pr_k, t_k), ..., (pr_N, t_N) \rangle \] where \(t_k\) is the least timestamp among values \(t_1 ... t_N\) that is greater than \(last\_paid\). \[t_k = min \{ t_i | t_i \in T \land t_i > last\_paid \}\]

In other words, the values \(t_k, ..., t_N\) form a subset of \(T\) where each value is strictly greater than \(last\_paid\).

We also add two more elements from the left and the right: \[Pr'' = \langle (pr_k, last\_paid) \rangle \cdot Pr' \cdot \langle (pr_N, now) \rangle\] Here, dot operator denotes lists concatenation operation.

We use the following shortcuts: \(pr_i = fst(Pr''_i)\) - the first element of a two-element tuple, and \(t_i = snd(Pr''_i)\), the second element.

The total storage fee for the time interval is:

\[ total\_storage\_fee = \sum_{i=1..|Pr''|}{(cells * pr_{i}.cell\_price + bits * pr_{i}.bits\_price) * (t_i - t_{i-1})} \]

3.10.6.2 Data Storage Fee Calculation Algorithm

For greater convenience, besides having the formula, we provide the pseudo-code for the algorithm calc_storage_fee, implemented in imperative fashion.

Input:

  • config - current blockchain configuration, has type BlockchainConfig
  • storage_info - the account storage info struct, has type StorageInfo.
  • is_masterchain - is the account inhabits Masterchain or not, has type Bool
  • now - current time, measured in Unix Epoch, has type UInt

Output:

  • fee - the fee amount to be deducted from the account balance, has type UInt
  • storage_info - updated account storage info, has type StorageInfo
def calc_storage_fee(config, storage_info, is_masterchain, now):
    cells =  storage_info.used.cells
    bits = storage_info.used.bits
    last_paid = storage_info.last_paid
    prices = config.storage_prices   # see AccStoragePrices

    assert len(prices) > 0

    if now <= last_paid or last_paid == 0 or now <= prices[0].utime_since:
        return 0

    fee = 0

    # calculate the fee according to prices that were actual
    # during the specific period of time
    for i in len(prices):
        cur_price = prices[i]
        if i < len(prices) - 1:
            end = prices[i + 1].utime_since
        else:
            end = now_time

        if end >= last_paid:
            delta = end - max(cur_price.utime_since, last_paid)
            if is_masterchain:
                fee += (cells * cur_price.mc_cell_prices_ps + \
                    bits * cur_price.mc_bit_price_ps) * delta
            else:
                fee += (cells * cur_price.cell_price_ps + \
                    bits * cur_price.bit_price_ps) * delta

    storage_info.last_paid = end

    return (fee, storage_info)

3.10.7 Special Smart-Contracts

In EverScale blockchain, there is a set of smart-contracts that have a distinguished status in the system. For those contracts, validators are obligated to process their execution in a special priveledged manner. Such smart-contracts are called special or system. Accounts storing those contracts are called the same.

Special smart-contracts enjoy the following privilege:

  • No fee gets deducted for the code execution
  • No fee gets deducted for the storage use
  • No fee gets deducted for message passing
  • It has a special maximum gas limit, see GasLimitsPrices.special_gas_limit
  • Allowed to process TickTock timer messages

Upon executing a message for one of those special contracts, the Transaction Executor has to apply all those conditions.

excl2.png In this document, we mainly focus on ordinary accounts, leaving the special accounts processing details aside.

3.10.8 GlobalCapabilities Options

There are several flags defining different aspects of the blockchain node operation mode. They are defined in GlobalCapabilities enumeration.

Parameter Description
CapCreateStatsEnabled Allow update block statistics. Not related to Transaction Executor.
CapBounceMsgBody Include the first 256 bits of the original message in the bounce message body.
CapReportVersion Include the blockchain version info into the block.
CapShortDequeue Some special mode of managing outbound messages by the Validator. Not related to Transaction Executor
CapFastStorageStat Use alternative algorithm to update the structs AccountsStat.
CapInitCodeHash Use the field init_code_hash in the AccountState.
CapOffHypercube Turn off Hypercube routing algorithm for message delivery
CapMycode Provide the virtual machine with the code of a smart-contract being executed.
CapMbppEnabled Not used
CapIhrEnabled Not used
CapSplitMergeTransactions Not used

3.10.9 RawConfig options

Besides already mentioned options, there are yet another set of options residing in the BlockchainConfig.raw_config. This field has the following structure:

pub struct ConfigParams {
    pub config_addr: UInt256,
    pub config_params: HashmapE // <u32, SliceData>
}



  • config_addr - is the configuration smart-contract account identifier (the workchain identifier equals -1)
  • config_params - dictionary with parameters; dictionary keys refers to an option number. We will not go deep into those options, because they are not relevant to our work.

See ton-labs-block/src/config_params.rs for further investigation.

3.10.10 Error Code Enumeration

When Transaction Executor encounters an error during message processing, it returns a special answer to the calling side. The answer contains an error code. Here we list possible error codes and their short description. In our further discussion, we rely on those mnemonic names.

Error Mnemonic Name Description
InvalidExtMessage Incorrect format of an incoming external message
TrExecutorError(e) Wide range of errors during message processing
TvmExceptionCode(e) TVM produced exception e during byte-code execution
NoAcceptError The smart-contract did not accept external message
NoFundsToImportMsg Not enough funds to process external message
ExtMsgComputeSkipped(r) During the external message processing, the Compute phase was skipped with the reason r

3.10.11 Account State Update

In the transaction object, there is a special field reflecting the change of the account state, the state_update field of type HashUpdate.

The type is defined as follows:

pub struct HashUpdate {
    pub old_hash: UInt256,
    pub new_hash: UInt256,
}

Here, old_hash refers to a hash value taken from the initial account state, before message processing; the new_hash is a hash taken from the updated account state, after successful message processing.

3.10.12 Transaction Description Object

During the incoming message processing, Transaction Executor constructs the report about the processing. This report has a special name - Transaction Description, and defined by the following structure:

pub struct TransactionDescrOrdinary {
    pub credit_first: bool,
    pub storage_ph: Option<TrStoragePhase>,
    pub credit_ph: Option<TrCreditPhase>,
    pub compute_ph: TrComputePhase,
    pub action: Option<TrActionPhase>,
    pub aborted: bool,
    pub bounce: Option<TrBouncePhase>,
    pub destroyed: bool
}

This description object may be used for fast checkups on the main system invariants, critical for its safety, during runtime.

Field Description
storage_ph Storage phase descriptor
credit_ph Credit phase descriptor
compute_ph Compute phase descriptor
action Action phase descriptor
bounce Bounce phase descriptor
credit_first Credit phase was executed before Storage phase
aborted Is Action phase failed
destroyed Is account deleted after message execution

We now describe each descriptor separately.

3.10.12.1 Storage Phase Descriptor

pub struct TrStoragePhase {
    pub storage_fees_collected: Grams,
    pub storage_fees_due: Option<Grams>,
    pub status_change: AccStatusChange
}
  • storage_fees_collected denotes the amount of tokens deducted from the account balance to cover the storage fee.
  • storage_fees_due denotes the debt value, if there is any. Otherwise, this value equals None.
  • status_change denotes the possible account status change. It may have been the case that the status were frozen or deleted due to having a significant debt value. Possible values are:
pub enum AccStatusChange {
    Unchanged,
    Frozen,
    Deleted,
}
3.10.12.2 Credit Phase Descriptor

The Credit Phase descriptor is defined as follows:

pub struct TrCreditPhase {
    pub due_fees_collected: Option<Grams>,
    pub credit: CurrencyCollection,
}
Field Description
due_fees_collected Amount of coins deducted from the message balance to cover the debt of the account, if any existed at the beginning of the credit phase. If there were no debt, the value is None.
credit Message value after the fees were conducted from it.
3.10.12.3 Compute Phase Descriptor

Compute Phase descriptor is defined with the following enumeration:

pub enum TrComputePhase {
    Skipped(TrComputePhaseSkipped),
    Vm(TrComputePhaseVm)
}

Choice 1. Skipped

If the Compute phase was not successfully performed, the descriptor value is Skipped in this case. It should have an argument with following type:

pub struct TrComputePhaseSkipped {
    pub reason: ComputeSkipReason
}

reason has to be one of the following:

pub enum ComputeSkipReason {
    NoState,
    BadState,
    NoGas,
}
Field Description
NoState Caused by the following conditions: 1) The account did not exist by the time of message arrival, and the incoming message did not contain the StateInit part; 2) The account was not initialized and the incoming message did not contain the StateInit part.
BadState Caused by the following conditions: 1) The account was in AccStateUninit state, the message did contain the StateInit part, but an attempt to initialize the account with the given StateInit failed due to being inconsistent with the account; 2) The account was in AccStateFrozen state, the message contained the StateInit part, but an attempt to unfreeze the account with the given state init failed due to being inconsistent with the account.
NoGas Caused by the following conditions: 1) After Credit and Storage phases, the account balance had no coins: its balance equals zero; 2) Values gas_limit and gas_credit, calculated with the init_gas algorithm, both equals 0.

Choice 2. Successful computation

Successful Compute phase result is defined by the following TrComputePhaseVm structure:

pub struct TrComputePhaseVm {
    pub success: bool,
    pub msg_state_used: bool,
    pub account_activated: bool,
    pub gas_fees: Grams,
    pub gas_used: VarUInteger7,
    pub gas_limit: VarUInteger7,
    pub gas_credit: Option<VarUInteger3>,
    pub mode: i8,
    pub exit_code: i32,
    pub exit_arg: Option<i32>,
    pub vm_steps: u32,
    pub vm_init_state_hash: UInt256,
    pub vm_final_state_hash: UInt256
}
Field Description
success Compute phase completion status. See 4.5.1
gas_fees Fees for the gas used by a smart-contract execution, see here
gas_used An exact amount of gas used by the VM during the execution
gas_limit A strict upper bound on the amount of gas allowed for this account 4.5.6
gas_credit An amount of gas credited to be used for external messages before being accepted 4.5.6
vm_steps Number of steps performed by the VM
exit_code Computation exit code, see 4.5.2
exit_arg Computation exit argument, see 4.5.2
mode Always equals 0
vm_init_state_hash Not used
vm_final_state_hash Not used
msg_stated_used Not used
account_activated Not used
3.10.12.4 Action Phase Descriptor

Action Phase descriptor is defined as follows:

pub struct TrActionPhase {
    pub success: bool,
    pub valid: bool,
    pub no_funds: bool,
    pub status_change: AccStatusChange,
    pub total_fwd_fees: Option<Grams>,
    pub total_action_fees: Option<Grams>,
    pub result_code: i32,
    pub result_arg: Option<i32>,
    pub tot_actions: i16,
    pub spec_actions: i16,
    pub skipped_actions: i16,
    pub msgs_created: i16,
    pub action_list_hash: UInt256,
    pub tot_msg_size: StorageUsedShort,
}
Field Description
success Action phase completed successfully. The success condition is described here.
valid Action phase is valid. The validity condition is described here.
result_code Action phase failed with the result code, see 3.10.12.5. In case of success, the value equals to 0
result_arg In case of an error, the item number of an action in the action list that caused the error
no_funds True if the error was caused by a balance insufficiency
status_change Equals AccStatusChange::Deleted in case of the account being deleted after processing actions
total_fwd_fees Total fees for the SendMsg actions processing
total_action_fees Total fees for the whole action list processing
tot_actions Total number of actions in the action list at a beginning of the Action phase
spec_actions Number of special actions, i.e. Reserve, SetCode, SetLib
msg_created Number of successful SendMsg actions
action_list_hash Hash of action list calculated at a beginning of the Action phase
tot_msg_size Total size of all the generated messages
skipped_actions Not used
3.10.12.5 Action Result Codes

Result Code Description
RESULT_CODE_ACTIONLIST_INVALID Message serialization error
RESULT_CODE_TOO_MANY_ACTIONS Contract generated more actions than allowed. Maximum actions count is 255
RESULT_CODE_UNKNOWN_OR_INVALID_ACTION Binary serialization error, or invalid flags. See remarks.
RESULT_CODE_INCORRECT_SRC_ADDRESS Wide source address 3.6.2, or the source address does not equal to the account address
RESULT_CODE_INCORRECT_DST_ADDRESS Incorrect destination address, or destination workchain is not allowed to receive messages, or destination workchain does not exist
RESULT_CODE_ANYCAST Destination address of type Anycast. It is no longer supported and considered an error.
RESULT_CODE_NOT_ENOUGH_GRAMS Insufficient balance. See remarks.
RESULT_CODE_NOT_ENOUGH_EXTRA Extra-tokens balance is insufficient to execute to action
RESULT_CODE_INVALID_BALANCE Reserve action lead to an error, or outgoing message is too big to process
RESULT_CODE_BAD_ACCOUNT_STATE Actions SetCode or ChangeLib lead to an error
RESULT_CODE_UNSUPPORTED SendMsg action has incorrect flags set

Remarks:

  1. RESULT_CODE_UNKNOWN_OR_INVALID_ACTION reasons are:
    • Actions serialization error
    • SendMsg action has invalid flags, that is:
      • The mutually exclusive flags are set: SENDMSG_REMAINING_MSG_BALANCE and SENDMSG_ALL_BALANCE
      • Message was sent with an unknown flag 3.11.2;
      • The flag SENDMSG_DELETE_IF_EMPTY is set, but the flag SENDMSG_ALL_BALANCE isn't;
    • Reserve action has invalid flags
      • Unknown flag is set
      • Flag RESERVE_PLUS_ORIG is set, but RESERVE_REVERSE isn't
  2. RESULT_CODE_NOT_ENOUGH_GRAMS reasons are:
    • For SendMsg action, the flag SENDMSG_REMAINING_MSG_BALANCE is set, but SENDMSG_PAY_FEE_SEPARATELY isn't
    • Message balance is insufficient to cover message delivery fees
    • Account balance is insufficient to cover all message delivery fees
3.10.12.6 Bounce Phase Transaction Descriptor

Bounce Phase descriptor is defined with the following enumeration:

pub enum TrBouncePhase {
    Negfunds,
    Nofunds(TrBouncePhaseNofunds),
    Ok(TrBouncePhaseOk),
}
  • Negfunds choice is not used.
  • Nofunds(TrBouncePhaseNofunds) denotes the insufficiency of account balance, details are put into the parameter value
  • Ok(TrBouncePhaseOk) denotes success, i.e. that the bounce message has been formed and put into the Msg queue. Details of the phase are put into the parameter value.

Choice 1. Nofunds

pub struct TrBouncePhaseNofunds {
    pub msg_size: StorageUsedShort,
    pub req_fwd_fees: Grams,
}
  • msg_size denotes the size of generated bounce message. This value is not used.
  • req_fwd_fees denotes the fee for the message delivery.

Choice 2. Ok(TrBouncePhaseOk)

pub struct TrBouncePhaseOk {
    pub msg_size: StorageUsedShort,
    pub msg_fees: Grams,
    pub fwd_fees: Grams,
}
  • msg_size not used.
  • fwd_fees is a full forwarding fee for the bounce message.
  • msg_fees is a part of fwd_fees that goes to the validator processing the message.

3.11 Actions

After successfully executing a smart-contract code, the TVM virtual machine provides the executor with updated contract state and a list of actions to be further processed.

In our context, an action refers to an order for the Transaction Executor to perform a distinguished stateful act. It could be sending a message, changing the smart-contract's code or reserving coins on the balance.

3.11.1 Type of Actions

Here we provide a set of possible actions, with the description. We go deep on each of them further.

pub enum OutAction {
    None, // default value
    SendMsg {
        mode: u8,
        out_msg: Message,
    },
    SetCode {
        new_code: Cell,
    },
    ReserveCurrency {
        mode: u8,
        value: CurrencyCollection,
    },
    ChangeLibrary {
        mode: u8,
        code: Option<Cell>,
        hash: Option<UInt256>,
    }
}
Action Description
SendMsg Send the message out_msg to some account using the provided mode
ReserveCurrency Manage the account's balance to guarantee its sufficiency
SetCode Change the contract byte-code with the given new_code
ChangeLibrary Update code library

3.11.2 Action SendMsg

\(SendMsg(mode,out\_msg)\) action sends a message to an account. The message \(out\_msg\) contains the destination address as well as the payload to be delivered.

This action has a lot of modes that can be combined using logical OR operator. Some mode combinations are prohibited. See 3.10.12.5.

Mode Value Description
SENDMSG_ORDINARY 0 Send the message. Without other modes, the forwarding fee for the delivery is paid by the receiver.
SENDMSG_PAY_FEE_SEPARATELY 1 Send the message. The forwarding fee is paid by the sender.
SENDMSG_IGNORE_ERROR 2 If an error occurs during the processing of this action, ignore it.
SENDMSG_DELETE_IF_EMPTY 32 The account gets deleted if, after the action processed, the balance becomes zero
SENDMSG_REMAINING_MSG_BALANCE 64 The message should carry all the remaining value of the inbound message additionally to the value specified in the field
SENDMSG_ALL_BALANCE 128 The message should carry all the remaining balance of the account, instead of the value specified in the value field

3.11.3 Action ReserveCurrency

\(ReserveCurrency(mode, val)\) action makes a coin reserve on the balance. This action has several modes of operation. Modes can be combined.

Mode Value Description
RESERVE_EXACTLY 0 Reserve exactly \(val\) coins
RESERVE_ALL_BUT 1 Reserve \(acc\_balance - val\) coins, where \(acc\_balance\) is a remaining balance of the account
RESERVE_IGNORE_ERROR 2 Skip the action on failure
RESERVE_PLUS_ORIG 4 Reserve \(acc\_balance + val\) coins. It should be used only with RESERVE_REVERSE.
RESERVE_REVERSE 8 Reverse value of \(val\) in the calculation of the reserve, i.e. substitute \(val\) with \(-val\)

3.11.4 Action SetCode

Currently, we skip this action.

3.11.5 Action ChangeLibrary

Currently, we skip this action.

4 Message Processing Algorithm

In this section, we present a pseudo-code for incoming message processing algorithm.

The algorithm is divided in two mutually exclusive parts:

  • ExecuteInternalMessage - internal message execution 4.1
  • ExecuteExternalMessage - external message execution 4.2

Both algorithms rely on executing some or all of the phases:

  • Credit phase 4.3
  • Storage phase 4.4
  • Compute phase 4.5
  • Action phase 4.6
  • Bounce phase 4.7

Please note that we consider only ordinary accounts here. The algorithm for executing messages on special accounts is not considered.

Input:

Output:

On success, returns Ok(acc1, trans), such that:

On error, returns error of the type ExecutorError

Modifies:
None

def ExecuteMessage(in_msg, account, params, config):
    if in_msg.header is ExtOutMsgInfo:
        return ExecutorError.InvalidExtMessage
    if in_msg.header.dst == None:
        return ExecutorError.TrExecutorError()
    if in_msg.header is ExtInMessageHeader and account.balance == 0:
        return ExecutorError.NoFundsToImportMsg()

    acc = account.clone()

    if in_msg.header is ExtInMsgInfo:
        return ExecuteExternalMessage(in_msg, acc, params, config)
    elif in_msg.header is IntMsgInfo:
        return ExecuteInternalMessage(in_msg, acc, params, config)

    return ExecutorError.TrExecutorError()

4.1 Internal Message Processing Algorithm

At this point, the message is known to be internal. Execute it with the given account.

Input:

Output:

On success, returns Ok(acc1, trans), such that:

On error, returns error of the type TransactionExecutor.TrExecutorError

Modifies:

  • account
def ExecuteInternalMessage(in_msg, account, params, config):
    acc_balance = account.balance
    msg_balance = in_msg.hdr.value
    credit_first = not in_msg.hdr.bounce
    lt = max(account.last_tr_time, max(params.last_tr_lt, in_msg.lt + 1))
    tr = Transaction(account.account_id, account.status, lt, now(), in_msg)
    descr = TransactionDescrOrdinary(credit_first: credit_first)

    # If the bounce flag is not set, execute the Credit Phase
    # before Storage phase
    if credit_first:
        credit_ph_res = credit_phase(account, tr, msg_balance, acc_balance)
        if credit_ph_res is Ok:
            descr.credit_ph = credit_ph_res.credit_ph
        else:
            return ExecutorError.TrExecutorError()

    # Execute Storage Phase
    storage_ph_res = storage_phase(account,
                                   acc_balance,
                                   tr,
                                   is_masterchain,
                                   config)
    descr.storage_ph = storage_ph_res.storage_ph

    # Why this is needed?
    if credit_first and (msg_balance > acc_balance):
        msg_balance = acc_balance

    original_acc_balance = account.balance - tr.total_fees

    if not credit_first:
        credit_ph_res = credit_phase(account, tr, msg_balance, acc_balance)
        if credit_ph_res is Ok:
            descr.credit_ph = credit_ph_res.credit_ph
        else:
            return ExecutorError.TrExecutorError()

    # Both storage and credit phases are completed at this point.
    # We need to update the last_paid field not to loose this
    # information in case of some further errors showing up.
    account.last_paid = params.block_unixtime

    # Parameters to be passed into TVM
    smci = build_contract_info(acc_balance,
                               account.address,
                               params.block_lt,
                               lt,
                               params.seed_block)
    # First element is the bottom of the stack
    stack = Stack([acc_balance, msg_balance, Cell(in_msg), in_msg.body, False])

    # Execute Compute Phase
    compute_ph_res = compute_phase(in_msg,
                                   account,
                                   acc_balance,
                                   msg_balance,
                                   params.state_libs,
                                   smci,
                                   stack,
                                   is_masterchain)

    if not (compute_ph_res is Ok):
        return ExecutorError.TrExecutorError()

    descr.compute_ph = compute_ph_res.compute_ph
    actions = compute_ph_res.actions
    new_data = compute_ph_res.new_data

    # Generated outbound messages to be sent into other accounts
    out_msgs = []

    compute_gas_fees = descr.compute_ph.gas_fees
    tr.total_fee = tr.total_fee + compute_gas_fees

    if descr.compute_ph.success:
        act_phase_res = action_phase(tr,
                                     account,
                                     original_acc_balance,
                                     acc_balance,
                                     msg_balance,
                                     phase.gas_fees,
                                     actions,
                                     new_data)
        if act_phase_res is Ok:
            descr.action = act_phase_res.action_ph
            out_msgs = act_phase_res.msgs
        else:
            return ExecutorError.TrExecutorError()

    if descr.action != None:
        if descr.action.status_change == AccStatusChange.Deleted:
            account = Account()
            descr.destroyed = True
        descr.aborted = not descr.action.success
    else:
        descr.aborted = True

    # If the Action Phase failed, and the incoming message allows
    # bounce answer, execute the Bounce Phase
    if (descr.aborted == True) and (in_msg.hdr.bounce = True):
        if descr.compute_ph is Vm:
            bounce_ph_res = \
                bounce_phase(msg_balance,
                             acc_balance,
                             compute_gas_fees,
                             tr,
                             my_addr)
            if bounce_ph_res is Ok:
                descr.bounce = bounce_ph_res.bounce_ph
                if (bounce_ph_res.bounce_msg != None):
                    out_msgs = out_msgs + [bounce_ph_res.bounce_msg]
            else:
                return ExecutorError.TrExecutorError()
        if descr.bounce is Ok:
            acc_balance = original_acc_balance
            if account.status == AccountStatus.AccStateUninit and \
               acc_balance == 0:
                account = Account()
            else:
                if account.is_none() and acc_balance != 0:
                    account = Account.uninit(is_msg.hdr.dst, 0, last_paid,
                                             acc_balance)

    if account.status() == AccountStatus.AccStateUninit and acc_balance == 0:
        account = Account()

    tr.acc_end_status = account.status
    account.balance = acc_balance
    params.last_tr_lt = lt

    upd_lt = add_messages(tr, out_msgs, params.last_tr_lt)
    account.last_tr_time = upd_lt
    tr.descr = descr
    return Ok(tr, account)

The function add_messages assigns the proper logical timestamp for each message from the out_msgs collection, and then include the message into the transaction.

def add_messages(tr, out_msgs, lt):
    lt_next = lt + len(out_msgs) + 1
    lt_next += 1

    for msg in out_msgs:
        msg.at = now()
        msg.lt = lt
        tr.add_out_message(msg)
        lt_next += 1

    return Ok(lt_next)

4.2 External Message Processing Algorithm

The execution of external message on the given account.

Input:

Output:

On success, returns Ok(acc1, trans), such that:

On error, returns error of the type TransactionExecutor.TrExecutorError

Modifies:

  • account
def ExecuteExternalMessage(in_msg, account, params, config):
    acc_balance = account.balance
    msg_balance = in_msg.hdr.value
    is_masterchain = (in_msg.dst_workchain_id == -1)

    lt = max(account.last_tr_time, max(params.last_tr_lt, in_msg.lt + 1))
    tr = Transaction(account.account_id, account.status, lt, now(), Cell(in_msg))
    descr = TransactionDescrOrdinary(credit_first: True)
    in_fwd_fee = fwd_fee(Cell(in_msg))

    if acc_balance < in_fwd_fee:
        return ExecutorError.NoFundsToImportMsg

    tr.total_fee = tr.total_fee + in_fwd_fee

    # Execute Storage Phase
    storage_ph_res = storage_phase(account,
                                   acc_balance,
                                   tr,
                                   is_masterchain,
                                   config)
    descr.storage_ph = storage_ph_res.storage_ph

    if account.balance >= tr.total_fees:
        original_acc_balance = account.balance - tr.total_fees
    else:
        original_acc_balance = account_balance

    # Credit Phase is skipped for external messages

    # Storage phase is completed at this point
    # We need to update the last_paid field not to loose this
    # information in case of some further errors showing up
    account.last_paid = params.block_unixtime

    # Parameters to be passed into TVM
    smci = build_contract_info(acc_balance,
                               account.address,
                               params.block_lt,
                               lt,
                               params.seed_block)
    # First element is the bottom of the stack
    stack = Stack([acc_balance, msg_balance, Cell(in_msg), in_msg.body, False])

    # Execute Compute Phase
    compute_ph_res = compute_phase(in_msg,
                                   account,
                                   acc_balance,
                                   msg_balance,
                                   params.state_libs,
                                   smci,
                                   stack,
                                   is_masterchain)

    if compute_ph_res is Ok:
        descr.compute_ph = compute_ph_res.compute_ph
    else:
        return ExecutorError.TrExecutorError()

    # Generated outbound messages to be sent into other
    # accounts
    out_msgs = []

    compute_gas_fees = descr.compute_ph.gas_fees
    tr.total_fee = tr.total_fee + compute_gas_fees

    if descr.compute_ph.success:
        act_phase_res = action_phase(tr,
                                     account,
                                     original_acc_balance,
                                     acc_balance,
                                     msg_balance,
                                     phase.gas_fees,
                                     accounts,
                                     compute_ph_res.new_data)
        if act_phase_res is Ok:
            descr.action = act_phase_res.action_ph
            out_msgs = act_phase_res.msgs
        else:
            return ExecutorError.TrExecutorError()

    if descr.action != None:
        if descr.action.status_change == AccStatusChange.Deleted:
            account = Account()
            descr.destroyed = True
        descr.aborted = not descr.action.success
    else:
        descr.aborted = True

    # The Bounce Phase is skipped for external messages

    if account.status() == AccountStatus.AccStateUninit and acc_balance == 0:
        account = Account()

    tr.acc_end_status = account.status
    account.balance = acc_balance
    params.last_tr_lt = lt

    upd_lt = add_messages(tr, out_msgs, params.last_tr_lt)
    account.last_tr_time = upd_lt
    tr.descr = descr
    return Ok(tr, account)

4.3 Credit Phase

At this phase, coins from the message balance goes to the account balance. This phase is executed only for internal messages. External messages have no coins attached.

Input:

  • account - account that the message is executed on, Account
  • tr - forming transaction, has type Transaction
  • msg_balance - message balance, has type Grams
  • acc_balance - current balance of the account, has type Grams

Output:

The phase always succeeds. It returns the value of type: Ok(TrCreditPhase(collected, msg_balance)), such that:

  • collected - the amount of coins withheld for the account debt, if any.
  • msg_balance - the amount of coins put on the account balance after the debt fee was conducted.

Modifies:

  • account - updates the due_payment field with the remaining debt, if any
  • tr - updates the total_fees field
  • msg_balance - the original message balance after the debt conducted, if any
  • acc_balance - the account balance with message coins
def credit_phase(account, tr, msg_balance, acc_balance):
    due_payment = account.due_payment
    collected = min(due_payment, msg_balance)

    msg_balance = msg_balance - collected
    due_payment_remaining = due_payment - collected

    account.due_payment = due_payment_remaining
    tr.total_fees = tr.total_fees + collected

    # put message coins on the account balance
    acc_balance = acc_balance + msg_balance
    return Ok(TrCreditPhase(collected, msg_balance))

4.4 Storage Phase

This phase withholds the storage fee from the account balance. The fee amount is calculated using the algorithm calc_storage_fee 3.10.6.2

Input:

  • account - account that the message is executed on, has type Account
  • tr - forming transaction, has type Transaction
  • msg_balance - message balance, has type Grams
  • acc_balance - current balance of the account, has type Grams
  • config - main blockchain parameters, has type BlockchainConfig

Output:

This phase always succeeds. The return values may differ: Ok(TrStoragePhase(collected, fee, status_change)), such that:

  • collected - the amount of coins withheld for the storage fee
  • debt - if the balance was insufficient, the remaining debt of the account
  • status_change - should the account be frozen or deleted afterwards

Modifies:

  • account - updates due_payment and status fields
  • acc_balance - the current balance after the fee got deducted
  • tr - updates the total_fee field
def storage_phase(account, tr, msg_balance, acc_balance, config):
    # It is assumed that the current transaction must have a more
    # recent timestamp than the latest payment timestamp.
    # Otherwise, something is terribly wrong.
    assert (tr.now >= acc.last_paid)

    # The account does not occupy any space, so do not charge the fee
    if account == None:
        return Ok(TrStoragePhase())

    fee, account.storage_info = config.calc_storage_fee(account.storage_info,
                                                        is_masterchain, tr.now)
    if account.due_payment > 0:
        fee = fee + account.due_payment
        account.due_payment = None

    if acc_balance >= fee:
        acc_balance = acc_balance - fee
        tr.total_fee = tr.total_fee + fee
        return Ok(TrStoragePhase(fee, None, AccStatusChange.Unchanged))

    storage_fees_collected = acc_balance
    acc_balance = 0
    tr.total_fee = tr.total_fee + storage_fees_collected
    fee = fee - storage_fees_collected

    need_freeze = fee > config.get_gas_config(is_masterchanin).freeze_due_limit
    need_delete = \
        (account.status == AccountStatus.AccStateUninit or \
         account.status == AccountStatus.AccStateFrozen) and \
         fee > config.get_gas_config(is_masterchain).delete_due_limit

    if need_delete:
        tr.total_fee = 0
        account = Account()
        return Ok(TrStoragePhase(storage_fees_collected, fee,
                                 AccStatusChange.Deleted))
    elif need_freeze:
        account.due_payment = fee
        if account.status == AccountStatus.AccStateActive:
            account.status = AccountStatus.AccStateFrozen
            return Ok(TrStoragePhase(storage_fees_collected, fee,
                                     AccStatusChange.Frozen))
        else:
            return Ok(TrStoragePhase(storage_fees_collected, fee,
                                     AccStatusChange.Unchanged))
    else:
        account.due_payment = fee
        return Ok(TrStoragePhase(storage_fees_collected, fee,
                                 AccStatusChange.Unchanged))

4.5 Compute Phase

Execute the account smart-contract, update the state, gather generated actions to pass on the next phase.

Input:

  • msg - message, has type Message
  • account - account, has type Account
  • acc_balance - current account balance, has type Grams
  • msg_balance - message balance,has type Grams
  • state_libs - code libraries, has type Blob (not relevant; omitted)
  • smc_info - extra data for TVM, has type SmartContractInfo
  • stack - TVM initial stack values
  • is_masterchain - is the account belongs to Masterchain, has type bool

Output:

  • On success, returns Ok(TrComputePhase, out_actions, new_data), such that:
    • TrComputePhase - actual Compute Phase Descriptor
    • out_actions - an ordered list of generated actions
    • new_data - updated smart-contract state
  • On error, returns Err(ExecutorError) with proper code.

Modifies:

  • account - updated account state
  • smc_info - mycode field set to point to the code of the smart-contract
  • acc_balance - account balance after the gas fee deduction
def uninit_account(account):
    if account.storage.state is AccountState.AccountActive:
        account.storage.state = AccountState.AccountUninit

def compute_phase(msg, account, acc_balance, msg_balance, state_libs, smc_info,
                  stack, is_masterchain):
    result_acc = account.clone()
    vm_phase = TrComputePhaseVm()

    is_external = msg.header is ExtInMsgInfo
    if result_acc == None:
        new_acc = account_from_message(msg, msg_balance)
        if new_acc != None:
            result_acc = new_acc
            result_acc.last_paid = smc_info.unix_time
            account = result_acc
            account.uninit_account()

    if acc_balance == 0:
        return Ok(TrComputePhase:skipped(ComputeSkipReason.NoGas), None, None)

    gas_config = config.get_gas_config(is_masterchain)
    gas = init_gas(acc_balance, msg_balance, is_external, gas_config)

    # Is it possible?
    if gas.gas_limit == 0 and gas.gas_credit == 0:
        return Ok(TrComputePhase.skipped(ComputeSkipReason.NoGas), None, None)

    libs = []
    if msg.state_init != None:
        libs = state_init.libraries

    (reason, result_acc) = result_acc.compute_new_state(acc_balance, msg)

    if reason != None:
        return Ok(TrComputePhase.skipped(reason), None, None)

    vm_phase.gas_credit = gas.gas_credit
    vm_phase.gas_limit = gas.gas_limit

    if result_acc.code == None:
        if is_external:
            return ExecutorError.NoAcceptError()
        vm_phase.success = False
        vm_phase.gas_fees = gas_config.calc_gas_fee(0)
        if acc_balance < vm_phase.gas_fees:
            return ExecutorError.TrExecutorError()
        acc_balance -= vm_phase.gas_fees
        account = result_acc
        return Ok(TrComputePhase.Vm(vm_phase), None, None)

    code = result_acc.code
    data = result_acc.data

    libs.push(result_acc.libraries)   # local libraries
    libs.push(state_libs)             # masterchain libraries

    smc_info.mycode = code

    # Here, we initialize abstract TVM virtual machine.
    # The exact behavior of this device is out of scope.
    vm = TVM(code)
    vm.smc_info = smc_info
    vm.config = config
    vm.stack = stack
    vm.data = data
    vm.libraries = libs
    vm.gas = gas

    result = vm.execute()

    vm_phase.success = vm.commited_state.is_committed
    # vm.gas may have been updated after the execution
    gas_vm = vm.gas
    # how much credited gas remains unspent
    credit = gas_vm.gas_credit
    used = gas_vm.gas_used
    vm_phase.gas_used = used

    if credit != 0:
        if is_external:
            # The smart-contract has to explicitly accept the external message,
            # otherwise it gets rejected. The acceptance of a message manifests
            # itself in the credit field being equal to 0.
            return ExecutorError.NoAcceptError()
        vm_phase.gas_fees = 0
    else:
        gas_fees = gas_config.calc_gas_fee(used)
        vm_phase.gas_fees = gas_fees

    vm_phase.mode = 0
    vm_phase.vm_steps = vm.steps

    new_data = vm.commited_state
    if new_data == None:
        vm_phase.success = False

    out_actions = vm.actions
    if out_actions = None:
        vm_phase.success = False

    account = result_acc
    return Ok(TrComputePhase.Vm(vm_phase), out_actions, new_data)

4.5.1 Compute Phase Success Conditions

We would like to explicitly articulate what it means for the Compute Phase to succeed. To do that, we specify the opposite condition, i.e. when it fails. In all other scenarios the phase is considered successful.

The success status is important, because it decides if the action phase has to be executed afterwards.

For the phase to fail, one of the following conditions must hold:

  1. The smart-contract data is not committed after the execution 5
  2. The new smart-contract data is ill-formed
  3. The generated actions list is ill-formed


excl2.png The compute phase may be considered successful even if the computation thrown an exception. This is quite unintuitive, yet very important fact.

4.5.2 Compute Phase Exit Code

The exit code value shows if the computation finished normally or was aborted due to some exception.

In case of the former, the exit code should have values 0 or 1.

In case of the latter, the exception might be of a system or custom type. If the exception is a system one, i.e. not intentionally emitted by the code using a special TVM instruction, the exit code contains one of the standard exit codes.

If the exception is custom, then the exit code should also equal to 0 or 1, but there is an extra exit_arg field that provides the user defined code.

For standard TVM exception codes, see here.

4.5.3 Calculate Gas Fee Algorithm

The algorithm to calculate the amount of coins to be paid for the consumed gas.

Input:

  • gas_prices - a structure with actual gas prices, has type GasLimitsPrice
  • gas_used - amount of gas units consumed by the computation, has type Uint

Output: The amount of coins to be paid for the gas.

Modifies: None

def calc_gas_fee(gas_prices, gas_used):
    if gas_used <= gas_prices.flat_gas_limit:
        return gas_prices.flat_gas_limit

    gas_fee = flat_gas_price + (gas_used - gas_prices.flag_gas_limit) * \
        gas_prices.gas_price
    return gas_fee

4.5.4 Compute New State Algorithm

The algorithm compute_new_state computes the actual account state by given account record, the balance and the message. In particular, this algorithm is used to initialize uninitialized accounts with code and data borrowed from an external message with non-empty state_init field.

Input:

  • account - account structure, has type Account
  • acc_balace - current account balance, has type Uint
  • in_msg - message being executed, has type Message

Output:

  • On success, returns None.
  • On failure, returns one of the ComputeSkipReason codes.

Modifies:

  • account
def compute_new_state(account, acc_balance, in_msg):
    if account.status == AccountStatus.AccStateNonexist:
        if in_msg.state_init == None:
            return ComputeSkipReason.NoState
        else:
            return ComputeSkipReason.BadState
    elif account.status == AccountStatus.AccStateActive:
        return None
    elif account.status == AccountStatus.AccStateUninit:
        if in_msg.state_init != None:
            if account.try_activate_by_init_code_hash(in_msg.state_init) != None:
                return None
            else:
                return ComputeSkipReason.BadState
        else:
            return ComputeSkipReason.NoState
    elif account.status == AccountStatus.AccStateFrozen:
        if acc_balance != 0 and in_msg.state_init != None:
            if account.try_activate_by_init_code_hash(in_msg.state_init) != None:
                return None
            else:
                return ComputeSkipReason.BadState
        return ComputeSkipReason.NoState

    return None

4.5.5 Activate By Init Algorithm

The algorithm try_activate_by_init_code_hash does the initialization or re-initialization of the account, with the given state_init.

Input:

  • account - account structure to be initialized
  • state_init - state_init field from the inbound message

Output:

On success, returns Ok

On failure, returns Err

Modifies:

  • account - the field storage.state gets updated by the state_init on success
def try_activate_by_init_code_hash(account, state_init):
    if account == None:
        return Err

    new_state = None

    if account.storage.state == AccountState.AccountUninit:
        if hash(state_init) == account.addr.address:
            new_state = AccountState.AccountActive(
                hash(state_init.code), state_init
            )
        else:
            return Err
    elif account.storage.state == \
         AccountState.AccountFrozen(init_code_hash, state_init_hash):
        if state_init_hash == hash(state_init):
            new_state = AccountState.AccountActive(init_code_hash, state_init)
        else:
            return Err
    else:
        new_state = account.storage.state

    account.storage.state = new_state
    return Ok

4.5.6 Initial Gas Algorithm

The algorithm computes TVM Gas-related initial values. Those values are provided to the virtual machine right before a smart-contract execution. If the execution takes more than allowed gas, it gets stopped.

Input

  • acc_balance: current account balance
  • msg_balance: message balance
  • is_external: is the message external
  • gas_info: structure with limits and prices for the workchain

Output Returns the structure Gas() containing 4 values:

  • gas_limit: the maximum gas value available for any smart-contract of the workchain
  • gas_credit: the amount of gas to be credited for the execution before the smart-contract accepts the message
  • gas_max: the maximum allowed gas to be spent on the execution of the current smart-contract
  • gas_prices: a structure with gas prices

Modifies: None

def init_gas(acc_balance, msg_balance, is_external, gas_info):
    gas_max = min(gas_info.gas_limit, gas_info.calc_gas(acc_balance))
    gas_credit = 0
    if is_external:
        gas_credit = min(gas_info.gas_credit, gas_max)
        gas_limit = gas_credit
    else:
        gas_limit = min(gas_max, gas_info.calc_gas(msg_balance))

    return Gas(gas_limit, gas_credit, gas_max, gas_info.get_real_gas_price())

4.5.7 Account From Message Algorithm

The algorithm creates new account by using data from the internal message. External messages are rejected. Creation of a new account based on an external message is located elsewhere. See compute_new_state algorithm.

Input

  • msg: incoming message being processed, has type Message
  • msg_remaining_balance: the current amount of coins left on the message balance, has type Uint

Output

  • Either returns a new Account object, or None. Both results are considered successful.

Modifies: None

def account_from_message(msg, msg_remaining_balance):
    if not (msg.header is IntMsgInfo):
        return None
    if msg_remaining_balance == 0:
        return None

    header = msg.header
    init = msg.state_init

    if init != None and init.code != None and hash(init) == header.dst.address:
        return Account.active_by_init_code_hash(hdr.dst, msg_remaining_balance,
                                                0, init)
    if header.bounce:
        return None
    else:
        return Account.uninit(hdr.dst, 0, 0, msg_remaining_balance)

4.6 Action Phase

By given ordered action list, the Action phase executes each action item in the list by applying proper action handler.

Input:

  • tr - transaction being constructed, has type Transaction
  • account - account executing the message, has type Account
  • original_acc_balance - account balance after storage and credit phase, has type Uint
  • acc_balance -the mutable copy of the original_acc_balance, has type Uint
  • msg_remaining_balance - message balance without debt value if any, has type Uint
  • compute_phase_fees - gas fees from the compute phase, has type Uint
  • actions - list of actions generated on the Compute Phase, has type list(OutAction)
  • new_data - the smart-contract data after the Compute Phase, some binary blob.

Output:

On success, returns Ok(phase, messages) such that:

On error, returns Err(result_code), such that:

  • result_code describes a type of an error, see here.

Modifies:

  • tr
  • account
  • acc_balance
  • msg_remaining_balance
MAX_ACTIONS = 255

def action_phase(tr, account, original_acc_balance, acc_balance,
                 msg_remaining_balance, compute_phase_fees, actions, new_data):

  acc_copy = account.clone()
  acc_remaining_balance = acc_balance
  phase = TrActionPhase()
  total_reserved_value = 0

  # Serialization issues are put aside, it is too low-level for
  # our purpose.

  # Interesting to note, actions overload leads to OK, not Error?
  if len(actions) > MAX_ACTIONS:
      phase.result_code = RESULT_CODE_TOO_MANY_ACTIONS
      return Ok(phase, [])

  phase.action_list_hash = hash(actions)
  phase.tot_actions = len(actions)

  account_deleted = False
  out_msgs_tmp = []

  address = acc_copy.address

  for action in actions:
      if action is OutAction.SendMsg:
          if action.mode & SENDMSG_ALL_BALANCE:
              out_msgs_tmp.push((action.mode, action.out_msg))
              continue
          result = outmsg_action_handler(phase,
                                         action.mode,
                                         action.out_msg,
                                         acc_remaining_balance,
                                         msg_remaining_balance,
                                         compute_phase_fees,
                                         config,
                                         address,
                                         total_reserved_value,
                                         account_deleted)
          if result is Ok:
              phase.msgs_created += 1
              out_msgs_tmp.push((action.mode, action.out_msg))
          else:
              return result
      elif action is OutAction.ReserveCurrency:
          result = reserve_action_handler(action.mode,
                                          action.value,
                                          original_acc_balance,
                                          acc_remaining_balance)
          if result is Ok:
              phase.spec_actions += 1
              total_reserved_value += result.reserved_value
          else:
              phase.valid = True
              phase.result_code = result
              # phase.no_funds = True
              return Ok(phase, [])
      else:
          return Ok(phase, [])

  # process messages that have SENDMSG_ALL_BALANCE flag set last
  # skipping all other already processed messages
  out_msgs = []
  for (mode, out_msg) in out_msgs_tmp:
      if not (mode & SENDMSG_ALL_BALANCE):
          out_msgs.push(out_msg)
          continue
      result = outmsg_action_handler(phase, mode, out_msg,
                                     acc_remaining_balance,
                                     msg_remaining_balance,
                                     compute_phase_fees,
                                     config,
                                     address,
                                     total_reserved_value,
                                     account_deleted)
      if result == Ok:
          phase.msgs_created += 1
          out_msgs.push(out_msg)
      else:
          return Ok(phase, [])

  acc_remaining_balance += total_reserved_value
  tr.total_fee += phase.total_action_fees

  if account_deleted:
      phase.status_change = AccStatusChange.Deleted

  phase.valid = True
  phase.success = True

  acc_balance = acc_remaining_balance
  account = acc_copy
  account.data = new_data

  return Ok(phase, out_msgs)

4.6.1 Action Phase Success Condition

All actions formed at the Compute Phase were successfully processed. If an action had a special error-canceling flag set, such error will not result in the whole phase failure. The action will be skipped in this case.

4.6.2 Action Phase Validity Condition

To specify the validity condition, we will define the opposite, i.e. when the action phase is considered invalid.

  • The number of actions in the action list is greater than MAX_ACTIONS
  • Any SendMsg action processing finished with an error
  • The unknown action type was found during the processing

In all other cases, the action phase is considered valid.

4.6.3 SendMsg Action Handler

The SendMsg action handler is responsible for generating messages to be sent. It may fail due to several reasons. In this case, the action phase get stopped, unless the SENDMSG_IGNORE_ERROR flag is set.

Input:

  • phase - actual Action Phase Descriptor
  • mode - flags for sending the message
  • msg - message being sent, has type Message
  • acc_balance - actual account balance, has type UInt
  • msg_balance - the message balance after debt being deducted, has type Uint
  • compute_phase_fees - gas fees from Compute Phase
  • config - blockchain configuration, has type BlockchainConfig
  • my_addr - account address
  • reserved_value - the value of coins reserved by the ReserveCoins actions
  • account_deleted - the output value, set to True if account needs to be deleted

Output:

On success, returns Ok(value), such that:

  • value - the amount of coins to be deducted from the account balance

On failure, returns Err(result_code), such that:

  • result_code - describes the error, see here

Modifies:

  • phase
  • mode
  • msg
  • acc_balance
  • msg_balance
  • account_deleted

MAX_MSG_BITS = 2**21  # 2 Mb
MAX_MSG_CELLS = 2**13

def get_fwd_prices(config, is_masterchain):
    if is_masterchain:
        return config.fwd_prices_mc
    else:
        return config.fwd_prices_wc

def outmsg_action_handler(phase, mode, msg, acc_balance, msg_balance,
                          compute_phase_fees, config, my_addr, reserved_value,
                          account_deleted):
    invalid_flags = SENDMSG_REMAINING_MSG_BALANCE or SENDMSG_ALL_BALANCE
    mode_not_valid = mode and (not SENDMSG_VALID_FLAGS)
    mode_has_invalid = mode and invalid_flags == invalid_flags
    mode_delete_not_sab = (mode and SENDMSG_DELETE_IF_EMPTY) and \
        (not (mode and SENDMSG_ALL_BALANCE))
    if mode_not_valid or mode_has_invalid or mode_delete_not_sab:
        return Err(RESULT_CODE_UNSUPPORTED)

    skip = not (mode and SENDMSG_IGNORE_ERROR)

    msg.header.src = my_addr
    fwd_prices = config.get_fwd_prices(msg.is_masterchain())
    compute_wd_fee = fwd_prices.fwd_fee(Cell(msg))

    # The message should be either internal message or event.
    # It is impossible to send external message from the smart-contract.
    if not ((msg.header is IntMsgInfo) or (msg.header is ExtOutMsgInfo)):
        return Err(-1)

    if msg.header is IntMsgInfo:
        # =====================================
        # Internal message
        # =====================================
        msg.header.bounced = False
        result_value = msg.header.value
        msg.header.ihr_disabled = True
        msg.header.ihr_fee = 0

        fwd_fee = max(msg.header.fwd_fee, compute_wd_fee)
        fwd_mine_fee = fwd_prices.mine_fee(fwd_fee)
        total_fwd_fees = fwd_fee + msg.header.ihr_fee
        fwd_remain_fee = fwd_fee - fwd_mine_fee

        if (mode and SENDMSG_ALL_BALANCE):
            result_value = acc_balance
            msg.header.value = acc_balance
            mode = (mode and (not SENDMSG_PAY_FEE_SEPARATELY))

        if (mode and SENDMSG_REMAINING_MSG_BALANCE):
            # Send all the remaining balance of the inbound message
            result_value += msg_balance
            if not (mode and SENDMSG_PAY_FEE_SEPARATELY):
                if result_value < compute_phase_fees:
                    return Err()
                result_value -= compute_phase_fees
            msg.header.value = result_value

        if (mode and SENDMSG_PAY_FEE_SEPARATELY):
            result_value += total_fwd_fees
        else:
            if msg.header.value < total_fwd_fees:
                return Err()
            else:
                msg.header.value -= total_fwd_fees
        msg.header.fwd_fee = fwd_remain_fee
    else:
        # =====================================
        # Event
        # =====================================
        fwd_mine_fee = compute_fwd_fee
        total_fwd_fees = compute_fwd_fee
        result_value = compute_fwd_fee

    if acc_balance < result_value:
        return Err(RESULT_CODE_NOT_ENOUGH_GRAMS)

    if (mode and SENDMSG_DELETE_IF_EMPTY) and \
       (mode and SENDMSG_ALL_BALANCE) and \
       (acc_balance + reserved_value == 0):
        account_deleted = True

    if total_fwd_fees != 0:
        phase.total_fwd_fees += total_fed_fees

    if fwd_mine_fee != 0:
        phase.total_action_fees += fwd_mine_fee

    phase.tot_msg_size.append(Cell(msg))

    if phase.tot_msg_size.bits() > MAX_MSG_BITS or \
       phase.tot_msg_size.cells() > MAX_MSG_CELLS:
        return Err(RESULT_CODE_INVALID_BALANCE)

    if mode and (SENDMSG_ALL_BALANCE or SENDMSG_REMAINING_MSG_BALANCE):
        msg_balance = 0

    return Ok(result_value)

4.6.4 ReserveCurrency Action Handler

ReserveCurrency action handler is responsible for managing the reserve coins.

Input:

  • mode - Reserve flags for the reserve action, has type Uint
  • val - amount of coins to be reserved, has type Uint
  • orig_acc_balance - account balance after the deduction of the storage fee and the debt, if any, has type Uint
  • acc_remaining_balance - amount of coins left on the balance after the reserve, has type Uint

Output:

On success, returns Ok(reserved) value, such that:

  • reserved denotes the amount of coins being reserved for the account

On failure, returns Err(result_code), such that:

  • result_code

Modifies:

  • acc_remaining_balance - remaining account balance after the reserve amount being withheld
def reserve_action_handler(mode, val, orig_acc_balance, acc_remaining_balance):
    if mode and (not RESERVE_VALID_MODES):
        return Err(RESULT_CODE_UNKNOWN_OR_INVALID_ACTION)

    reserved = 0
    if mode and RESERVE_PLUS_ORIG:
        if mode and RESERVE_REVERSE:
            reserved = orig_acc_balance
            if reserved < val:
                return Err(RESULT_CODE_UNSUPPORTED)
            reserved -= val
        else:
            reserved = val
            reserved += orig_acc_balance
    else:
        if mode and RESERVE_REVERSE:
            return Err(RESULT_CODE_UNKNOWN_OR_INVALID_ACTION)
        reserved = val

    if mode and RESERVE_IGNORE_ERROR:
        reserved = min(reserved, acc_remaining_balance)

    remaining = acc_remaining_balance
    if remaining < reserved:
        return Err(RESULT_CODE_NOT_ENOUGH_GRAMS)

    remaining -= reserved
    remaining, acc_remaining_balance = acc_remaining_balance, remaining

    if mode and RESERVE_ALL_BUT:
        reserved, acc_remaining_balance = acc_remaining_balance, reserved

    return Ok(reserved)

4.7 Bounce Phase

If error happens on the previous phases, the bounce phase takes place.

Input:

  • remaining_msg_balance - message balance after all previous phases executed, has type Uint
  • acc_balance - remaining account balance after all previous phases executed, has type Uint
  • compute_phase_fees - the fees of the compute phase, has type Uint
  • msg - message being processed, has type Message
  • tr - transaction object, has type Transaction

Output:

On success, returns Ok(TrBouncePhase, bounce_message), such that:

  • TrBouncePhase is a Bounce Phase Descriptor
  • bounce_message is a bounce message to be included into the out_msgs queue

On error, returns ExecutorError.TrExecutorError

Modifies:

  • tr - adds the bounce message delivery fee to the total

NOTE:

  • Function get_fwd_prices() was defined here.
def bounce_phase(remaining_msg_balance, acc_balance,
                 compute_phase_fees, msg, tr):
    header = msg.header
    if not header.bounce:
        return ExecutorError.TrExecutorError()

    header2 = header.clone()
    header2.src, header2.dst = header.dst, header.src
    storage = StorageUsedShort()
    fwd_prices = config.get_fwd_prices(msg.is_masterchain)
    fwd_full_fees = fwd_prices.fwd_fee(Cell())
    fwd_mine_fees = fwd_prices.mine_fee(fwd_full_fees)
    fwd_fees = fwd_full_fees - fwd_mine_fees

    if remaining_msg_balance < fwd_full_fees + compute_phase_fees:
        return Ok(TrBouncePhase.no_funds, None)

    acc_balance -= remaining_msg_balance
    remaining_msg_balance -= fwd_full_fees
    remaining_msg_balance -= compute_phase_fees
    header2.ihr_disabled = True
    header2.bounce = False
    header2.bounced = True
    header2.ihr_fee = 0
    header2.fwd_fee = fwd_fees
    header2.value = remaining_msg_balance

    bounce_msg = Message.with_header(header2)
    if config.has_capability(GlobalCapabilities.CapBounceMsgBody):
        body = msg.body.clone()
        body.shrink_data(0..256) # leave only 256 bits of the original body
        bounce_msg.body = body

    tr.total_fees += fwd_mine_fees
    return Ok(TrBouncePhase.ok(storage, fwd_mine_fees, fwd_fees), bounce_msg)

5 Functional Properties

In this section, we define the main risks of malfunction in the module, and define several higher-level properties that should hold for the module to mitigate those risks.

5.1 Risks

We define a risk as a hazard event causing a significant loss for the end user. We distinguish the following types of risks.

5.1.1 Financial Risks

The Transaction Executor is the only place in the Node that is responsible for changing the account balance. Hence, any errors in related operations lead to tokens loss for the user. We identify the following financial risks for the module:

  1. Incorrect storage, delivery or gas fees calculation logic
  2. Incorrect message value processing logic
  3. Incorrect SendMsg, ReserveCoins actions processing logic

5.1.2 Behavioral Risks

EverScale blockchain praises the distributed programming paradigm in application development. It means that instead of producing huge smart-contract monoliths, it is encouraged to separate the system into many manageable smart-contracts that communicate with each other by means of message passing.

The message passing scheme used in a system induces some protocol. If message passing breaks in an unexpected way, the whole protocol may stall, potentially leading to global system deadlocks.

It is of utter importance to guarantee that all produced correct messages will be eventually delivered to the destination account. The delivery process is complicated and rely on several node components. Here, we identify risks related to the Transaction Executor part of it:

  • Successful SendMsg action does not lead to creation of a corresponding message
  • Generated messages do not occur in the Out Message queue
  • Message delivery order gets broken
  • A bounce message does not get generated as expected

5.2 Assumptions

All the properties formulated with the following assumptions in mind:

  • We consider only ordinary accounts, not system (special) accounts. For the latter, the properties might look different.

5.3 System Properties

System properties are high-level general statements on the system behavior that the Transaction Executor should obey to. A subset of those statements related to mitigating the main risks, identified in the previous section.

5.3.1 Fees

  • FEE1. Gas fees for the computation equal the amount calculated using the algorithm 4.5.3.
  • FEE2. Storage fees for an account equal the amount calculated using the algorithm 3.10.6.2.
  • FEE3. Forwarding fees are calculated according to the algorithm 3.10.5 .
  • FEE4. During the message execution process, all type of fees get deducted only once for an account.

5.3.2 Message Processing

  • MSG1. The message coins get credited to the account balance before executing a smart-contract logic.
  • MSG2. Messages delivery order between the current account \(a_1\) and some other account \(a_2\) does not depend on messages sent from \(a_1\) to some other account \(a_3\), when \(a_2 \neq a_3\).

5.3.3 Credit Phase Processing

  • CRD1. If the inbound message is external, the credit phase does not get executed.
  • CRD2. If the inbound message is internal, the account's balance get credited with the message value minus the account debt, if any.

5.3.4 Storage Phase Processing

  • STR1. If there is not enough funds to cover the storage phase fee on the account's balance, and if the account is in the Active status, then the account gets a debt storing in the due_payment field of the account. If the debt value exceeds the freeze_due_limit value, the account is switched into a frozen status. If the debt exceeds the delete_due_limit value, the account gets deleted.

5.3.5 Compute Phase Processing

  • CMP1. If the Compute Phase fails, the execution of a message is aborted. The bounce message is not created in this case.
  • CMP2. After the Compute Phase, the account's balance gets decreased exactly on the amount of consumed gas.

5.3.6 Action Phase Processing

  • ACT1. Each successful SendMsg action leads to creation of a message.
  • ACT2. Successfully created message is added into the out queue exactly once.
  • ACT3. If the action phase fails and the incoming message has the bounce flag set, then a single bounce message is generated and put into the out queue.

5.3.7 Bounce Phase Processing

  • BNC1. The bounce message is generated only if and only if all of the following conditions hold:
    1. The incoming message is an internal message
    2. The incoming message has the bounce flag set
    3. During the message processing, the action phase was executed, but failed
    4. After the failed action phase, there is enough funds left on the incoming message balance to cover the bounce message processing 6
  • BNC2. A bounce message attach all the original message value minus the storage, gas and delivery fees.

Footnotes:

1

In the original TON, message dichotomy is different: they distinguish 4 types of messages: (inbound + outbound) * (internal + external). We find this dichotomy a bit tedious to use in practice.

2

In the current protocol implementation, not all validator nodes process external messages. This is subject to change in the future protocol versions.

3

For example, by sending GraphQL requests to the DApp-server.

4

Well, at least, until the part of the chain residing the transaction gets cut-off to reduce the disk space consumption.

5

See the definition of COMMIT TVM instruction.

6

Note that bounce message delivery fee is conducted from the message balance, not the account balance.