DCIPs/EIPS/eip-2733.md

293 lines
10 KiB
Markdown
Raw Normal View History

---
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. Its 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).