diff --git a/EIPS/eip-2733.md b/EIPS/eip-2733.md index cb4e25d8..57d6d0a7 100644 --- a/EIPS/eip-2733.md +++ b/EIPS/eip-2733.md @@ -48,7 +48,6 @@ 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`. @@ -56,7 +55,7 @@ Introduce a new [EIP-2718](./eip-2718.md) transaction type where `id = 2`. ``` struct TransactionPackage { chain_id: u256, - children: [Child], + children: [ChildPackage], nonce: u64, gas_price: u256, v: u256, @@ -72,7 +71,7 @@ struct TransactionPackage { `keccak256(rlp([2, chain_id, children, nonce, gas_price])` ##### Receipt -Each `Child` transaction will generate a `ChildReceipt` after execution. Each +Each `ChildTransaction` transaction will generate a `ChildReceipt` after execution. Each of these receipts will be aggregated into a `Receipt`. ``` @@ -89,21 +88,28 @@ struct ChildReceipt { ``` #### Child Transaction -Let `Child` be interpreted as follows. +Let `ChildPackage` be interpreted as follows. ``` -struct Child { +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], - max_gas_price: u256, - gas_limit: u256, - v: u256, - r: u256, - s: u256 + gas_limit: u256 } ``` @@ -114,8 +120,8 @@ signer. | 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])` | +| `0x00` | `keccak256(rlp([0, nonce, transactions, max_gas_price])` | +| `0x01` | `keccak256(rlp([1, nonce, transactions_without_gas_limit])` | ### Validity @@ -123,38 +129,36 @@ 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 + if ( + config.chain_id() != tx.chain_id || + tx.children.len() == 0 || + state.nonce(tx.from()) + 1 != tx.nonce + ) { + return false; } - if tx.children.len() == 0 { - false - } - - if state.nonce(tx.from()) + 1 != tx.nonce { - 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) { - false + return false; } for child in tx.children { - if state.nonce(child.from()) + 1 != child.nonce { - false + if ( + child.nonce != state.nonce(child.from()) + 1 || + child.value > state.balance(child.from()) || + child.max_gas_price < tx.gas_price + ) { + return false; } - if state.balance(child.from()) < child.value { - false - } - - if child.max_gas_price < tx.gas_price { - false - } - - if child.extra.len() != 0 { - false + for tx in child.txs { + if ( + tx.flags != 0 || + tx.extra.len() != 0 || + tx.gas_limit < intrinsic_gas(tx) + ) { + return false; + } } } @@ -164,10 +168,10 @@ fn is_valid(config: &Config, state: &State, tx: TransactionPackage) bool { ### Results -Subsequent transactions will be able to receive the result of the previous -transaction 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. +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 { @@ -193,34 +197,34 @@ 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 + 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. Execute the first child in the list. -3. Record all state changes, logs, and the receipt. -4. If there are no more transactions, stop. +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 -8. Goto `3` + 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 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. +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. -### `Child` specifies `max_gas_price` instead of `gas_price` -Allowing child transactions to specify a range of acceptable gas prices is +### `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 @@ -229,8 +233,10 @@ 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. -### `Child` is also typed +### `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 @@ -238,16 +244,13 @@ 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. - -### 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. +### 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