Skip to content

Commit

Permalink
Update EIP-6493: Use Variant[S] for type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
etan-status committed Oct 29, 2023
1 parent d72afda commit aa86972
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 143 deletions.
187 changes: 161 additions & 26 deletions EIPS/eip-6493.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,36 +121,145 @@ class TransactionSignature(StableContainer[MAX_TRANSACTION_SIGNATURE_FIELDS]):
class SignedTransaction(Container):
payload: TransactionPayload
signature: TransactionSignature
```

Valid transaction types can be defined using [EIP-7495](./eip-7495.md) `Variant`.

```python
class ReplayableTransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]

class ReplayableSignedTransaction(SignedTransaction):
payload: ReplayableTransactionPayload
signature: TransactionSignature

class LegacyTransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType

class LegacySignedTransaction(SignedTransaction):
payload: LegacyTransactionPayload
signature: TransactionSignature

class Eip2930TransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]

class Eip2930SignedTransaction(SignedTransaction):
payload: Eip2930TransactionPayload
signature: TransactionSignature

class Eip1559TransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fee_per_gas: uint256

class Eip1559SignedTransaction(SignedTransaction):
payload: Eip1559TransactionPayload
signature: TransactionSignature

class Eip4844TransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: ExecutionAddress
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fee_per_gas: uint256
max_fee_per_blob_gas: uint256
blob_versioned_hashes: List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]

class Eip4844SignedTransaction(SignedTransaction):
payload: Eip4844TransactionPayload
signature: TransactionSignature

class BasicTransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: Optional[ExecutionAddress]
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fee_per_gas: uint256

class BasicSignedTransaction(SignedTransaction):
payload: BasicTransactionPayload
signature: TransactionSignature

class BlobTransactionPayload(Variant[TransactionPayload]):
nonce: uint64
max_fee_per_gas: uint256
gas: uint64
to: ExecutionAddress
value: uint256
input_: ByteList[MAX_CALLDATA_SIZE]
type_: TransactionType
access_list: List[AccessTuple, MAX_ACCESS_LIST_SIZE]
max_priority_fee_per_gas: uint256
max_fee_per_blob_gas: uint256
blob_versioned_hashes: List[VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK]

def check_transaction_supported(tx: SignedTransaction):
if tx.payload.max_fee_per_blob_gas is not None:
assert tx.payload.blob_versioned_hashes is not None
assert tx.payload.max_priority_fee_per_gas is not None
assert tx.payload.to is not None
else:
assert tx.payload.blob_versioned_hashes is None

if tx.payload.max_priority_fee_per_gas is not None:
assert tx.payload.access_list is not None

if tx.payload.type_ != TRANSACTION_TYPE_SSZ:
if tx.payload.max_fee_per_blob_gas is not None:
assert tx.payload.type_ == TRANSACTION_TYPE_EIP4844
elif tx.payload.max_priority_fee_per_gas is not None:
assert tx.payload.type_ == TRANSACTION_TYPE_EIP1559
elif tx.payload.access_list is not None:
assert tx.payload.type_ == TRANSACTION_TYPE_EIP2930
else:
assert tx.payload.type_ == TRANSACTION_TYPE_LEGACY or tx.payload.type_ is None
class BlobSignedTransaction(SignedTransaction):
payload: BlobTransactionPayload
signature: TransactionSignature

class AnySignedTransaction(OneOf[SignedTransaction]):
@classmethod
def select_variant(cls, value: SignedTransaction) -> Type[SignedTransaction]:
if value.payload.type_ == TRANSACTION_TYPE_SSZ:
if value.payload.blob_versioned_hashes is not None:
return BlobSignedTransaction
return BasicSignedTransaction

if value.payload.type_ == TRANSACTION_TYPE_EIP4844:
return Eip4844SignedTransaction

if value.payload.type_ == TRANSACTION_TYPE_EIP1559:
return Eip1559SignedTransaction

if value.payload.type_ == TRANSACTION_TYPE_EIP2930:
return Eip2930SignedTransaction

if value.payload.type_ == TRANSACTION_TYPE_LEGACY:
return LegacySignedTransaction

