2020-06-22 12:38:04 -06:00
|
|
|
|
---
|
|
|
|
|
eip: 2733
|
2020-10-11 11:22:32 -06:00
|
|
|
|
title: Transaction Package
|
2020-06-22 12:38:04 -06:00
|
|
|
|
author: Matt Garnett (@lightclient)
|
|
|
|
|
discussions-to: https://ethereum-magicians.org/t/eip-transaction-package/4365
|
|
|
|
|
status: Draft
|
|
|
|
|
type: Standards Track
|
|
|
|
|
category: Core
|
|
|
|
|
created: 2020-06-16
|
|
|
|
|
requires: 2718
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Simple Summary
|
|
|
|
|
Creates a new transaction type which executes a package of one or more
|
2020-09-07 23:38:25 -06:00
|
|
|
|
transactions, while passing status information to subsequent transactions.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Abstract
|
2020-10-11 11:22:32 -06:00
|
|
|
|
Introduce a new transaction type which includes a list of transactions that
|
|
|
|
|
must be executed serially by clients. Execution information (e.g. success,
|
|
|
|
|
gas_used, etc.) will be propagated forward to the next transaction.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Motivation
|
2020-10-11 11:22:32 -06:00
|
|
|
|
Onboarding new users to Ethereum has been notoriously difficult due to the need
|
|
|
|
|
for new users to acquire enough ether to pay for their transactions. This
|
|
|
|
|
hurdle has seen a significant allocation of resources over the years to solve.
|
|
|
|
|
Today, that solution is meta-transactions. This is, unfortunately, a brittle
|
|
|
|
|
solution that requires signatures to be recovered within a smart contract to
|
|
|
|
|
authenticate the message. This EIP aims to provide a flexible framework for
|
|
|
|
|
relayers to "sponsor" many transactions at once, trustlessly.
|
|
|
|
|
|
|
|
|
|
Meta-transactions often use relay contracts to maintain nonces and allow users
|
|
|
|
|
to pay for gas using alternative assets. They have historically been designed
|
|
|
|
|
to catch reversions in their inner transactions by only passing a portion of
|
|
|
|
|
the available gas to the subcall. This allows them to be certain the outer call
|
|
|
|
|
will have enough gas to complete any required account, like processing a gas
|
|
|
|
|
payment. This type of subcall has been considered bad practice for a long time,
|
|
|
|
|
but in the case of where you don't trust the subcalls, it is the only available
|
|
|
|
|
solution.
|
|
|
|
|
|
|
|
|
|
Transaction packages are an alternative that allow multiple transactions to be
|
|
|
|
|
bundled into one package and executed atomically, similarly to how relay
|
|
|
|
|
contracts operate. Transactions are able to pass their result to subsequent
|
|
|
|
|
transactions. This allows for conditional workflows based on the outcome of
|
|
|
|
|
previous transactions. Although this functionality is already possible as
|
|
|
|
|
described above, workflows using transaction packages are more robust, because
|
|
|
|
|
they are protected from future changes to the gas schedule.
|
|
|
|
|
|
|
|
|
|
An important byproduct of this EIP is that it also facilitates bundling
|
|
|
|
|
transactions for single users.
|
|
|
|
|
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Specification
|
2020-10-11 11:22:32 -06:00
|
|
|
|
Introduce a new [EIP-2718](./eip-2718.md) transaction type where `id = 2`.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
#### Structure
|
2020-06-22 12:38:04 -06:00
|
|
|
|
```
|
2020-10-11 11:22:32 -06:00
|
|
|
|
struct TransactionPackage {
|
|
|
|
|
chain_id: u256,
|
|
|
|
|
children: [Child],
|
|
|
|
|
nonce: u64,
|
|
|
|
|
gas_price: u256,
|
|
|
|
|
v: u256,
|
|
|
|
|
r: u256,
|
|
|
|
|
s: u256
|
|
|
|
|
}
|
2020-06-22 12:38:04 -06:00
|
|
|
|
```
|
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
##### Hash
|
|
|
|
|
`keccak256(rlp([2, chain_id, children, nonce, gas_price, v, r, s])`
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
##### Signature Hash
|
|
|
|
|
`keccak256(rlp([2, chain_id, children, nonce, gas_price])`
|
|
|
|
|
|
|
|
|
|
##### Receipt
|
|
|
|
|
Each `Child` transaction will generate a `ChildReceipt` after execution. Each
|
|
|
|
|
of these receipts will be aggregated into a `Receipt`.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
type Receipt = [ChildReceipt]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
struct ChildReceipt {
|
|
|
|
|
status: u256,
|
|
|
|
|
cumulative_gas_used: u256,
|
2020-10-11 11:28:43 -06:00
|
|
|
|
logs_bloom: [u8; 256],
|
|
|
|
|
logs: [u8]
|
2020-10-11 11:22:32 -06:00
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### Child Transaction
|
|
|
|
|
Let `Child` be interpreted as follows.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
struct Child {
|
|
|
|
|
type: u8,
|
|
|
|
|
nonce: u64,
|
|
|
|
|
to: Address,
|
|
|
|
|
value: u256,
|
|
|
|
|
data: [u8],
|
|
|
|
|
extra: [u8],
|
|
|
|
|
max_gas_price: u256,
|
|
|
|
|
gas_limit: u256,
|
|
|
|
|
v: u256,
|
|
|
|
|
r: u256,
|
|
|
|
|
s: u256
|
|
|
|
|
}
|
|
|
|
|
```
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
##### Types
|
|
|
|
|
The `type` field is used to denote whether the `Child` signer wishes to
|
|
|
|
|
delegate the `max_gas_price` and `gas_limit` choice to the `TransactionPackage`
|
|
|
|
|
signer.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
| type | signature hash |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `0x00` | `keccak256(rlp([0, nonce, to, value, data, extra, max_gas_price, gas_limit])` |
|
|
|
|
|
| `0x01` | `keccak256(rlp([1, nonce, to, value, data, extra])` |
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
### Validity
|
|
|
|
|
|
|
|
|
|
A `TransactionPackage` can be deemed valid or invalid as follows.
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn is_valid(config: &Config, state: &State, tx: TransactionPackage) bool {
|
|
|
|
|
if config.chain_id() != tx.chain_id {
|
|
|
|
|
false
|
|
|
|
|
}
|
2020-10-11 20:24:08 -06:00
|
|
|
|
|
|
|
|
|
if tx.children.len() == 0 {
|
|
|
|
|
false
|
|
|
|
|
}
|
2020-10-11 11:22:32 -06:00
|
|
|
|
|
|
|
|
|
if state.nonce(tx.from()) + 1 != tx.nonce {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cum_limit = tx.children.map(|x| x.gas_limit).sum();
|
2020-10-11 20:24:08 -06:00
|
|
|
|
if state.balance(tx.from()) < cum_limit * tx.gas_price + intrinsic_gas(tx) {
|
2020-10-11 11:22:32 -06:00
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for child in tx.children {
|
|
|
|
|
if state.nonce(child.from()) + 1 != child.nonce {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if state.balance(child.from()) < child.value {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if child.max_gas_price < tx.gas_price {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if child.extra.len() != 0 {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
```
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
### Results
|
2020-10-11 11:22:32 -06:00
|
|
|
|
|
2020-06-22 12:38:04 -06:00
|
|
|
|
Subsequent transactions will be able to receive the result of the previous
|
2020-10-11 11:22:32 -06:00
|
|
|
|
transaction via `RETURNDATACOPY (0x3E)` in first frame of execution, before
|
2020-09-07 23:38:25 -06:00
|
|
|
|
making any subcalls. Each element, except the last, will be `0`-padded left to
|
|
|
|
|
32 bytes.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
```
|
|
|
|
|
struct Result {
|
|
|
|
|
// Status of the previous transaction
|
|
|
|
|
success: bool,
|
|
|
|
|
|
|
|
|
|
// Total gas used by the previous transaction
|
|
|
|
|
gas_used: u256,
|
|
|
|
|
|
|
|
|
|
// Cumulative gas used by previous transactions
|
|
|
|
|
cum_gas_used: u256,
|
|
|
|
|
|
|
|
|
|
// The size of the return value
|
|
|
|
|
return_size: u256,
|
|
|
|
|
|
|
|
|
|
// The return value of the previous transaction
|
|
|
|
|
return_value: [u8]
|
|
|
|
|
}
|
|
|
|
|
```
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 20:24:08 -06:00
|
|
|
|
### Intrinsic Cost
|
|
|
|
|
Let the intrinsic cost of the transaction package be defined as follows:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
fn intrinsic_gas(tx: TransactionPackage) u256 {
|
|
|
|
|
let data_cost = tx.children.map(|c| data_cost(&c.data)).sum();
|
|
|
|
|
17000 + 8000 * tx.children.len() + data_cost
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-22 12:38:04 -06:00
|
|
|
|
### Execution
|
|
|
|
|
Transaction packages should be executed as follows:
|
2020-10-11 11:22:32 -06:00
|
|
|
|
1. Deduct the cumulative cost from the outer signer's balance.
|
|
|
|
|
2. Execute the first child in the list.
|
|
|
|
|
3. Record all state changes, logs, and the receipt.
|
|
|
|
|
4. If there are no more transactions, stop.
|
|
|
|
|
5. Compute `Result` for the previously executed transaction.
|
|
|
|
|
6. Prepare `Result` to be available via return opcodes in the next
|
2020-06-22 12:38:04 -06:00
|
|
|
|
transaction's first frame
|
2020-10-11 11:22:32 -06:00
|
|
|
|
7. Execute the next transaction
|
|
|
|
|
8. Goto `3`
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Rationale
|
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
### Each `Child` has its own signature
|
|
|
|
|
For simplicity, the author has chosen to require each child transaction to
|
|
|
|
|
specify its own signature, even if the signer is the same as the package
|
|
|
|
|
signer. This choice is made to allow for maximum flexibility, with minimal
|
|
|
|
|
client changes. A future transaction type can be specified with only a single
|
|
|
|
|
signature, if such an optimization is desired.
|
|
|
|
|
|
|
|
|
|
### `Child` specifies `max_gas_price` instead of `gas_price`
|
|
|
|
|
Allowing child transactions to specify a range of acceptable gas prices is
|
|
|
|
|
strictly more versatile than a static price. It gives relayers more flexibility
|
|
|
|
|
in terms of building transaction bundles, and it makes it possible for relayers
|
|
|
|
|
to try and achieve the best price for the transaction sender. With a fixed
|
|
|
|
|
price, the relayer may require the user to sign multiple different
|
|
|
|
|
transactions, with varying prices. This can be avoided by specifying a max
|
|
|
|
|
price, and communicating out-of-band how the urgency of the transaction (e.g.
|
|
|
|
|
the relayer should package it with the max price immediately vs. slowly
|
|
|
|
|
increasing the gas price).
|
|
|
|
|
|
|
|
|
|
### `Child` is also typed
|
|
|
|
|
The type element serves a modest role in the transaction type, denoting whether
|
|
|
|
|
the transaction signer wishes to delegate control of the gas price and gas
|
|
|
|
|
limit to the outer signer. This is a useful UX improvement when interacting
|
|
|
|
|
with a trusted relayer, as once the user decides to make a transaction the
|
|
|
|
|
relayer can ensure it is included on chain by choosing the best gas price and
|
|
|
|
|
limit.
|
|
|
|
|
|
|
|
|
|
The type also simplifies upgradability to the child transactions. For example,
|
|
|
|
|
suppose [EIP-2803](./eip-2803.md) is implemented. The upper 4 bits of the type
|
|
|
|
|
can be used as a flag, alongside the specified types in this EIP.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
2020-10-11 11:22:32 -06:00
|
|
|
|
### The `extra` field isn't used
|
|
|
|
|
This field is included to better support future changes to the transaction
|
|
|
|
|
type. This would likely be used in conjunction with the `type` field. Avoiding
|
|
|
|
|
specialized serialization of RLP simplifies clients and downstream
|
|
|
|
|
infrastructure. The author believe the cost of 1 byte per transaction is
|
|
|
|
|
acceptable for smoother integration of future features.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Backwards Compatibility
|
|
|
|
|
Contracts which rely on `ORIGIN (0x32) == CALLER (0x33) && RETURNDATASIZE
|
|
|
|
|
(0x3D) == 0x00` will now always fail in transaction packages, unless they are
|
2020-10-11 11:22:32 -06:00
|
|
|
|
the first executed transaction. It’s unknown if any contracts conduct this
|
2020-06-22 12:38:04 -06:00
|
|
|
|
check.
|
|
|
|
|
|
|
|
|
|
## Test Cases
|
|
|
|
|
TBD
|
|
|
|
|
|
|
|
|
|
## Implementation
|
|
|
|
|
TBD
|
|
|
|
|
|
|
|
|
|
## Security Considerations
|
2020-10-11 11:22:32 -06:00
|
|
|
|
### Managing packages efficiently in the mempool
|
|
|
|
|
The introduction of a new transaction type brings along new concerns regarding
|
|
|
|
|
the mempool. Done naively, it could turn into a DDoS vector for clients. This
|
|
|
|
|
EIP has been written to reduce as much validation complexity as possible.
|
|
|
|
|
|
|
|
|
|
An existing invariant in the mempool that is desirable for new transactions to
|
|
|
|
|
maintain, is that transactions can be validated in constant time. This is also
|
|
|
|
|
possible for packaged transactions. There is an inherent 10Mb limit for RLPx
|
|
|
|
|
frames, so that would be the upper bound on transactions that could be included
|
|
|
|
|
in a package. On the other hand, clients can also just configure their own
|
|
|
|
|
bound locally (e.g. packages must be less than 1Mb). Validity can then be
|
|
|
|
|
determined by using the function above.
|
|
|
|
|
|
|
|
|
|
Once a package has been validated, it must continuously be monitored for nonce
|
|
|
|
|
invalidations within its package. One potential way to achieve this efficiently
|
|
|
|
|
is to modify the mempool to operate on thin pointers to the underlying
|
|
|
|
|
transaction. This will allow packages to ingest as many "single" transactions,
|
|
|
|
|
simplifying the facilities for monitoring changes. These "parts" of the package
|
|
|
|
|
can maintain a pointer to a structure with pointers to all the parts of the
|
|
|
|
|
package. This way, as soon as one part becomes invalid, it can request the
|
|
|
|
|
parent to invalidate all outstanding parts of the package.
|
2020-06-22 12:38:04 -06:00
|
|
|
|
|
|
|
|
|
## Copyright
|
|
|
|
|
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|