Skip to content

Commit 052b4f2

Browse files
committed
feat(fw): Add type-4 transaction and authorization tuple type
1 parent c46fbef commit 052b4f2

File tree

6 files changed

+224
-17
lines changed

6 files changed

+224
-17
lines changed

src/ethereum_test_tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Account,
2121
Address,
2222
Alloc,
23+
AuthorizationTuple,
2324
DepositRequest,
2425
EngineAPIError,
2526
Environment,
@@ -69,6 +70,7 @@
6970
"Account",
7071
"Address",
7172
"Alloc",
73+
"AuthorizationTuple",
7274
"BaseFixture",
7375
"BaseTest",
7476
"Block",

src/ethereum_test_tools/common/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
AccessList,
4141
Account,
4242
Alloc,
43+
AuthorizationTuple,
4344
DepositRequest,
4445
Environment,
4546
Removable,
@@ -57,6 +58,7 @@
5758
"AddrAA",
5859
"AddrBB",
5960
"Alloc",
61+
"AuthorizationTuple",
6062
"Bloom",
6163
"Bytes",
6264
"DepositRequest",

src/ethereum_test_tools/common/types.py

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
List,
1717
Sequence,
1818
SupportsBytes,
19+
Tuple,
1920
Type,
2021
TypeAlias,
2122
TypeVar,
@@ -980,6 +981,115 @@ def to_list(self) -> List[Address | List[Hash]]:
980981
return [self.address, self.storage_keys]
981982

982983

