Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update EIP-6493: Use Variant[S] for type safety #7939

Merged
merged 2 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 HomesteadReceipt(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[Receipt]:
if value.status is not None:
return BasicReceipt

return HomesteadReceipt
```

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 HomesteadReceipt(
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
Loading