Receipt

All cross-contract (we assume that each account lives in its own shard) communication in Near happens through Receipts.

Receipts are stateful in a sense that they serve not only as messages between accounts but also can be stored in the account storage to await DataReceipts.

Each receipt has a predecessor_id (who sent it) and receiver_id the current account.

Receipts are one of these kinds: action receipts, data receipts, promise yield receipts, promise resume receipts and global contract distribution receipts.

Data Receipts are receipts that contains some data for some ActionReceipt with the same receiver_id. Data Receipts have 2 fields: the unique data identifier data_id and data the received result. data is an Option field and it indicates whether the result was a success or a failure. If it's Some, it means the remote execution was successful and it represents the result as a vector of bytes.

Each ActionReceipt also contains fields related to data:

  • input_data_ids - a vector of input data with the data_ids required for the execution of this receipt.
  • output_data_receivers - a vector of output data receivers. It indicates where to send outgoing data. Each DataReceiver consists of data_id and receiver_id for routing.

Before any action receipt is executed, all input data dependencies need to be satisfied. Which means all corresponding data receipts have to be received. If any of the data dependencies are missing, the action receipt is postponed until all missing data dependencies arrive.

Because chain and runtime guarantees that no receipts are missing, we can rely that every action receipt will be executed eventually (Receipt Matching explanation).

A promise yield receipt is an action receipt with exactly one input data_id. A contract creates it with a yield host function such as promise_yield_create. The runtime does not postpone it like other action receipts. Instead, it stores the receipt under its input data_id and runs it when a promise resume receipt with the same data_id arrives. A promise resume receipt is a data receipt. A contract creates it with a resume host function such as promise_yield_resume. If the yield is not resumed before it times out, the runtime creates a promise resume receipt with no data.

A global contract distribution receipt carries the code of a global contract. A DeployGlobalContractAction creates it. When the receipt is applied on a shard, the runtime stores the code there. It then sends a new distribution receipt to the next shard, until every shard has the code. Its receiver_id is system. The runtime sends it to the shard named in the receipt, not to the shard of an account.

Each Receipt has the following fields:

predecessor_id

  • type: AccountId

The account_id which issued a receipt. In case of a gas or deposit refund, the account ID is system.

receiver_id

  • type: AccountId

The destination account_id.

receipt_id

  • type: CryptoHash

An unique id for the receipt.

receipt

  • type: ReceiptEnum
#![allow(unused)]
fn main() {
pub enum ReceiptEnum {
    Action(ActionReceipt) = 0,
    Data(DataReceipt) = 1,
    PromiseYield(ActionReceipt) = 2,
    PromiseResume(DataReceipt) = 3,
    GlobalContractDistribution(GlobalContractDistributionReceipt) = 4,
    ActionV2(ActionReceiptV2) = 5,
    PromiseYieldV2(ActionReceiptV2) = 6,
}
}

An ActionReceipt is a request to apply Actions, while a DataReceipt is a result of the application of these actions. PromiseYield, PromiseResume and GlobalContractDistribution are the receipts described above. ActionV2 and PromiseYieldV2 carry an ActionReceiptV2, which has the fields of ActionReceipt plus refund_to. If refund_to is set, deposit refunds for the receipt go to that account instead of predecessor_id. Action receipts that contracts create use these V2 kinds.

ActionReceipt

ActionReceipt represents a request to apply actions on the receiver_id side. It could be derived as a result of a Transaction execution or another ActionReceipt processing. ActionReceipt consists the following fields:

signer_id

  • type: AccountId

An account_id which signed the original transaction. In case of a deposit refund, the account ID is system.

signer_public_key

  • type: PublicKey

The public key of an AccessKey which was used to sign the original transaction. In case of a deposit refund, the public key is empty (all bytes are 0).

gas_price

  • type: u128

The price per unit of gas at which the gas attached to this receipt was bought. For a receipt created from a transaction, it is the higher of the gas price of the block where the transaction was converted into the receipt and the min_gas_purchase_price runtime parameter. From protocol version 85 (nearcore 2.13.0), min_gas_purchase_price is 10^9 yoctoNEAR per gas. Before that, it was 0. From protocol version 85 (nearcore 2.13.0), the receipt burns gas at the lower of this price and the gas price of the block where it is executed. The price difference is refunded, except for the account creation charge when the receipt creates an account (see Refunds). A receipt created while another receipt is executed, for example by a contract call or a delegate action, has the same gas price as that receipt. Refund receipts have a gas price of 0.

output_data_receivers

  • type: [DataReceiver{ data_id: CryptoHash, receiver_id: AccountId }]

If smart contract finishes its execution with some value (not Promise), runtime creates a [DataReceipt]s for each of the output_data_receivers.

input_data_ids

  • type: [CryptoHash]_

input_data_ids are the receipt data dependencies. input_data_ids correspond to DataReceipt.data_id.

actions

DataReceipt

DataReceipt represents a final result of some contract execution.

data_id

  • type: CryptoHash

A unique DataReceipt identifier.

data

  • type: Option([u8])

Associated data in bytes. None indicates an error during execution.

Creating Receipt

Receipts can be generated during the execution of a SignedTransaction (see example) or during application of some ActionReceipt which contains a FunctionCall action. The result of the FunctionCall could be either another ActionReceipt or a DataReceipt (returned data).

Receipt Matching

Runtime doesn't require that Receipts come in a particular order. Each Receipt is processed individually. The goal of the Receipt Matching process is to match all ActionReceipts to the corresponding DataReceipts.

Processing ActionReceipt

