forked from DecentralizedClimateFoundation/DCIPs
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).
|