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
3 changes: 2 additions & 1 deletion 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-0366.md) | Meta Transactions | @ilblackdragon @e-uleyskiy @fadeevab | Draft |



Expand Down Expand Up @@ -68,7 +69,7 @@ Spec changes are ultimately done via pull requests to this repository (formalize
```

* Once complete, submit the pull request for editor review.

* The formalization dance begins:
* NEP Editors, who are unopinionated shepherds of the process, check document formatting, completeness and adherence to [NEP-0001](neps/nep-0001.md) and approve the pull request.
* Once ready, the author updates the NEP status to `Review` allowing further community participation, to address any gaps or clarifications, normally part of the Review PR.
Expand Down
Binary file added neps/assets/nep-0366/NEP-DelegateAction.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
203 changes: 203 additions & 0 deletions neps/nep-0366.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
NEP: 366
Title: Meta Transactions
Author: Illia Polosukhin <ilblackdragon@gmail.com>, Egor Uleyskiy (egor.ulieiskii@gmail.com), Alexander Fadeev (fadeevab.com@gmail.com)
DiscussionsTo: https://github.com/nearprotocol/neps/pull/366
Status: Draft
Type: Protocol Track
Category: Runtime
Created: 19-Oct-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

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

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

## Specification

* **User** (Sender) is the one who is going to send the `DelegateAction` to Receiver via Relayer.
* **Relayer** is the one who publishes the `DelegateAction` to the protocol.
* **User** and Relayer doesn't trust each other.

The main flow of the meta transaction will be as follows:
- User specifies `sender_id` (the user's account id), `receiver_id` (the receiver's account id) and other information (see `DelegateAction` format).
- User signs `DelegateAction` specifying the set of actions that they need to be executed.
- User forms `SignedDelegateAction` with the `DelegateAction` and the signature.
- User forms `DelegateActionMessage` with the `SignedDelegateAction`.
- User sends `DelegateActionMessage` data to the relayer.
- Relayer verifies actions specified in `DelegateAction`: the total cost and whether the user included the reward for the relayer.
- Relayer forms a `Transaction` with `receiver_id` equals to `delegate_action.sender_id` and `actions: [SignedDelegateAction { ... }]`. Signs it with its key. Note that such transactions can contain other actions toward user's account (for example calling a function).
- This transaction is processed normally. A `Receipt` is created with a copy of the actions in the transaction.
- When processing a `SignedDelegateAction`, a number of checks are done (see below), mainly a check to ensure that the `signature` matches the user account's key.
- When a `Receipt` with a valid `SignedDelegateAction` 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 a normal receipt that could have originated from the user's account, with `predeccessor_id` equal to tbe user's account, `signer_id` equal to the relayer's account, `signer_public_key` equal to the relayer's public key.

## Diagram
![](assets/nep-0366/NEP-DelegateAction.png)

## Limitations
* If User account exist, then deposit and gas are refunded as usual: gas is refuned to Relayer, deposit is refunded to User.
* If User account doesn't exist then gas is refunded to Relayer, deposit is burnt.
* `DelegateAction` actions mustn't contain another `DelegateAction` (`DelegateAction` can't conatin the nested ones).

### DelegateAction

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?

/// Signer of the delegated actions
sender_id: AccountId,
/// 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.

/// Nonce to ensure that the same delegate action is not sent twice by a relayer and should match for given account's `public_key`.
/// After this action is processed it will increment.
nonce: Nonce,
/// The maximal height of the block in the blockchain below which the given DelegateAction is valid.
max_block_height: BlockHeight,
/// Public key that is used to sign this delegated action.
public_key: PublicKey,
}
```

```rust
pub struct SignedDelegateAction {
delegate_action: DelegateAction,
/// Signature of the `DelegateAction`.
signature: Signature,
}
```

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***:
1. Validate `DelegateAction` doesn't contain a nested `DelegateAction` in actions.
2. To ensure that a `DelegateAction` is correct, on receipt the following signature verification is performed: `verify_signature(hash(delegate_action), delegate_action.public_key, signature)`.
3. Verify `transaction.receiver_id` matches `delegate_action.sender_id`.
4. Verify `delegate_action.max_block_height`. The `max_block_height` must be greater than the current block height (at the `DelegateAction` processing time).
5. Verify `delegate_action.sender_id` owns `delegate_action.public_key`.
6. Verify `delegate_action.nonce > sender.access_key.nonce`.

A `message` is formed in the following format:
```rust
struct DelegateActionMessage {
signed_delegate_action: SignedDelegateAction
}
```

The next set of security concerns are addressed by this format:
- `sender_id` is included to ensure that the relayer set correct `transaction.receiver_id`.
- `max_block_height` is included to ensure that the `DelegateAction` isn't expired.
- `nonce` is included to ensure that the `DelegateAction` can't be replayed again.
- `public_key` and `sender_id` are needed to ensure that on the right account, work across rotating keys and fetch correct `nonce`.

The permissions are verified based on the variant of `public_key`:
- `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 the `signature` matches the receiver's account's `public_key`, a new receipt is created from this account with a set of `ActionReceipt { receiver_id, action }` for each action in `actions`.


***Recommendations***
- Because the User doesn't trust the Relayer, the User should verify whether the Relayer has submitted the `DelegateAction` and the execution result.

#### Errors

- If the Sender's account doesn't exist
```rust
/// Happens when TX receiver_id doesn't exist
AccountDoesNotExist
```

- If the `signature` does not match the data and the `public_key` 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
```

- If the `sender_id` doesn't match the `tx.receiver_id`
```rust
/// Receiver of the transaction doesn't match Sender of the delegate action
DelegateActionSenderDoesNotMatchReceiver
```

- If the current block is equal or greater than `max_block_height`
```rust
/// Delegate action has expired
DelegateActionExpired
```

- If the `public_key` does not exist for Sender account
```rust
/// The given public key doesn't exist for Sender account
DelegateActionAccessKeyError
```

- If the `nonce` does match the `public_key` for the `sender_id`
```rust
/// Nonce must be greater sender[public_key].nonce
DelegateActionInvalidNonce
```

- If `nonce` is too large
```rust
/// DelegateAction nonce is larger than the upper bound given by the block height (block_height * 1e6)
DelegateActionNonceTooLarge
```

- If the list of delegated actions contains another `DelegateAction`
```rust
/// DelegateAction actions contain another DelegateAction
DelegateActionCantContainNestedOne
```

- If the list of Transaction actions contains several `DelegateAction`
```rust
/// There should be the only one DelegateAction
DelegateActionMustBeOnlyOne
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why this is the case? I don't see the reasoning in this doc; sorry if I skimmed over it. This seems like a benefit to have to be able to schedule multiple meta transactions within a single transaction, to minimize costs for the relayer.

If you actually want this to be the only thing executed within the transaction, should regular actions not be allowed with a delegate action? Currently, the draft implementation seems to allow other actions with a delegate action, and I just want to make sure this is intended

Copy link
Contributor

@e-uleyskiy e-uleyskiy Jan 12, 2023

Choose a reason for hiding this comment

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

@austinabell
I added this case for two reasons:

  1. At this moment all actions in the transaction are executed in one by one. But in case of DelegateAction, they will be executed in parallel (inner actions will be executed in parallel). I think such behavior might be confusing for developer/users.
  2. For code simplification. If we allow several DelegateAction in a transaction, we should decide how limits should be handled. For example there is the limit for total number of actions in the transactions. Should it be shared between DelegateAction and the transaction or we need to define a new limit for number of actions in DelegateAction?

```

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

## Security Implications

Delegate actions do not override `signer_public_key`, 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_public_key` in the context with one from the `DelegateAction`, there is no clear value in that.

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

## Drawbacks

* 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.
* User can't call functions from different contracts in same `DelegateAction`. This is because `DelegateAction` has only one receiver for all inner actions.
* The Relayer must verify the most of parameters before submitting `DelegateAction`, making sure that one of the function calls is the reward action. Either way, this is a risk for Relayer in general.
* User must not trust Relayer’s response and should check execution errors in Blockchain.

## Future possibilities

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/).
1 change: 1 addition & 0 deletions specs/RuntimeSpec/Receipts.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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`](#predecessor_id) (who sent it) and [`receiver_id`](#receiver_id) the current account.
Expand Down