293 lines
10 KiB
Markdown
293 lines
10 KiB
Markdown
|
---
|
|||
|
eip: 2733
|
|||
|
title: Transaction Package
|
|||
|
author: Matt Garnett (@lightclient)
|
|||
|
discussions-to: https://ethereum-magicians.org/t/eip-transaction-package/4365
|
|||
|
status: Withdrawn
|
|||
|
type: Standards Track
|
|||
|
category: Core
|
|||
|
created: 2020-06-16
|
|||
|
requires: 2718
|
|||
|
withdrawal-reason: I have decided to pursue EIP-3074 as the preferred solution to transaction packages.
|
|||
|
---
|
|||
|
|
|||
|
## Simple Summary
|
|||
|
Creates a new transaction type which executes a package of one or more
|
|||
|
transactions, while passing status information to subsequent transactions.
|
|||
|
|
|||
|
## Abstract
|
|||
|
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.
|
|||
|
|
|||
|
## Motivation
|
|||
|
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.
|
|||
|
|
|||
|
## Specification
|
|||
|
Introduce a new [EIP-2718](./eip-2718.md) transaction type where `id = 2`.
|
|||
|
|
|||
|
#### Structure
|
|||
|
```
|
|||
|
struct TransactionPackage {
|
|||
|
chain_id: u256,
|
|||
|
children: [ChildPackage],
|
|||
|
nonce: u64,
|
|||
|
gas_price: u256,
|
|||
|
v: u256,
|
|||
|
r: u256,
|
|||
|
s: u256
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
##### Hash
|
|||
|
`keccak256(rlp([2, chain_id, children, nonce, gas_price, v, r, s])`
|
|||
|
|
|||
|
##### Signature Hash
|
|||
|
`keccak256(rlp([2, chain_id, children, nonce, gas_price])`
|
|||
|
|
|||
|
##### Receipt
|
|||
|
Each `ChildTransaction` 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,
|
|||
|
logs_bloom: [u8; 256],
|
|||
|
logs: [u8]
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
#### Child Transaction
|
|||
|
Let `ChildPackage` be interpreted as follows.
|
|||
|
|
|||
|
```
|
|||
|
struct ChildPackage {
|
|||
|
type: u8,
|
|||
|
nonce: u64,
|
|||
|
transactions: [ChildTransaction],
|
|||
|
max_gas_price: u256,
|
|||
|
v: u256,
|
|||
|
r: u256,
|
|||
|
s: u256
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
```
|
|||
|
struct ChildTransaction {
|
|||
|
flags: u8,
|
|||
|
to: Address,
|
|||
|
value: u256,
|
|||
|
data: [u8],
|
|||
|
extra: [u8],
|
|||
|
gas_limit: u256
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
##### 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.
|
|||
|
|
|||
|
| type | signature hash |
|
|||
|
|---|---|
|
|||
|
| `0x00` | `keccak256(rlp([0, nonce, transactions, max_gas_price])` |
|
|||
|
| `0x01` | `keccak256(rlp([1, nonce, transactions_without_gas_limit])` |
|
|||
|
|
|||
|
### 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 ||
|
|||
|
tx.children.len() == 0 ||
|
|||
|
state.nonce(tx.from()) + 1 != tx.nonce
|
|||
|
) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
let cum_limit = tx.children.map(|x| x.gas_limit).sum();
|
|||
|
if state.balance(tx.from()) < cum_limit * tx.gas_price + intrinsic_gas(tx) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
for child in tx.children {
|
|||
|
if (
|
|||
|
child.nonce != state.nonce(child.from()) + 1 ||
|
|||
|
child.value > state.balance(child.from()) ||
|
|||
|
child.max_gas_price < tx.gas_price
|
|||
|
) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
for tx in child.txs {
|
|||
|
if (
|
|||
|
tx.flags != 0 ||
|
|||
|
tx.extra.len() != 0 ||
|
|||
|
tx.gas_limit < intrinsic_gas(tx)
|
|||
|
) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
true
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Results
|
|||
|
|
|||
|
Subsequent `ChildTransaction`s will be able to receive the result of the
|
|||
|
previous `ChildTransaction` via `RETURNDATACOPY (0x3E)` in first frame of
|
|||
|
execution, before making any subcalls. Each element, except the last, will be
|
|||
|
`0`-padded left to 32 bytes.
|
|||
|
|
|||
|
```
|
|||
|
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]
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Intrinsic Cost
|
|||
|
Let the intrinsic cost of the transaction package be defined as follows:
|
|||
|
|
|||
|
```
|
|||
|
fn intrinsic_gas(tx: TransactionPackage) u256 {
|
|||
|
let data_gas = tx.children.map(|c| c.txs.map(|t| data_cost(&c.data)).sum()).sum();
|
|||
|
17000 + 8000 * tx.children.len() + data_gas
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Execution
|
|||
|
Transaction packages should be executed as follows:
|
|||
|
1. Deduct the cumulative cost from the outer signer's balance.
|
|||
|
2. Load the first child package, and execute the first child transaction.
|
|||
|
3. Record all state changes, logs, the receipt, and refund any unused gas.
|
|||
|
4. If there are no more child transactions, goto `8`.
|
|||
|
5. Compute `Result` for the previously executed transaction.
|
|||
|
6. Prepare `Result` to be available via return opcodes in the next
|
|||
|
transaction's first frame.
|
|||
|
7. Execute the next transaction, then goto `3`.
|
|||
|
8. Load the next child package, then goto `7`.
|
|||
|
|
|||
|
## Rationale
|
|||
|
|
|||
|
### Each `Child` has its own signature
|
|||
|
For simplicity, the author has chosen to require each child package 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.
|
|||
|
This transaction can still be used by a single user at the cost of only one
|
|||
|
additional signature recovery.
|
|||
|
|
|||
|
### `ChildPackage` specifies `max_gas_price` instead of `gas_price`
|
|||
|
Allowing child packages 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).
|
|||
|
A future transaction type can be specified with only a single
|
|||
|
signature, if such an optimization is desired.
|
|||
|
|
|||
|
### `ChildPackage` 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 `flags` and `extra` fields aren't used
|
|||
|
These fields are included to better support future changes to the transaction
|
|||
|
type. This would likely be used in conjunction with the `flags` and `type`
|
|||
|
fields. A benefit of explicitly defining them is that specialized serialization
|
|||
|
of RLP can be avoided, simplifing clients and downstream infrastructure. The
|
|||
|
author believe the cost of 2 bytes per transaction is acceptable for smoother
|
|||
|
integration of future features.
|
|||
|
|
|||
|
## Backwards Compatibility
|
|||
|
Contracts which rely on `ORIGIN (0x32) == CALLER (0x33) && RETURNDATASIZE
|
|||
|
(0x3D) == 0x00` will now always fail in transaction packages, unless they are
|
|||
|
the first executed transaction. It’s unknown if any contracts conduct this
|
|||
|
check.
|
|||
|
|
|||
|
## Test Cases
|
|||
|
TBD
|
|||
|
|
|||
|
## Implementation
|
|||
|
TBD
|
|||
|
|
|||
|
## Security Considerations
|
|||
|
### 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.
|
|||
|
|
|||
|
## Copyright
|
|||
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|