Skip to main content

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 2 types: action receipts or data 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).

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

There are 2 types of Receipt: ActionReceipt and DataReceipt. An ActionReceipt is a request to apply Actions, while a DataReceipt is a result of the application of these actions.

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

Gas price which was set in a block where the original transaction has been applied.

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
/// 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
/// 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,
},