984+
class AuthorizationTupleGeneric(CamelModel, Generic[NumberBoundTypeVar]):
985+
"""
986+
Authorization tuple for transactions.
987+
"""
988+
989+
chain_id: NumberBoundTypeVar = Field(1) # type: ignore
990+
address: Address
991+
nonce: List[NumberBoundTypeVar] | NumberBoundTypeVar | None = None
992+
993+
v: NumberBoundTypeVar = Field(0) # type: ignore
994+
r: NumberBoundTypeVar = Field(0) # type: ignore
995+
s: NumberBoundTypeVar = Field(0) # type: ignore
996+
997+
magic: ClassVar[int] = 0x05
998+
999+
def model_post_init(self, __context: Any) -> None:
1000+
"""
1001+
Automatically converts the nonce to a list if it is not already.
1002+
"""
1003+
super().model_post_init(__context)
1004+
if self.nonce is not None and not isinstance(self.nonce, list):
1005+
self.nonce = [self.nonce]
1006+
1007+
def nonce_list(self) -> List[Uint]:
1008+
"""
1009+
Returns the nonce as a list.
1010+
"""
1011+
if self.nonce is None:
1012+
return []
1013+
return (
1014+
[Uint(n) for n in self.nonce] if isinstance(self.nonce, list) else [Uint(self.nonce)]
1015+
)
1016+
1017+
def to_list(self) -> List[Any]:
1018+
"""
1019+
Returns the authorization tuple as a list of serializable elements.
1020+
"""
1021+
return [
1022+
Uint(self.chain_id),
1023+
self.address,
1024+
self.nonce_list(),
1025+
Uint(self.v),
1026+
Uint(self.r),
1027+
Uint(self.s),
1028+
]
1029+
1030+
def signing_hash(self) -> Hash:
1031+
"""
1032+
Returns the data to be signed.
1033+
"""
1034+
return Hash(
1035+
keccak256(
1036+
int.to_bytes(self.magic, length=1, byteorder="big")
1037+
+ eth_rlp.encode(
1038+
[
1039+
Uint(self.chain_id),
1040+
self.address,
1041+
self.nonce_list(),
1042+
]
1043+
)
1044+
)
1045+
)
1046+
1047+
def signature(self, private_key: Hash) -> Tuple[int, int, int]:
1048+
"""
1049+
Returns the signature of the authorization tuple.
1050+
"""
1051+
signing_hash = self.signing_hash()
1052+
signature_bytes = PrivateKey(secret=private_key).sign_recoverable(
1053+
signing_hash, hasher=None
1054+
)
1055+
return (
1056+
signature_bytes[64],
1057+
int.from_bytes(signature_bytes[0:32], byteorder="big"),
1058+
int.from_bytes(signature_bytes[32:64], byteorder="big"),
1059+
)
1060+
1061+
1062+
class AuthorizationTuple(AuthorizationTupleGeneric[HexNumber]):
1063+
"""
1064+
Authorization tuple for transactions.
1065+
"""
1066+
1067+
signer: EOA | None = None
1068+
secret_key: Hash | None = None
1069+
1070+
def model_post_init(self, __context: Any) -> None:
1071+
"""
1072+
Automatically signs the authorization tuple if a secret key or sender are provided.
1073+
"""
1074+
super().model_post_init(__context)
1075+
1076+
if self.secret_key is not None:
1077+
self.sign(self.secret_key)
1078+
elif self.signer is not None:
1079+
assert self.signer.key is not None, "signer must have a key"
1080+
self.sign(self.signer.key)
1081+
1082+
def sign(self, private_key: Hash) -> None:
1083+
"""
1084+
Signs the authorization tuple with a private key.
1085+
"""
1086+
signature = self.signature(private_key)
1087+
1088+
self.v = HexNumber(signature[0])
1089+
self.r = HexNumber(signature[1])
1090+
self.s = HexNumber(signature[2])
1091+
1092+
9831093
class TransactionGeneric(BaseModel, Generic[NumberBoundTypeVar]):
9841094
"""
9851095
Generic transaction type used as a parent for Transaction and FixtureTransaction (blockchain).
@@ -1070,6 +1180,8 @@ class Transaction(TransactionGeneric[HexNumber], TransactionTransitionToolConver
10701180
to: Address | None = Field(Address(0xAA))
10711181
data: Bytes = Field(Bytes(b""), alias="input")
10721182

1183+
authorization_tuples: List[AuthorizationTuple] | None = None
1184+
10731185
secret_key: Hash | None = None
10741186
error: List[TransactionException] | TransactionException | None = Field(None, exclude=True)
10751187

@@ -1117,7 +1229,9 @@ def model_post_init(self, __context):
11171229

11181230
if "ty" not in self.model_fields_set:
11191231
# Try to deduce transaction type from included fields
1120-
if self.max_fee_per_blob_gas is not None or self.blob_kzg_commitments is not None:
1232+
if self.authorization_tuples is not None:
1233+
self.ty = 4
1234+
elif self.max_fee_per_blob_gas is not None or self.blob_kzg_commitments is not None:
11211235
self.ty = 3
11221236
elif self.max_fee_per_gas is not None or self.max_priority_fee_per_gas is not None:
11231237
self.ty = 2
@@ -1150,6 +1264,9 @@ def model_post_init(self, __context):
11501264
if self.ty == 3 and self.max_fee_per_blob_gas is None:
11511265
self.max_fee_per_blob_gas = 1
11521266

1267+
if self.ty == 4 and self.authorization_tuples is None:
1268+
self.authorization_tuples = []
1269+
11531270
if "nonce" not in self.model_fields_set and self.sender is not None:
11541271
self.nonce = HexNumber(self.sender.get_nonce())
11551272

@@ -1233,18 +1350,40 @@ def signing_envelope(self) -> List[Any]:
12331350
Returns the list of values included in the envelope used for signing.
12341351
"""
12351352
to = self.to if self.to else bytes()
1236-
if self.ty == 3:
1353+
if self.ty == 4:
1354+
# EIP-7702: https://eips.ethereum.org/EIPS/eip-7702
1355+
if self.max_priority_fee_per_gas is None:
1356+
raise ValueError(f"max_priority_fee_per_gas must be set for type {self.ty} tx")
1357+
if self.max_fee_per_gas is None:
1358+
raise ValueError(f"max_fee_per_gas must be set for type {self.ty} tx")
1359+
if self.access_list is None:
1360+
raise ValueError(f"access_list must be set for type {self.ty} tx")
1361+
if self.authorization_tuples is None:
1362+
raise ValueError(f"authorization_tuples must be set for type {self.ty} tx")
1363+
return [
1364+
Uint(self.chain_id),
1365+
Uint(self.nonce),
1366+
Uint(self.max_priority_fee_per_gas),
1367+
Uint(self.max_fee_per_gas),
1368+
Uint(self.gas_limit),
1369+
to,
1370+
Uint(self.value),
1371+
self.data,
1372+
[a.to_list() for a in self.access_list],
1373+
[a.to_list() for a in self.authorization_tuples],
1374+
]
1375+
elif self.ty == 3:
12371376
# EIP-4844: https://eips.ethereum.org/EIPS/eip-4844
12381377
if self.max_priority_fee_per_gas is None:
1239-
raise ValueError("max_priority_fee_per_gas must be set for type 3 tx")
1378+
raise ValueError(f"max_priority_fee_per_gas must be set for type {self.ty} tx")
12401379
if self.max_fee_per_gas is None:
1241-
raise ValueError("max_fee_per_gas must be set for type 3 tx")
1380+
raise ValueError(f"max_fee_per_gas must be set for type {self.ty} tx")
12421381
if self.max_fee_per_blob_gas is None:
1243-
raise ValueError("max_fee_per_blob_gas must be set for type 3 tx")
1382+
raise ValueError(f"max_fee_per_blob_gas must be set for type {self.ty} tx")
12441383
if self.blob_versioned_hashes is None:
1245-
raise ValueError("blob_versioned_hashes must be set for type 3 tx")
1384+
raise ValueError(f"blob_versioned_hashes must be set for type {self.ty} tx")
12461385
if self.access_list is None:
1247-
raise ValueError("access_list must be set for type 3 tx")
1386+
raise ValueError(f"access_list must be set for type {self.ty} tx")
12481387
return [
12491388
Uint(self.chain_id),
12501389
Uint(self.nonce),
@@ -1261,11 +1400,11 @@ def signing_envelope(self) -> List[Any]:
12611400
elif self.ty == 2:
12621401
# EIP-1559: https://eips.ethereum.org/EIPS/eip-1559
12631402
if self.max_priority_fee_per_gas is None:
1264-
raise ValueError("max_priority_fee_per_gas must be set for type 2 tx")
1403+
raise ValueError(f"max_priority_fee_per_gas must be set for type {self.ty} tx")
12651404
if self.max_fee_per_gas is None:
1266-
raise ValueError("max_fee_per_gas must be set for type 2 tx")
1405+
raise ValueError(f"max_fee_per_gas must be set for type {self.ty} tx")
12671406
if self.access_list is None:
1268-
raise ValueError("access_list must be set for type 2 tx")
1407+
raise ValueError(f"access_list must be set for type {self.ty} tx")
12691408
return [
12701409
Uint(self.chain_id),
12711410
Uint(self.nonce),
@@ -1280,9 +1419,9 @@ def signing_envelope(self) -> List[Any]:
12801419
elif self.ty == 1:
12811420
# EIP-2930: https://eips.ethereum.org/EIPS/eip-2930
12821421
if self.gas_price is None:
1283-
raise ValueError("gas_price must be set for type 1 tx")
1422+
raise ValueError(f"gas_price must be set for type {self.ty} tx")
12841423
if self.access_list is None:
1285-
raise ValueError("access_list must be set for type 1 tx")
1424+
raise ValueError(f"access_list must be set for type {self.ty} tx")
12861425

12871426
return [
12881427
Uint(self.chain_id),
@@ -1296,7 +1435,7 @@ def signing_envelope(self) -> List[Any]:
12961435
]
12971436
elif self.ty == 0:
12981437
if self.gas_price is None:
1299-
raise ValueError("gas_price must be set for type 0 tx")
1438+
raise ValueError(f"gas_price must be set for type {self.ty} tx")
13001439

13011440
if self.protected:
13021441
# EIP-155: https://eips.ethereum.org/EIPS/eip-155
@@ -1338,11 +1477,11 @@ def payload_body(self) -> List[Any]:
13381477
elif self.ty == 3 and self.wrapped_blob_transaction:
13391478
# EIP-4844: https://eips.ethereum.org/EIPS/eip-4844
13401479
if self.blobs is None:
1341-
raise ValueError("blobs must be set for type 3 tx")
1480+
raise ValueError(f"blobs must be set for type {self.ty} tx")
13421481
if self.blob_kzg_commitments is None:
1343-
raise ValueError("blob_kzg_commitments must be set for type 3 tx")
1482+
raise ValueError(f"blob_kzg_commitments must be set for type {self.ty} tx")
13441483
if self.blob_kzg_proofs is None:
1345-
raise ValueError("blob_kzg_proofs must be set for type 3 tx")
1484+
raise ValueError(f"blob_kzg_proofs must be set for type {self.ty} tx")
13461485
return [
13471486
signing_envelope + [Uint(self.v), Uint(self.r), Uint(self.s)],
13481487
list(self.blobs),

src/ethereum_test_tools/spec/blockchain/types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ...common.constants import EmptyOmmersRoot, EngineAPIError
3434
from ...common.types import (
3535
Alloc,
36+
AuthorizationTupleGeneric,
3637
CamelModel,
3738
DepositRequest,
3839
DepositRequestGeneric,
@@ -509,11 +510,28 @@ def from_fixture_header(
509510
return new_payload
510511

511512

513+
class FixtureAuthorizationTuple(AuthorizationTupleGeneric[ZeroPaddedHexNumber]):
514+
"""
515+
Authorization tuple for fixture transactions.
516+
"""
517+
518+
@classmethod
519+
def from_authorization_tuple(
520+
cls, auth_tuple: AuthorizationTupleGeneric
521+
) -> "FixtureAuthorizationTuple":
522+
"""
523+
Returns a FixtureAuthorizationTuple from an AuthorizationTuple.
524+
"""
525+
return cls(**auth_tuple.model_dump())
526+
527+
512528
class FixtureTransaction(TransactionFixtureConverter, TransactionGeneric[ZeroPaddedHexNumber]):
513529
"""
514530
Representation of an Ethereum transaction within a test Fixture.
515531
"""
516532

533+
authorization_tuples: List[FixtureAuthorizationTuple] | None = None
534+
517535
@classmethod
518536
def from_transaction(cls, tx: Transaction) -> "FixtureTransaction":
519537
"""

src/ethereum_test_tools/spec/state/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ...common.types import (
1313
AccessList,
1414
Alloc,
15+
AuthorizationTupleGeneric,
1516
CamelModel,
1617
EnvironmentGeneric,
1718
Transaction,
@@ -29,6 +30,21 @@ class FixtureEnvironment(EnvironmentGeneric[ZeroPaddedHexNumber]):
2930
prev_randao: Hash | None = Field(None, alias="currentRandom") # type: ignore
3031

3132

33+
class FixtureAuthorizationTuple(AuthorizationTupleGeneric[ZeroPaddedHexNumber]):
34+
"""
35+
Authorization tuple for fixture transactions.
36+
"""
37+
38+
@classmethod
39+
def from_authorization_tuple(
40+
cls, auth_tuple: AuthorizationTupleGeneric
41+
) -> "FixtureAuthorizationTuple":
42+
"""
43+
Returns a FixtureAuthorizationTuple from an AuthorizationTuple.
44+
"""
45+
return cls(**auth_tuple.model_dump())
46+
47+
3248
class FixtureTransaction(TransactionFixtureConverter):
3349
"""
3450
Type used to describe a transaction in a state test.
@@ -43,6 +59,7 @@ class FixtureTransaction(TransactionFixtureConverter):
4359
value: List[ZeroPaddedHexNumber]
4460
data: List[Bytes]
4561
access_lists: List[List[AccessList]] | None = None
62+
authorization_tuples: List[FixtureAuthorizationTuple] | None = None
4663
max_fee_per_blob_gas: ZeroPaddedHexNumber | None = None
4764
blob_versioned_hashes: Sequence[Hash] | None = None
4865
sender: Address | None = None

0 commit comments

Comments
 (0)