-
-
Published
Linked with GitHub
# Ethereum transactions overhaul
[toc]
There have been various proposals to extend the transaction system. At least the following were proposed:
- separate gas payer vs. eoa (EIP-2711)
- valid until (transaction expiry) (EIP-2711)
- simple batched transactions (EIP-2711)
- account access list (EIP-2930)
- 1559 gas market (EIP-1559)
- escalator fee market (EIP-2593)
- complex batched transactions with observability of tx outcome (EIP-2733)
- rich transactions (EIP-2803)
- account abstraction (EIP-2938)
- cancellation transactions
- separate init vs. runtime code for contracts
- evm bytecode metadata / versioning
## Existing proposals
### EIP-2711: New transction types
> ChildTransaction A nested transaction consisting of [to, value, data].
>
> SenderPayload Defined based on the TransactionSubtype as follows:
>
> 1. [1, ChildTransaction[], nonce, ChainId, ValidUntil, gasLimit, gasPrice]
> 2. [2, ChildTransaction[], nonce, ChainId, ValidUntil, gasLimit, gasPrice]
> 3. [3, ChildTransaction[], nonce, ChainId, ValidUntil, gasLimit]
> 4. [4, ChildTransaction[], nonce, ChainId, ValidUntil]
>
> SenderSignature [YParity, r, s] of secp256k1(keccak256(rlp([TransactionType, SenderPayload])))
>
> GasPayerPayload Defined based on the TransactionSubtype as follows:
>
> 1. []
> 2. []
> 3. [gasPrice]
> 4. [gasLimit, gasPrice]
>
> GasPayerSignature is [] for TransactionSubType 1 or [YParity, r, s] of secp256k1(keccak256(rlp([SenderPayload, SenderSignature, GasPayerPayload]))) for others.
>
>
> Final wrapping:
[...SenderPayload, ...SenderSignature, ...GasPayerPayload, ...GasPayerSignature]
### EIP-1559: Gas Market
```
@dataclass
class Transaction1559:
account_nonce: int = 0
gas_limit: int = 0
amount: int = 0
payload: bytes = bytes()
gas_premium: int = 0
fee_cap: int = 0
v: int = 0
r: int = 0
s: int = 0
Transaction = Union[TransactionLegacy, Transaction1559]
```
### EIP-2930: Access list
> We introduce a new EIP-2718 transaction type, with the format rlp([3, [nonce, gasPrice, gasLimit, to, value, data, access_list, senderV, senderR, senderS]]).
>
> The access_list specifies a list of addresses and storage keys; these addresses and storage keys are added into the accessed_addresses and accessed_storage_keys global sets (introduced in EIP-2929). A gas cost is charged, though at a discount relative to the cost of accessing outside the list.
### EIP-2718: Account Abstraction
> A new EIP-2718 transaction with type `AA_TX_TYPE` is introduced. Transactions of this type are referred to as "AA transactions". Their payload should be interpreted as `rlp([nonce, target, data])`.
### Cancellation
It does not need anything, but a nonce and a signature.
### EIP-2733: Trasanction Packages
> After FORK_BLOCK, a new EIP-2718 transaction type N will be interpreted as follows:
>
> rlp([N, rlp([nonce, v, r, s, [inner_tx_0, ..., inner_tx_n]])
>
> where inner_tx_n is defined as:
>
> [chain_id, to, value, data, gas_limit, gas_price]
### EIP-2803: Rich transactions
> A new reserved address is specified at x, in the range used for precompiles. When a transaction is sent to this address from an externally owned account, the payload of the transaction is treated as EVM bytecode, and executed with the signer of the transaction as the current account.
### "Richer transactions"
[EIP-2803](https://eips.ethereum.org/EIPS/eip-2803) specifies a special account which receiving a transaction would execute the code passed.
Instead of that we have the ability to a transaction as "execute code". Furthermore could consider to have separate `code` and `data` fields in the transaction, allowing for `calldatacopy` to work properly and the possibility for reusable (and potentially cacheable?!) pieces of code.
The split code/data fields could be reused for contract creation, essentially removing the need for concatenating constructor arguments to data.
## Remarks
The above version of rich transactions removes the need for batched transactions and transaction packages. Therefore we omit that functionality from below.
## New transaction encoding
Following the terminology of EIP-2711 redefine `SenderPayload` as:
> [kind, flags, fields..]
(Would `kind << 16 | flags` be better?)
Kind:
- Cancellation
- Account call
- Execute code (~richtx)
- Contract creation (~create)
- Hash-based contract creation (~create2)
Flags:
- AccessList present
- Expiry present
- GasLimit present
- GasPrice present
- GasBribeAndCap present
Fields (strict ordering):
- chainid
- nonce
- target
- value
- code
- data
- access_list
- expiry
- gas_limit
- gas_price
- max_miner_bribe_per_gas
- fee_cap_per_gas
Cancellation:
- `[kind=0, flags=gasPrice, nonce, gasPrice]`
Account call:
Execute code:
Contract creation:
Hash-based contract creation:
## Rich transaction FTW
It would be possible to drop special transaction types and rely on always executing code.
### Specification
The external transaction now has the following fields:
- `code` (EVM bytecode)
- `data` (optional calldata)
- `nonce`
- `gasLimit`
- `gasPrice`
- signature
Note: there is no `value` field.
The bytecode under `code` is treated as EVM bytecode, and executed with the signer of the transaction as the current account, with the following clarifications:
- The `ADDRESS` opcode returns the address of the EOA that signed the transaction.
- The `BALANCE` opcode returns the balance of the EOA that signed the transaction.
- Any `CALL*` operations that send value take their value from the EOA that signed the transaction.
- `CALL*` will set the CALLER to the EOA.
- `DELEGATECALL` preserves the EOA as the owning account.
- The `CALLER` and `ORIGIN` opcodes both return the address of the EOA that signed the transaction.
- There no code associated with the EOA address. `EXTCODE*` opcodes work as intended.
- The `CODE*` opcodes return the currently executing code (`tx.code` field), as expected.
- The `CALLDATA*` opcodes operate on the transaction payload (`tx.data` field), as expected.
- `SLOAD` and `SSTORE` operate on the storage of the EOA. As a result, an EOA can have data in storage, that persists between transactions.
- The `SELFDESTRUCT` opcode transfers the balance of the EOA to the specified address, and at the end of the transaction zeroes out the account’s state storage, but does not zero the account’s nonce. No refund is applied for the `SELFDESTRUCT` at the end of the transaction.
- All other opcodes behave as expected for a call to a contract address.
**Question:** What happens to logs?
## Transactions using only rich transactions
**Questions:** What to do with any return data? Should it be passed through? Should these snippets emit logs? What is the best way for users' to inspect their transaction outcomes?
### Ether transfer
Payload is as follows: target:uint256 value:uint256
```typescript
{
let target := calldataload(0)
let value := calldataload(1)
// 0 ensures it is only sending the standard "stipend"
pop(call(0, target, value, 0, 0, 0, 0))
}
```
> 600035600135600060006000600084866000f1505050
### Account call
Payload is as follows: target:uint256 value:uint256 payload...
```typescript
{
let target := calldataload(0)
let value := calldataload(1)
let payload_length := sub(calldatasize(), 64)
calldatacopy(0, 64, payload_length)
// There is no need to limit gas, but could pass in gas_limit
pop(call(gas(), target, value, 0, payload_length, 0, 0))
}
```
> 00035600135604036038060406000376000600082600085875af150505050
### Contract creation
Payload is as follows: value:uint256 initcode...
```typescript
{
let value := calldataload(0)
let payload_length := sub(calldatasize(), 32)
calldatacopy(0, 32, payload_length)
pop(create(gas(), 0, payload_length))
}
```
> 600035602036038060206000378060005af0505050
### Hash-based contract creation
Payload is as follows: value:uint256 salt:uint256 initcode...
```typescript
{
let value := calldataload(0)
let salt := calldataload(1)
let payload_length := sub(calldatasize(), 64)
calldatacopy(0, 64, payload_length)
pop(create2(gas(), 0, payload_length, salt))
}
```
> 60003560013560403603806040600037818160005af550505050