Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEP-366: Meta transactions #366

Merged
merged 9 commits into from
Nov 24, 2022
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Changes to the protocol specification and standards are called NEAR Enhancement
|[0245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) | Multi Token Standard | @zcstarr @riqi @jriemann @marcos.sun | Review |
|[0297](https://github.com/near/NEPs/blob/master/neps/nep-0297.md) | Events Standard | @telezhnaya | Final |
|[0330](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) | Source Metadata | @BenKurrek | Review |
|[0366](https://github.com/near/NEPs/blob/master/neps/nep-0000.md) | Meta Transactions | @ilblackdragon | Draft |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
|[0366](https://github.com/near/NEPs/blob/master/neps/nep-0000.md) | Meta Transactions | @ilblackdragon | Draft |
|[0366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md) | Meta Transactions | @ilblackdragon | Draft |




Expand Down
148 changes: 148 additions & 0 deletions neps/nep-0366.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
NEP: 366
Title: Meta Transactions
Author: Illia Polosukhin <ilblackdragon@gmail.com>
DiscussionsTo: https://github.com/nearprotocol/neps/pull/366
Status: Draft
Type: Protocol Track
Category: Runtime
Created: 21-Jun-2022
---

## Summary

In-protocol meta transactions allowing for third-party account to initiate and pay transaction fees on behalf of the account.

## Motivation

NEAR has been designed with simplicity of onboarding in mind. One of the large hurdles right now is that after creating an implicit or even named account the user does not have NEAR to pay gas fees to interact with apps.

For example, apps that pay user for doing work (like NEARCrowd or Sweatcoin) or free-to-play games.

[Aurora Plus](https://aurora.plus) has shown viability of the relayers that can offer some number of free transactions and a subscription model. Shifting the complexity of dealing with fees to the infrastructure from the user space.

## Rationale and alternatives

Proposed here design provides the easiest for users and developers way to onboard and pay for user transactions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Proposed here design provides the easiest for users and developers way to onboard and pay for user transactions.
The proposed design here provides the easiest way for users and developers to onboard and to pay for user transactions.

nit: grammar

There is no requirement on the user account, including user account may not even exist on chain and implicit account can be used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
There is no requirement on the user account, including user account may not even exist on chain and implicit account can be used.
There are no requirements on the user account. The user account may not even exist on the chain and an implicit account can be used instead.

nit: grammar


An alternative is proxy contracts deployed on the user account.
This design has severe limitations as it requires user to deploy such contract and additional costs for storage.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
An alternative is proxy contracts deployed on the user account.
This design has severe limitations as it requires user to deploy such contract and additional costs for storage.
An alternative is to use proxy contracts deployed on the user account.
This design has severe limitations as it requires the user to deploy such contracts and incur additional costs for storage.


## Specification

The main flow of the meta transaction will be as follows:
- User will sign `DelegateActionMessage` specifying set of actions that they need to be executed. It also includes specific relayer to ensure secure execution.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- User will sign `DelegateActionMessage` specifying set of actions that they need to be executed. It also includes specific relayer to ensure secure execution.
- User signs a `DelegateActionMessage` specifying the set of actions that they need to be executed. It will also specify a relayer to ensure secure execution.

- User sends signed `DelegateAction` data to the relayer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a user send DelegateAction data to the relayer out off-chain?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of having two structures: DelegateActionMessage and DelegateAction? A user can send DelegateActionMessage to the relayer and the realyer can add DelegateActionMessage to the transaction as an action. I think, this is simpler.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for explaining more clearly the role of the two messages.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also - does user send a DelegateAction or DelegateActionMessage (based on context, I think it is the latter - as this is the one that was created in the previous point)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- User sends signed `DelegateAction` data to the relayer
- User sends the signed `DelegateAction` data to the relayer

- Relayer forms a `Transaction` with `receiver_id` equal to the user's account and `actions: [DelegateAction { ... }]`, and signs it with it's key. Note, that such transaction can contain other actions toward user's account (for example calling a function).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify, do you mean here that the protocol specifies that for meta transactions, the actions field is a singleton that consists of one DelegateAction? In other words, if a transaction contains more than one action and one of them is DelegateAction, then this transaction is invalid

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if that was the goal, but I don't think having this as a singleton is mandatory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Relayer forms a `Transaction` with `receiver_id` equal to the user's account and `actions: [DelegateAction { ... }]`, and signs it with it's key. Note, that such transaction can contain other actions toward user's account (for example calling a function).
- Relayer forms a `Transaction` with `receiver_id` equal to the user's account and `actions: [DelegateAction { ... }]` and signs it with it's key. Note that such transactions can contain other actions toward user's account (for example calling a function).

- This transaction is processed normally, by creating a receipt with copy of the all the actions into the `Receipt`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- This transaction is processed normally, by creating a receipt with copy of the all the actions into the `Receipt`.
- This transaction is processed normally. A `Receipt` is created with a copy of the actions in the transaction.

- When processing `DelegateAction` a number of checks are done (see below), mainly `signature` matching user account's key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- When processing `DelegateAction` a number of checks are done (see below), mainly `signature` matching user account's key.
- When processing a `DelegateAction`, a number of checks are done (see below), mainly a check to ensure that the `signature` matches the user account's key.

- When such `Receipt` with valid `DelegateAction` in actions arrives to the user's account it gets executed. The executed means creation of a new Receipt with `receiver_id: AccountId`, `actions: Action` matching `receiver_id` and `actions` inside `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- When such `Receipt` with valid `DelegateAction` in actions arrives to the user's account it gets executed. The executed means creation of a new Receipt with `receiver_id: AccountId`, `actions: Action` matching `receiver_id` and `actions` inside `DelegateAction`.
- When a `Receipt` with a valid `DelegateAction` in actions arrives at the user's account, it gets executed. Execution means creation of a new Receipt with `receiver_id: AccountId` and `actions: Action` matching `receiver_id` and `actions` in the `DelegateAction`.

- The new `Receipt` looks like normal receipt that would have been originating from user's account, with `predeccessor_id` equal to user's account.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear what signer_id should be equal to. According to the discussion in near/nearcore#7497 it should be equal to Relayer's account.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that using signer_id should be considered unsafe most of the time. See this comment for the equivalent in Ethereum: ethereum/solidity#683 (comment).


Having said that I think signer_id should be the account that is paying for the attached balance (not gas), and if I understand correctly it is not the relayer, but the user signing the transaction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The new `Receipt` looks like normal receipt that would have been originating from user's account, with `predeccessor_id` equal to user's account.
- The new `Receipt` looks like a normal receipt that could have originated from the user's account, with `predeccessor_id` equal to user's account.


### DelegateAction

Delegate action allows for an account to initiate a batch of actions on behalf of the receiving account, allowing to proxy actions. This is used in implementation of meta transactions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Delegate action allows for an account to initiate a batch of actions on behalf of the receiving account, allowing to proxy actions. This is used in implementation of meta transactions.
Delegate actions allows for an account to initiate a batch of actions on behalf of a receiving account, allowing proxy actions. This can be used to implement meta transactions.


```rust
pub struct DelegateAction {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we plan to add DelegateAction to the Action enum?

/// Receiver of the delegated actions.
receiver_id: AccountId,
/// List of actions to be executed.
actions: Vec<Action>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vec<Action> here leads to a type recursion:

pub struct Transaction {
   ....
   pub actions: Vec<Action>, // <--- Recursion
}

pub enum Action {
   ...
   Delegate(DelegateAction),
}

pub struct DelegateAction {
   ...
   pub actions: Vec<Action>, /// <--- Recursion
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is acceptable in this context because an Action can't refer to itself or an ancestor, so recursion is finite.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant, rust compiler would not compile such code because of the recursion.
Example code in playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a6c54d6ed7588eb03d2ad19da1c0a9c6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, a common way to prevent this recursion is to use a Box. As an example, a singly-linked list in Rust can be represented as:

struct Node<T> {
    element: T,
    next: Option<Box<Node<T>>>,
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=14f24298eee755a2a08c7b5b66d8b4fa suggests a fix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. When I wrote my comment, I didn't know that Borsh supported Box. But there is another issue with this structure: Borsh doesn't support a recursive types near/borsh-rs#96

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the video call, for the purposes of the NEP, the recursive data structure is fine. For nearcore, we will have to come up with a workaround. Best to discuss on Zulip.


/// Public key that is used to sign this delegated action.
signer_pk: PublicKey,
/// Nonce is used to determine that the same delegate action is not sent twice by relayers and should match for given account's `signer_pk`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Nonce is used to determine that the same delegate action is not sent twice by relayers and should match for given account's `signer_pk`.
/// Nonce to ensure that the same delegate action is not sent twice by a relayer and should match for given account's `signer_pk`.

/// After this action is processed it will increment.
nonce: Nonce,
/// Signature of the originating user signing `DelegateActionMessage` formed out of the data in the `DelegateAction`.
signature: Signature,
}
```

Supporting a batch of `actions` means `DelegateAction` can initiate a complex steps like creating a new account and transferring funds, deploying a contract and executing an initialization function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Supporting a batch of `actions` means `DelegateAction` can initiate a complex steps like creating a new account and transferring funds, deploying a contract and executing an initialization function.
Supporting batches of `actions` means `DelegateAction` can be used initiate a complex steps like creating new accounts, transferring funds, deploying contracts, and executing an initialization function all within the same transaction.


***Validation***:
To ensure that `DelegateAction` is correct, on receival the verification of signature is done: `verify_signature(hash(message), signer_pk, signature)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To ensure that `DelegateAction` is correct, on receival the verification of signature is done: `verify_signature(hash(message), signer_pk, signature)`.
To ensure that a `DelegateAction` is correct, on receipt the following signature verification is performed: `verify_signature(hash(message), signer_pk, signature)`.


The `message` is formed in the next format and must be signed by the user:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `message` is formed in the next format and must be signed by the user:
A `message` is formed in the following format and it must be signed by the user:

```rust
struct DelegateActionMessage {
/// If not None, should match the `predecessor_id` that have created `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// If not None, should match the `predecessor_id` that have created `DelegateAction`.
/// If not None, then should match the `predecessor_id` that created the `DelegateAction`.

sender_id: Option<AccountId>,
/// Matching the given account_id.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you specify the "given account_id" here please?

signer_id: AccountId,
/// Matching the `signer_pk` from `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Matching the `signer_pk` from `DelegateAction`.
/// Should match the `signer_pk` from the `DelegateAction`.

public_key: PublicKey,
/// Nonce for the given `public_key` from `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Nonce for the given `public_key` from `DelegateAction`.
/// Nonce for the given `public_key` from the `DelegateAction`.

nonce: Nonce,
/// Matching the `receiver_id` from `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Matching the `receiver_id` from `DelegateAction`.
/// Should match the `receiver_id` from the `DelegateAction`.

receiver_id: AccountId,
/// Block hash to ensure validity.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field appears to be missing?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes ! as this struct was supposed to be similar to the 'Transaction' struct - it should have a 'block_hash' field here.

/// Actions matching `actions` from `DelegateAction`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Actions matching `actions` from `DelegateAction`.
/// Should match `actions` from the `DelegateAction`.

actions: Vec<Action>,
}
```

The next set of security concerns are addressed by this format:
- If format matches `Transaction`, the relayer can just send it directly, not receiving payment.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If format matches `Transaction`, the relayer can just send it directly, not receiving payment.
- If format matches `Transaction`, then the relayer can just send it directly, not receiving payment.

- Because set of actions can include pay back to the relayer (for example by paying in FT), the `sender_id` is added directly into the message to ensure that nobody else can send this message.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Because set of actions can include pay back to the relayer (for example by paying in FT), the `sender_id` is added directly into the message to ensure that nobody else can send this message.
- Because the set of actions can include payment for the relayer (for example in FT), the `sender_id` is added directly into the message to ensure that nobody else can send this message.

- `nonce` is included to ensure that this message can't be replayed again.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- `nonce` is included to ensure that this message can't be replayed again.
- `nonce` is included to ensure that a message can not be replayed.

- `public_key` and `signer_id` are needed to ensure the on the right account, work across rotating keys and fetch correct `nonce`.

The permissions are verified based on kind `AccessKeyPermission` of `signer_pk`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The permissions are verified based on kind `AccessKeyPermission` of `signer_pk`:
The permissions are verified based on the variant of `signer_pk`:

- `AccessKeyPermission::FullAccess`, all actions are allowed.
- `AccessKeyPermission::FunctionCall`, only a single `FunctionCall` action is allowed in `actions`.
- `DelegateAction.receiver_id` must match to the `account[public_key].receiver_id`
- `DelegateAction.actions[0].method_name` must be in the `account[public_key].method_names`

***Outcomes***:
- If `signature` matches receiver's accounts `signer_pk`, new receipt is created from this account with set of `ActionReceipt { receiver_id, action }` for each item in `actions`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If `signature` matches receiver's accounts `signer_pk`, new receipt is created from this account with set of `ActionReceipt { receiver_id, action }` for each item in `actions`.
- If the `signature` matches the receiver's account's `signer_pk`, a new receipt is created from this account with a set of `ActionReceipt { receiver_id, action }` for each action in `actions`.


#### Errors

**Validation Error**
- If `signer_pk` does not exist for the given account, the following error will be returned
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If `signer_pk` does not exist for the given account, the following error will be returned
- If the `signer_pk` does not exist for the given account, the following error will be returned

```rust
/// Signer key for delegate action is missing from given account
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Signer key for delegate action is missing from given account
/// Signer key for delegate action is missing from the given account

DelegateActionSignerDoesNotExist
```

- If `nonce` does not exist match `signer_pk` for `receiver_id`, the following error will be returned
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If `nonce` does not exist match `signer_pk` for `receiver_id`, the following error will be returned
- If the `nonce` does not exist match the `signer_pk` for the `receiver_id`, the the following error will be returned

```rust
/// Nonce must be account[signer_pk].nonce + 1
DelegateActionInvalidNonce
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we enforce +1 here? That is not how access key works in other cases

Copy link
Member

@mfornet mfornet Sep 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bowenwang1996 Can you share a reference about how nonces work today? I can't find it in nomicon.

```

- If `signature` does not match to the data and `signer_pk` of the given key, the following error will be returned
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- If `signature` does not match to the data and `signer_pk` of the given key, the following error will be returned
- If the `signature` does not match the data and the `signer_pk` of the given key, then the following error will be returned

```rust
/// Signature does not match the provided actions and given signer public key.
DelegateActionInvalidSignature
```


See the [DelegateAction specification](specs/RuntimeSepc/Actions.md#DelegateAction) for details.

## Security Implications

Delegate actions do not override `signer_pk`, leaving that to the original signer that initiated transaction (eg relayer in meta transaction case). Although it is possible to override `signer_pk` in the context with one from `DelegateAction`, there is no clear value to do that.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Delegate actions do not override `signer_pk`, leaving that to the original signer that initiated transaction (eg relayer in meta transaction case). Although it is possible to override `signer_pk` in the context with one from `DelegateAction`, there is no clear value to do that.
Delegate actions do not override `signer_pk`, leaving that to the original signer that initiated transaction (e.g. the relayer in the meta transaction case). Although it is possible to override the `signer_pk` in the context with one from the `DelegateAction`, there is no clear value in that.


See ***Validation*** section in [DelegateAction specification](specs/RuntimeSepc/Actions.md#DelegateAction) for security considerations around what user signs and validation of actions with different permissions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
See ***Validation*** section in [DelegateAction specification](specs/RuntimeSepc/Actions.md#DelegateAction) for security considerations around what user signs and validation of actions with different permissions.
See the ***Validation*** section in [DelegateAction specification](specs/RuntimeSepc/Actions.md#DelegateAction) for security considerations around what the user signs and the validation of actions with different permissions.


## Drawbacks

Increases complexity of the the NEAR's transactional model.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Increases complexity of the the NEAR's transactional model.
Increases complexity of NEAR's transactional model.


Meta transactions take an extra block to execute, as they first need to be included by the originating account, then routed to the delegate account and only after that to the real destination.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Meta transactions take an extra block to execute, as they first need to be included by the originating account, then routed to the delegate account and only after that to the real destination.
Meta transactions take an extra block to execute, as they first need to be included by the originating account, then routed to the delegate account, and only after that to the real destination.


Delegate actions are not programmable as they require having signatures. Original proposal contained a new `AccessKey` kind that would support programmable delegated actions. On the other hand, that would be limiting without programmability of smart contracts, hence that idea evolved into [Account Extensions](https://github.com/nearprotocol/neps/pull/346).

## Future possibilities

Supporting ZK proofs instead of just signatures can allow for an anonymous transactions, which pay fees to relayers in anonymous way.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Supporting ZK proofs instead of just signatures can allow for an anonymous transactions, which pay fees to relayers in anonymous way.
Supporting ZK proofs instead of just signatures can allow for anonymous transactions, which pay fees to relayers anonymously.


## Copyright
[copyright]: #copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
2 changes: 1 addition & 1 deletion specs/RuntimeSpec/Receipts.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Receipt

All cross-contract (we assume that each account lives in it's own shard) communication in Near happens trough Receipts.
All cross-contract (we assume that each account lives in it's 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`](#predecessor_id) (who sent it) and [`receiver_id`](#receiver_id) the current account.
Expand Down