assert value.payload.type_ is None
return ReplayableSignedTransaction
```

Future specifications MAY:

- Add fields to the end of `TransactionPayload` and `TransactionSignature`
- Convert existing fields to `Optional`
- Relax the validation rules in `check_transaction_supported`
- Define new `Variant` types and update `select_variant` logic

Such changes [do not affect](./eip-7495.md) how existing transactions serialize, merkleize, or validate.
Such changes [do not affect](./eip-7495.md) how existing transactions serialize or merkleize.

![Transaction merkleization](../assets/eip-6493/transaction.png)

Expand Down Expand Up @@ -226,9 +335,8 @@ def ecdsa_recover_from_address(signature: ByteVector[ECDSA_SIGNATURE_SIZE],
uncompressed = public_key.serialize(compressed=False)
return ExecutionAddress(keccak(uncompressed[1:])[12:])

def validate_transaction(tx: SignedTransaction,
def validate_transaction(tx: AnySignedTransaction,
chain_id: ChainId):
check_transaction_supported(tx)
ecdsa_validate_signature(tx.signature.ecdsa_signature)
assert tx.signature.from_ == ecdsa_recover_from_address(
tx.signature.ecdsa_signature,
Expand Down Expand Up @@ -294,12 +402,39 @@ class Receipt(StableContainer[MAX_RECEIPT_FIELDS]):
status: Optional[boolean]
```

Valid receipt types can be defined using [EIP-7495](./eip-7495.md) `Variant`.

```python
class LegacyReceipt(Variant[Receipt]):
root: Hash32
gas_used: uint64
contract_address: Optional[ExecutionAddress]
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
logs: List[Log, MAX_LOGS_PER_RECEIPT]

class BasicReceipt(Variant[Receipt]):
gas_used: uint64
contract_address: Optional[ExecutionAddress]
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
logs: List[Log, MAX_LOGS_PER_RECEIPT]
status: boolean

class AnyReceipt(OneOf[Receipt]):
@classmethod
def select_variant(cls, value: Receipt) -> Type[Variant[Receipt]]:
if value.status is not None:
return BasicReceipt

return LegacyReceipt
```

Future specifications MAY:

- Add fields to the end of `Receipt`
- Convert existing fields to `Optional`
- Define new `Variant` types and update `select_variant` logic

Such changes [do not affect](./eip-7495.md) how existing receipts serialize, merkleize, or validate.
Such changes [do not affect](./eip-7495.md) how existing receipts serialize or merkleize.

![Receipt merkleization](../assets/eip-6493/receipt.png)

Expand Down Expand Up @@ -360,7 +495,7 @@ Mixing the chain ID into the `TransactionDomainData` further allows dropping the

### What about EIP-2718 transaction types?

All SSZ transactions (including future ones) share the single [EIP-2718](./eip-2718.md) transaction type `TRANSACTION_TYPE_SSZ`. Future features can introduce new optional fields as well as new allowed combination of optional fields, as determined by `check_transaction_supported`.
All SSZ transactions (including future ones) share the single [EIP-2718](./eip-2718.md) transaction type `TRANSACTION_TYPE_SSZ`. Future features can introduce new optional fields as well as new allowed combination of optional fields, as determined by `select_variant` in `AnySignedTransaction`.

This also reduces combinatorial explosion; for example, the `access_list` property could be made optional for all SSZ transactions without having to double the number of defined transaction types.

Expand Down
73 changes: 50 additions & 23 deletions assets/eip-6493/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from ssz_types import *

def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
chain_id: ChainId) -> SignedTransaction:
chain_id: ChainId) -> AnySignedTransaction:
type_ = pre_bytes[0]

if type_ == 0x03: # EIP-4844
pre = decode(pre_bytes[1:], Eip4844SignedTransaction)
pre = decode(pre_bytes[1:], Eip4844SignedRlpTransaction)
assert pre.chain_id == chain_id

assert pre.signature_y_parity in (0, 1)
Expand All @@ -19,8 +19,8 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip4844_sig_hash(pre))

