-
Notifications
You must be signed in to change notification settings - Fork 140
/
access_list_transaction.py
247 lines (224 loc) · 8.17 KB
/
access_list_transaction.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
from typing import (
Any,
Dict,
List,
Tuple,
cast,
)
from eth_rlp import (
HashableRLP,
)
from eth_utils import (
keccak,
)
from eth_utils.curried import (
apply_formatters_to_dict,
)
from eth_utils.toolz import (
dissoc,
merge,
partial,
pipe,
)
from hexbytes import (
HexBytes,
)
import rlp
from rlp.sedes import (
BigEndianInt,
Binary,
CountableList,
List as ListSedesClass,
big_endian_int,
binary,
)
from ..transaction_utils import (
transaction_rlp_to_rpc_structure,
transaction_rpc_to_rlp_structure,
)
from ..validation import (
LEGACY_TRANSACTION_VALID_VALUES,
is_int_or_prefixed_hexstr,
is_rpc_structured_access_list,
)
from .base import (
TYPED_TRANSACTION_FORMATTERS,
_TypedTransactionImplementation,
)
# Define typed transaction common sedes.
# [[{20 bytes}, [{32 bytes}...]]...], where ... means
# “zero or more of the thing to the left”.
access_list_sede_type = CountableList(
ListSedesClass(
[
Binary.fixed_length(20, allow_empty=False),
CountableList(BigEndianInt(32)),
]
),
)
class AccessListTransaction(_TypedTransactionImplementation):
"""
Represents an access list transaction per EIP-2930.
"""
# This is the first transaction to implement the EIP-2718 typed transaction.
transaction_type = 1 # '0x01'
unsigned_transaction_fields = (
("chainId", big_endian_int),
("nonce", big_endian_int),
("gasPrice", big_endian_int),
("gas", big_endian_int),
("to", Binary.fixed_length(20, allow_empty=True)),
("value", big_endian_int),
("data", binary),
("accessList", access_list_sede_type),
)
signature_fields = (
("v", big_endian_int),
("r", big_endian_int),
("s", big_endian_int),
)
transaction_field_defaults = {
"type": b"0x1",
"chainId": 0,
"to": b"",
"value": 0,
"data": b"",
"accessList": [],
}
_unsigned_transaction_serializer = type(
"_unsigned_transaction_serializer",
(HashableRLP,),
{
"fields": unsigned_transaction_fields,
},
)
_signed_transaction_serializer = type(
"_signed_transaction_serializer",
(HashableRLP,),
{
"fields": unsigned_transaction_fields + signature_fields,
},
)
def __init__(self, dictionary: Dict[str, Any]):
self.dictionary = dictionary
@classmethod
def assert_valid_fields(cls, dictionary: Dict[str, Any]) -> None:
transaction_valid_values = merge(
LEGACY_TRANSACTION_VALID_VALUES,
{
"type": is_int_or_prefixed_hexstr,
"accessList": is_rpc_structured_access_list,
},
)
if "v" in dictionary and dictionary["v"] == 0:
# This is insane logic that is required because the way we evaluate
# correct types is in the `if not all()` branch below, and 0 obviously
# maps to the int(0), which maps to False... This was not an issue in
# non-typed transaction because v=0, couldn't exist with the chain offset.
dictionary["v"] = "0x0"
valid_fields = apply_formatters_to_dict(
transaction_valid_values,
dictionary,
) # type: Dict[str, Any]
if not all(valid_fields.values()):
invalid = {
key: dictionary[key] for key, valid in valid_fields.items() if not valid
}
raise TypeError(f"Transaction had invalid fields: {repr(invalid)}")
@classmethod
def from_dict(
cls, dictionary: Dict[str, Any], blobs: List[bytes] = None
) -> "AccessListTransaction":
"""
Builds an AccessListTransaction from a dictionary.
Verifies that the dictionary is well formed.
"""
if blobs is not None:
raise ValueError("Blob data is not supported for `AccessListTransaction`.")
# Validate fields.
cls.assert_valid_fields(dictionary)
sanitized_dictionary = pipe(
dictionary,
dict,
partial(merge, cls.transaction_field_defaults),
apply_formatters_to_dict(TYPED_TRANSACTION_FORMATTERS),
)
# We have verified the type, we can safely remove it from the dictionary,
# given that it is not to be included within the RLP payload.
transaction_type = sanitized_dictionary.pop("type")
if transaction_type != cls.transaction_type:
raise ValueError(
f"expected transaction type {cls.transaction_type}, "
f"got {transaction_type}"
)
return cls(
dictionary=sanitized_dictionary,
)
@classmethod
def from_bytes(cls, encoded_transaction: HexBytes) -> "AccessListTransaction":
"""Builds an AccesslistTransaction from a signed encoded transaction."""
if not isinstance(encoded_transaction, HexBytes):
raise TypeError(f"expected Hexbytes, got type: {type(encoded_transaction)}")
if not (
len(encoded_transaction) > 0
and encoded_transaction[0] == cls.transaction_type
):
raise ValueError("unexpected input")
# Format is (0x01 || TransactionPayload)
# We strip the prefix, and RLP unmarshal the payload into our
# signed transaction serializer.
transaction_payload = encoded_transaction[1:]
rlp_serializer = cls._signed_transaction_serializer
dictionary = rlp_serializer.from_bytes( # type: ignore
transaction_payload
).as_dict()
rpc_structured_dict = transaction_rlp_to_rpc_structure(dictionary)
rpc_structured_dict["type"] = cls.transaction_type
return cls.from_dict(rpc_structured_dict)
def as_dict(self) -> Dict[str, Any]:
"""Returns this transaction as a dictionary."""
dictionary = self.dictionary.copy()
dictionary["type"] = self.__class__.transaction_type
return dictionary
def hash(self) -> bytes:
"""
Hashes this AccessListTransaction to prepare it for signing.
As per the EIP-2930 specifications, the signature is a secp256k1 signature over
keccak256(0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList])). # noqa E501
Here, we compute the keccak256(...) hash.
"""
# Remove signature fields.
transaction_without_signature_fields = dissoc(self.dictionary, "v", "r", "s")
# RPC-structured transaction to rlp-structured transaction
rlp_structured_txn_without_sig_fields = transaction_rpc_to_rlp_structure(
transaction_without_signature_fields
)
rlp_serializer = self.__class__._unsigned_transaction_serializer
hash = pipe(
rlp_serializer.from_dict(rlp_structured_txn_without_sig_fields), # type: ignore # noqa: E501
lambda val: rlp.encode(val), # rlp([...])
lambda val: bytes([self.__class__.transaction_type])
+ val, # (0x01 || rlp([...]))
keccak, # keccak256(0x01 || rlp([...]))
)
return cast(bytes, hash)
def payload(self) -> bytes:
"""
Returns this transaction's payload as bytes.
Here, the TransactionPayload = rlp([chainId,
nonce, gasPrice, gasLimit, to, value, data, accessList,
signatureYParity, signatureR, signatureS])
"""
if not all(k in self.dictionary for k in "vrs"):
raise ValueError("attempting to encode an unsigned transaction")
rlp_serializer = self.__class__._signed_transaction_serializer
rlp_structured_dict = transaction_rpc_to_rlp_structure(self.dictionary)
payload = rlp.encode(
rlp_serializer.from_dict(rlp_structured_dict) # type: ignore
)
return cast(bytes, payload)
def vrs(self) -> Tuple[int, int, int]:
"""Returns (v, r, s) if they exist."""
if not all(k in self.dictionary for k in "vrs"):
raise ValueError("attempting to encode an unsigned transaction")
return (self.dictionary["v"], self.dictionary["r"], self.dictionary["s"])