Skip to content

Commit cfd65cb

Browse files
Update EIP-6404 with basic EIP-7932 support
1 parent bbf933a commit cfd65cb

File tree

7 files changed

+223
-77
lines changed

7 files changed

+223
-77
lines changed

EIPS/eip-6404.md

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ created: 2023-01-30
1111
requires: 155, 1559, 2718, 2930, 4844, 7495, 7702, 7916, 7932, 8016
1212
---
1313

14-
<!-- TODO -->
15-
<!-- Update ../assets/eip-6404 -->
16-
17-
1814
## Abstract
1915

2016
This EIP defines a migration process of [EIP-2718](./eip-2718.md) Recursive-Length Prefix (RLP) transactions to [Simple Serialize (SSZ)](https://github.com/ethereum/consensus-specs/blob/b5c3b619887c7850a8c1d3540b471092be73ad84/ssz/simple-serialize.md).
@@ -66,28 +62,45 @@ Transaction signatures are represented by their native, opaque representation, p
6662
| `ExecutionSignature` | `ProgressiveByteList` |
6763
| `ExecutionSignatureAlgorithm` | `uint8` |
6864

65+
#### Secp256k1
66+
67+
| Name | Value | Description |
68+
| - | - | - |
69+
| `SECP256K1_ALGORITHM` | `uint8(0xFF)` | Defined from [EIP-7932](./eip-7932.md) |
70+
71+
#### Verification
72+
73+
The `algorithm_registry` is defined from [EIP-7932](./eip-7932.md) along with the `calculate_penalty` and `pubkey_to_address` functions.
6974

70-
The `algorithm_registry`, `calculate_penalty` and `pubkey_to_address` objects are imported from [EIP-7932](./eip-7932.md):
7175

7276
```python
73-
algorithm_registry: Dict[uint8, AlgorithmEntry]
74-
pubkey_to_address: Callable[[Bytes, uint8], ExecutionAddress]
77+
78+
def get_signature_gas_cost(
79+
signature: ExecutionSignature,
80+
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None
81+
) -> uint:
82+
assert len(signature) > 0
83+
84+
if expected_algorithm is not None:
85+
assert signature[0] == expected_algorithm
86+
87+
return calculate_penalty(signature)
88+
7589

7690
def validate_execution_signature(
7791
signature: ExecutionSignature,
78-
signing_hash: Hash32,
92+
signature_hash: Hash32,
93+
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None,
7994
) -> ExecutionAddress:
8095
assert len(signature) > 0
81-
algorithm = signature[0]
82-
assert algorithm in algorithm_registry
8396

84-
public_key = algorithm_registry[algorithm].verify(signature, signing_hash)
85-
return pubkey_to_address(public_key, algorithm)
86-
```
97+
if expected_algorithm is not None:
98+
assert signature[0] == expected_algorithm
8799

88-
The gas calculation section in [EIP-7932](./eip-7932.md) does apply to verification of transaction signatures.
100+
public_key = algorithm_registry[signature[0]].verify(signature, signature_hash)
101+
return pubkey_to_address(public_key, signature[0])
89102

90-
ECDSA signatures are handled via the `secp256k1` (0xFF) algorithm of [EIP-7932](./eip-7932.md). All restrictions applied to algorithmic transaction by the `secp256k1` algorithm do not apply to any object specified in this document.
103+
```
91104

92105
### Gas fees
93106

@@ -353,10 +366,26 @@ class RlpTxType(IntEnum):
353366
SET_CODE = 0x04
354367
SET_CODE_MAGIC = 0x05
355368

369+
def calculate_base_gas_usage(tx: Transaction) -> uint:
370+
tx_data = tx.payload.data()
371+
gas_cost = TX_BASE_COST # FIXME: Should this be defined from another EIP?
372+
373+
if hasattr(tx_data, "authorization_list"):
374+
for auth in tx_data.authorization_list:
375+
gas_cost += get_signature_gas_cost(auth.signature, expected_algorithm=expected_signature_algorithm)
376+
377+
gas_cost += get_signature_gas_cost(tx.signature, expected_algorithm=expected_signature_algorithm)
378+
379+
return gas_cost
380+
356381
def validate_transaction(tx: Transaction):
357382
tx_data = tx.payload.data()
358383

384+
expected_signature_algorithm = None
385+
assert tx_data.gas >= calculate_base_gas_usage(tx)
386+
359387
if hasattr(tx_data, "type_"):
388+
expected_signature_algorithm = SECP256K1_ALGORITHM
360389
match tx_data.type_:
361390
case RlpTxType.LEGACY:
362391
assert isinstance(tx_data, RlpLegacyTransactionPayload)
@@ -380,11 +409,10 @@ def validate_transaction(tx: Transaction):
380409
if hasattr(auth_data, "chain_id"):
381410
assert auth_data.chain_id != 0
382411

383-
# TODO: figure out how to get the auth_hash variable
384-
validate_execution_signature(auth.signature, auth_hash)
412+
validate_execution_signature(auth.signature, compute_auth_hash(auth), expected_algorithm=expected_signature_algorithm)
413+
414+
validate_execution_signature(tx.signature, compute_sig_hash(tx), expected_algorithm=expected_signature_algorithm)
385415

386-
# TODO: figure out how to get the signature_hash variable
387-
validate_execution_signature(tx.signature, signature_hash)
388416
```
389417

390418
### Execution block header changes
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from remerkleable.basic import uint8, uint256, uint
2+
from remerkleable.byte_arrays import ByteVector
3+
4+
from eth_hash.auto import keccak
5+
from eth_typing import Hash32
6+
7+
8+
from .registry import algorithm_registry
9+
10+
11+
class ExecutionAddress(ByteVector[20]):
12+
pass
13+
14+
15+
def pubkey_to_address(public_key: bytes, algorithm_id: uint8) -> ExecutionAddress:
16+
if algorithm_id == 0xFF: # Compatibility shim to ensure backwards compatibility
17+
return ExecutionAddress(keccak(public_key[1:])[12:])
18+
19+
# || is binary concatenation
20+
return ExecutionAddress(keccak(bytes(algorithm_id) + public_key)[12:])
21+
22+
23+
def calculate_penalty(signature_info: bytes) -> uint:
24+
GAS_PER_ADDITIONAL_VERIFICATION_BYTE = 16
25+
SECP256K1_SIGNATURE_SIZE = 65
26+
27+
assert len(signature_info) > 0
28+
assert uint8(signature_info[0]) in algorithm_registry
29+
30+
gas_penalty_base = (
31+
max(len(signature_info) - (SECP256K1_SIGNATURE_SIZE + 1), 0)
32+
* GAS_PER_ADDITIONAL_VERIFICATION_BYTE
33+
)
34+
total_gas_penalty = (
35+
gas_penalty_base + algorithm_registry[uint8(signature_info[0])].GAS_PENALTY
36+
)
37+
38+
return uint256(total_gas_penalty)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from typing import Callable, Dict
2+
from eth_typing import Hash32
3+
from remerkleable.byte_arrays import ByteVector
4+
from remerkleable.basic import uint8, uint256, uint
5+
6+
from secp256k1 import PublicKey, ECDSA
7+
8+
9+
# Registry setup
10+
11+
12+
class AlgorithmEntry:
13+
ALG_TYPE: uint8
14+
GAS_PENALTY: uint
15+
verify: Callable[[bytes, Hash32], bytes]
16+
17+
18+
algorithm_registry: Dict[uint8, AlgorithmEntry] = {}
19+
20+
# Secp256k1
21+
22+
SECP256K1_SIGNATURE_SIZE = 65
23+
24+
25+
def secp256k1_unpack(
26+
signature: ByteVector[SECP256K1_SIGNATURE_SIZE],
27+
) -> tuple[uint256, uint256, uint8]:
28+
r = uint256.from_bytes(signature[0:32], "big")
29+
s = uint256.from_bytes(signature[32:64], "big")
30+
y_parity = signature[64]
31+
return (r, s, uint8(y_parity))
32+
33+
34+
def secp256k1_validate(signature: ByteVector[SECP256K1_SIGNATURE_SIZE]):
35+
SECP256K1N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
36+
r, s, y_parity = secp256k1_unpack(signature)
37+
assert 0 < r < SECP256K1N
38+
assert 0 < s <= SECP256K1N // 2
39+
assert y_parity in (0, 1)
40+
41+
42+
class Secp256k1(AlgorithmEntry):
43+
ALG_TYPE = uint8(0xFF)
44+
GAS_PENALTY = uint256(0)
45+
46+
def verify(signature_info: bytes, payload_hash: Hash32) -> bytes:
47+
assert len(signature_info) == (SECP256K1_SIGNATURE_SIZE + 1)
48+
secp256k1_validate(signature_info[1:])
49+
50+
ecdsa = ECDSA()
51+
recover_sig = ecdsa.ecdsa_recoverable_deserialize(
52+
signature_info[1:65], signature_info[65]
53+
)
54+
public_key = PublicKey(ecdsa.ecdsa_recover(payload_hash, recover_sig, raw=True))
55+
uncompressed = public_key.serialize(compressed=False)
56+
return uncompressed
57+
58+
59+
algorithm_registry[Secp256k1.ALG_TYPE] = Secp256k1

assets/eip-6404/convert.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from rlp import decode
22
from rlp_types import *
33
from ssz_types import *
4+
from ssz_helpers import *
45

56
def upgrade_rlp_transaction_to_ssz(tx_bytes: bytes):
67
type_ = tx_bytes[0]

assets/eip-6404/convert_tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class Test:
1313
from_: ExecutionAddress
1414
authorities: list[ExecutionAddress]
1515

16+
# FIXME: Make the tests work. i.e. change the algorithm_id from `0x01` -> `0xFF`
17+
# Also the tx with a gas value of `5`, isn't that inherently invalid?
1618
tests = [
1719
Test(
1820
tx_payload_selector=0x01,

assets/eip-6404/ssz_helpers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from tx_hashes import compute_auth_hash, compute_sig_hash
2+
from ssz_types import *
3+
4+
def calculate_base_gas_usage(tx: Transaction) -> uint:
5+
tx_data = tx.payload.data()
6+
7+
TX_BASE_COST = 21000 # FIXME
8+
gas_cost = TX_BASE_COST
9+
10+
if hasattr(tx_data, "authorization_list"):
11+
for auth in tx_data.authorization_list:
12+
gas_cost += get_signature_gas_cost(auth.signature)
13+
14+
gas_cost += get_signature_gas_cost(tx.signature)
15+
16+
return uint256(gas_cost)
17+
18+
def validate_transaction(tx: Transaction):
19+
tx_data = tx.payload.data()
20+
21+
expected_signature_algorithm = None
22+
assert tx_data.gas >= calculate_base_gas_usage(tx)
23+
24+
if hasattr(tx_data, "type_"):
25+
expected_signature_algorithm = SECP256K1_ALGORITHM
26+
match tx_data.type_:
27+
case RlpTxType.LEGACY:
28+
assert isinstance(tx_data, RlpLegacyTransactionPayload)
29+
case RlpTxType.ACCESS_LIST:
30+
assert isinstance(tx_data, RlpAccessListTransactionPayload)
31+
case RlpTxType.FEE_MARKET:
32+
assert isinstance(tx_data, RlpFeeMarketTransactionPayload)
33+
case RlpTxType.BLOB:
34+
assert isinstance(tx_data, RlpBlobTransactionPayload)
35+
case RlpTxType.SET_CODE:
36+
assert isinstance(tx_data, RlpSetCodeTransactionPayload)
37+
case _:
38+
assert False
39+
40+
if hasattr(tx_data, "authorization_list"):
41+
for auth in tx_data.authorization_list:
42+
auth_data = auth.payload.data()
43+
44+
if hasattr(auth_data, "magic"):
45+
assert auth_data.magic == RlpTxType.SET_CODE_MAGIC
46+
if hasattr(auth_data, "chain_id"):
47+
assert auth_data.chain_id != 0
48+
49+
validate_execution_signature(auth.signature, compute_auth_hash(auth), expected_algorithm=expected_signature_algorithm)
50+
51+
validate_execution_signature(tx.signature, compute_sig_hash(tx), expected_algorithm=expected_signature_algorithm)

assets/eip-6404/ssz_types.py

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
from typing import Callable, Dict, Optional, Type
2-
from dataclasses import dataclass
1+
from typing import Optional
32
from enum import IntEnum
43
from eth_hash.auto import keccak
5-
from remerkleable.basic import uint8, uint64, uint256
4+
from remerkleable.basic import uint8, uint64, uint256, uint
65
from remerkleable.byte_arrays import ByteVector, Bytes32
76
from remerkleable.complex import Container
87
from remerkleable.progressive import CompatibleUnion, ProgressiveByteList, ProgressiveContainer, ProgressiveList
98
from secp256k1 import ECDSA, PublicKey
109

10+
from algorithm_registry.helpers import pubkey_to_address, calculate_penalty
11+
from algorithm_registry.registry import algorithm_registry
12+
13+
1114
class Hash32(Bytes32):
1215
pass
1316

@@ -23,29 +26,34 @@ class ExecutionSignature(ProgressiveByteList):
2326
class ExecutionSignatureAlgorithm(uint8):
2427
pass
2528

26-
@dataclass
27-
class ExecutionSignatureAttributes(object):
28-
validate: Callable[[ExecutionSignature], None]
29-
recover_signer: Callable[[ExecutionSignature, Hash32], ExecutionAddress]
3029

31-
execution_signature_registry: Dict[ExecutionSignatureAlgorithm, ExecutionSignatureAttributes] = {}
30+
def get_signature_gas_cost(
31+
signature: ExecutionSignature,
32+
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None
33+
) -> uint:
34+
assert len(signature) > 0
35+
36+
if expected_algorithm is not None:
37+
assert signature[0] == expected_algorithm
38+
39+
return calculate_penalty(signature)
40+
3241

3342
def validate_execution_signature(
3443
signature: ExecutionSignature,
44+
signature_hash: Hash32,
3545
expected_algorithm: Optional[ExecutionSignatureAlgorithm]=None,
36-
):
46+
) -> ExecutionAddress:
3747
assert len(signature) > 0
38-
algorithm = signature[0]
48+
3949
if expected_algorithm is not None:
40-
assert algorithm == expected_algorithm
41-
assert algorithm in execution_signature_registry
42-
execution_signature_registry[algorithm].validate(signature)
50+
assert signature[0] == expected_algorithm
4351

44-
def recover_execution_signer(signature: ExecutionSignature, sig_hash: Hash32) -> ExecutionAddress:
45-
algorithm = signature[0]
46-
return execution_signature_registry[algorithm].recover_signer(signature, sig_hash)
52+
public_key = algorithm_registry[signature[0]].verify(signature, signature_hash)
53+
return pubkey_to_address(public_key, signature[0])
4754

48-
SECP256K1_ALGORITHM = ExecutionSignatureAlgorithm(0x01)
55+
56+
SECP256K1_ALGORITHM = ExecutionSignatureAlgorithm(0xFF)
4957
SECP256K1_SIGNATURE_SIZE = 1 + 32 + 32 + 1
5058

5159
def secp256k1_pack(r: uint256, s: uint256, y_parity: uint8) -> ExecutionSignature:
@@ -62,25 +70,6 @@ def secp256k1_unpack(signature: ExecutionSignature) -> tuple[uint256, uint256, u
6270
y_parity = signature[65]
6371
return (r, s, y_parity)
6472

65-
def secp256k1_validate(signature: ExecutionSignature):
66-
r, s, y_parity = secp256k1_unpack(signature)
67-
SECP256K1N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
68-
assert 0 < r < SECP256K1N
69-
assert 0 < s <= SECP256K1N // 2
70-
assert y_parity in (0, 1)
71-
72-
def secp256k1_recover_signer(signature: ExecutionSignature, sig_hash: Hash32) -> ExecutionAddress:
73-
ecdsa = ECDSA()
74-
recover_sig = ecdsa.ecdsa_recoverable_deserialize(signature[1:65], signature[65])
75-
public_key = PublicKey(ecdsa.ecdsa_recover(sig_hash, recover_sig, raw=True))
76-
uncompressed = public_key.serialize(compressed=False)
77-
return ExecutionAddress(keccak(uncompressed[1:])[12:])
78-
79-
execution_signature_registry[SECP256K1_ALGORITHM] = ExecutionSignatureAttributes(
80-
validate=secp256k1_validate,
81-
recover_signer=secp256k1_recover_signer,
82-
)
83-
8473
class FeePerGas(uint256):
8574
pass
8675

@@ -294,25 +283,3 @@ class RlpTxType(IntEnum):
294283
SET_CODE = 0x04
295284
SET_CODE_MAGIC = 0x05
296285

297-
def validate_transaction(tx: Transaction):
298-
tx_data = tx.payload.data()
299-
match tx_data.type_:
300-
case RlpTxType.LEGACY:
301-
assert isinstance(tx_data, RlpLegacyTransactionPayload)
302-
case RlpTxType.ACCESS_LIST:
303-
assert isinstance(tx_data, RlpAccessListTransactionPayload)
304-
case RlpTxType.FEE_MARKET:
305-
assert isinstance(tx_data, RlpFeeMarketTransactionPayload)
306-
case RlpTxType.BLOB:
307-
assert isinstance(tx_data, RlpBlobTransactionPayload)
308-
case RlpTxType.SET_CODE:
309-
assert isinstance(tx_data, RlpSetCodeTransactionPayload)
310-
for auth in tx_data.authorization_list:
311-
auth_data = auth.payload.data()
312-
assert auth_data.magic == RlpTxType.SET_CODE_MAGIC
313-
if hasattr(auth_data, "chain_id"):
314-
assert auth_data.chain_id != 0
315-
validate_execution_signature(auth.signature, expected_algorithm=SECP256K1_ALGORITHM)
316-
case _:
317-
assert False
318-
validate_execution_signature(tx.signature, expected_algorithm=SECP256K1_ALGORITHM)

0 commit comments

Comments
 (0)