For each incoming ActionReceipt runtime checks whether we have all the DataReceipts (defined as ActionsReceipt.input_data_ids) required for execution. If all the required DataReceipts are already in the storage, runtime can apply this ActionReceipt immediately. Otherwise we save this receipt as a Postponed ActionReceipt. Also we save Pending DataReceipts Count and a link from pending DataReceipt to the Postponed ActionReceipt. Now runtime will wait for all the missing DataReceipts to apply the Postponed ActionReceipt.

Postponed ActionReceipt

A Receipt which runtime stores until all the designated DataReceipts arrive.

  • key = account_id,receipt_id
  • value = [u8]

Where account_id is Receipt.receiver_id, receipt_id is Receipt.receipt_id and value is a serialized Receipt (which type must be ActionReceipt).

Pending DataReceipt Count

A counter which counts pending DataReceipts for a Postponed Receipt initially set to the length of missing input_data_ids of the incoming ActionReceipt. It's decrementing with every new received DataReceipt:

  • key = account_id,receipt_id
  • value = u32

Where account_id is AccountId, receipt_id is CryptoHash and value is an integer.

Pending DataReceipt for Postponed ActionReceipt

We index each pending DataReceipt so when a new DataReceipt arrives we connect it to the Postponed Receipt it belongs to.

  • key = account_id,data_id
  • value = receipt_id

Processing DataReceipt

Received DataReceipt

First of all, runtime saves the incoming DataReceipt to the storage as:

  • key = account_id,data_id
  • value = [u8]

Where account_id is Receipt.receiver_id, data_id is DataReceipt.data_id and value is a DataReceipt.data (which is typically a serialized result of the call to a particular contract).

Next, runtime checks if there are any Postponed ActionReceipt waiting for this DataReceipt by querying Pending DataReceipt to the Postponed Receipt. If there is no postponed receipt_id yet, we do nothing else. If there is a postponed receipt_id, we do the following:

If Pending DataReceipt Count is now 0 that means all the Receipt.input_data_ids are in storage and runtime can safely apply the Postponed Receipt and remove it from the store.

Case 1: Call to multiple contracts and await responses

Suppose runtime got the following ActionReceipt:

# Non-relevant fields are omitted.
Receipt{
    receiver_id: "alice",
    receipt_id: "693406"
    receipt: ActionReceipt {
        input_data_ids: []
    }
}

If execution return Result::Value

Suppose runtime got the following ActionReceipt (we use a python-like pseudo code):

# Non-relevant fields are omitted.
Receipt{
    receiver_id: "alice",
    receipt_id: "5e73d4"
    receipt: ActionReceipt {
        input_data_ids: ["e5fa44", "7448d8"]
    }
}

We can't apply this receipt right away: there are missing DataReceipt'a with IDs: ["e5fa44", "7448d8"]. Runtime does the following:

postponed_receipts["alice,5e73d4"] = borsh_serialize(
    Receipt{
        receiver_id: "alice",
        receipt_id: "5e73d4"
        receipt: ActionReceipt {
            input_data_ids: ["e5fa44", "7448d8"]
        }
    }
)
pending_data_receipt_store["alice,e5fa44"] = "5e73d4"
pending_data_receipt_store["alice,7448d8"] = "5e73d4"
pending_data_receipt_count = 2

Note: the subsequent Receipts could arrived in the current block or next, that's why we save Postponed ActionReceipt in the storage

Then the first pending Pending DataReceipt arrives:

# Non-relevant fields are omitted.
Receipt {
    receiver_id: "alice",
    receipt: DataReceipt {
        data_id: "e5fa44",
        data: "some data for alice",
    }
}
data_receipts["alice,e5fa44"] = borsh_serialize(Receipt{
    receiver_id: "alice",
    receipt: DataReceipt {
        data_id: "e5fa44",
        data: "some data for alice",
    }
};
pending_data_receipt_count["alice,5e73d4"] = 1`
del pending_data_receipt_store["alice,e5fa44"]

And finally the last Pending DataReceipt arrives:

# Non-relevant fields are omitted.
Receipt{
    receiver_id: "alice",
    receipt: DataReceipt {
        data_id: "7448d8",
        data: "some more data for alice",
    }
}
data_receipts["alice,7448d8"] = borsh_serialize(Receipt{
    receiver_id: "alice",
    receipt: DataReceipt {
        data_id: "7448d8",
        data: "some more data for alice",
    }
};
postponed_receipt_id = pending_data_receipt_store["alice,5e73d4"]
postponed_receipt = postponed_receipts[postponed_receipt_id]
del postponed_receipts[postponed_receipt_id]
del pending_data_receipt_count["alice,5e73d4"]
del pending_data_receipt_store["alice,7448d8"]
apply_receipt(postponed_receipt)

Receipt Validation Error

Some postprocessing validation is done after an action receipt is applied. The validation includes:

  • Whether the generated receipts are valid. A generated receipt can be invalid, if, for example, a function call generates a receipt to call another function on some other contract, but the contract name is invalid. Here there are mainly two types of errors:

  • account id is invalid. If the receiver id of the receipt is invalid, a

#![allow(unused)]
fn main() {
/// The `receiver_id` of a Receipt is not valid.
InvalidReceiverId { account_id: AccountId },
}

error is returned.

  • some action is invalid. The errors returned here are the same as the validation errors mentioned in actions.
  • Whether the account still has enough balance to pay for storage. If, for example, the execution of one function call action leads to some receipts that require transfer to be generated as a result, the account may no longer have enough balance after the transferred amount is deducted. In this case, a
#![allow(unused)]
fn main() {
/// ActionReceipt can't be completed, because the remaining balance will not be enough to cover storage.
LackBalanceForState {
    /// An account which needs balance
    account_id: AccountId,
    /// Balance required to complete an action.
    amount: Balance,
},
}