return SignedTransaction(
payload=TransactionPayload(
return Eip4844SignedTransaction(
payload=Eip4844TransactionPayload(
nonce=pre.nonce,
max_fee_per_gas=pre.max_fee_per_gas,
gas=pre.gas_limit,
Expand All @@ -43,7 +43,7 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)

if type_ == 0x02: # EIP-1559
pre = decode(pre_bytes[1:], Eip1559SignedTransaction)
pre = decode(pre_bytes[1:], Eip1559SignedRlpTransaction)
assert pre.chain_id == chain_id

assert pre.signature_y_parity in (0, 1)
Expand All @@ -54,8 +54,8 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip1559_sig_hash(pre))

return SignedTransaction(
payload=TransactionPayload(
return Eip1559SignedTransaction(
payload=Eip1559TransactionPayload(
nonce=pre.nonce,
max_fee_per_gas=pre.max_fee_per_gas,
gas=pre.gas_limit,
Expand All @@ -76,7 +76,7 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)

if type_ == 0x01: # EIP-2930
pre = decode(pre_bytes[1:], Eip2930SignedTransaction)
pre = decode(pre_bytes[1:], Eip2930SignedRlpTransaction)
assert pre.chainId == chain_id

assert pre.signatureYParity in (0, 1)
Expand All @@ -87,8 +87,8 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_eip2930_sig_hash(pre))

return SignedTransaction(
payload=TransactionPayload(
return Eip2930SignedTransaction(
payload=Eip2930TransactionPayload(
nonce=pre.nonce,
max_fee_per_gas=pre.gasPrice,
gas=pre.gasLimit,
Expand All @@ -108,7 +108,7 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)

if 0xc0 <= type_ <= 0xfe: # Legacy
pre = decode(pre_bytes, LegacySignedTransaction)
pre = decode(pre_bytes, LegacySignedRlpTransaction)

if pre.v not in (27, 28): # EIP-155
assert pre.v in (2 * chain_id + 35, 2 * chain_id + 36)
Expand All @@ -119,15 +119,31 @@ def upgrade_rlp_transaction_to_ssz(pre_bytes: bytes,
)
from_ = ecdsa_recover_from_address(ecdsa_signature, compute_legacy_sig_hash(pre))

return SignedTransaction(
payload=TransactionPayload(
if (pre.v not in (27, 28)):
return LegacySignedTransaction(
payload=LegacyTransactionPayload(
nonce=pre.nonce,
max_fee_per_gas=pre.gasprice,
gas=pre.startgas,
to=ExecutionAddress(pre.to) if len(pre.to) > 0 else None,
value=pre.value,
input_=pre.data,
type_=TRANSACTION_TYPE_LEGACY,
),
signature=TransactionSignature(
from_=from_,
ecdsa_signature=ecdsa_signature,
),
)

return ReplayableSignedTransaction(
payload=ReplayableTransactionPayload(
nonce=pre.nonce,
max_fee_per_gas=pre.gasprice,
gas=pre.startgas,
to=ExecutionAddress(pre.to) if len(pre.to) > 0 else None,
value=pre.value,
input_=pre.data,
type_=TRANSACTION_TYPE_LEGACY if (pre.v not in (27, 28)) else None,
),
signature=TransactionSignature(
from_=from_,
Expand All @@ -152,7 +168,7 @@ def compute_contract_address(from_: ExecutionAddress,

def upgrade_rlp_receipt_to_ssz(pre_bytes: bytes,
prev_cumulative_gas_used: uint64,
transaction: SignedTransaction) -> Receipt:
transaction: AnySignedTransaction) -> AnyReceipt:
type_ = pre_bytes[0]

if type_ in (0x03, 0x02, 0x01): # EIP-4844, EIP-1559, EIP-2930
Expand All @@ -162,14 +178,26 @@ def upgrade_rlp_receipt_to_ssz(pre_bytes: bytes,
else:
assert False

if len(pre.post_state_or_status) == 32:
root = pre.post_state_or_status
status = None
else:
root = None
if len(pre.post_state_or_status) != 32:
status = len(pre.post_state_or_status) > 0 and pre.post_state_or_status[0] != 0

return Receipt(
return BasicReceipt(
gas_used=pre.cumulative_gas_used - prev_cumulative_gas_used,
contract_address=compute_contract_address(
transaction.signature.from_,
transaction.payload.nonce,
) if transaction.payload.to is None else None,
logs_bloom=pre.logs_bloom,
logs=[Log(
address=log[0],
topics=log[1],
data=log[2],
) for log in pre.logs],
status=status,
)

root = pre.post_state_or_status
return LegacyReceipt(
root=root,
gas_used=pre.cumulative_gas_used - prev_cumulative_gas_used,
contract_address=compute_contract_address(
Expand All @@ -182,12 +210,11 @@ def upgrade_rlp_receipt_to_ssz(pre_bytes: bytes,
topics=log[1],
data=log[2],
) for log in pre.logs],
status=status,
)

def upgrade_rlp_receipts_to_ssz(pre_bytes_list: PyList[bytes],
chain_id: ChainId,
transactions: PyList[SignedTransaction]) -> PyList[Receipt]:
transactions: PyList[AnySignedTransaction]) -> PyList[AnyReceipt]:
receipts = []
cumulative_gas_used = 0
for i, pre_bytes in enumerate(pre_bytes_list):
Expand Down
Loading

0 comments on commit aa86972

Please sign in to comment.