Skip to content

Commit c7f235f

Browse files
committed
generate test using debug_traceCall
1 parent ba1efef commit c7f235f

File tree

3 files changed

+308
-1
lines changed

3 files changed

+308
-1
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ evm_transition_tool =
4545
[options.entry_points]
4646
console_scripts =
4747
fill = entry_points.fill:main
48+
gentest = entry_points.gentest:main
4849
tf = entry_points.tf:main
4950
order_fixtures = entry_points.order_fixtures:main
5051
pyspelling_soft_fail = entry_points.pyspelling_soft_fail:main

src/entry_points/gentest.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
"""
2+
Define an entry point wrapper for test generator.
3+
"""
4+
5+
import json
6+
import os
7+
import sys
8+
from dataclasses import asdict, dataclass
9+
from typing import Dict, List, Union
10+
11+
import requests
12+
13+
from ethereum_test_tools import Account, Address, Transaction
14+
15+
16+
def main(): # noqa: D103
17+
if len(sys.argv) != 3:
18+
print_help()
19+
sys.exit(1)
20+
if sys.argv[1] in ["-h", "--help"]:
21+
print_help()
22+
sys.exit(0)
23+
make_test(sys.argv)
24+
25+
26+
def print_help(): # noqa: D103
27+
print("Extracts transaction and required state from the mainnet to make a BC test out of it")
28+
print("Usage: gentest <tx_hash> <file.py>")
29+
30+
31+
if __name__ == "__main__":
32+
main()
33+
34+
35+
def make_test(args): # noqa: D103
36+
transaction_hash = args[1]
37+
output_file = args[2]
38+
print("Load configs...")
39+
config = PyspecConfig(os.path.expanduser("~/.pyspec/config"))
40+
request = RequestManager(config.remote_nodes[0])
41+
42+
print("Perform tx request: eth_get_transaction_by_hash(" + f"{transaction_hash}" + ")")
43+
tr = request.eth_get_transaction_by_hash(transaction_hash)
44+
45+
print("Perform debug_trace_call")
46+
state = request.debug_trace_call(tr)
47+
48+
print("Generate py test >> " + output_file)
49+
test = make_test_template(tr, state)
50+
with open(output_file, "w") as file:
51+
file.write(test)
52+
53+
print(test)
54+
55+
56+
def make_test_template(
57+
tr: "RequestManager.RemoteTransaction", state: Dict[Address, Account]
58+
) -> str:
59+
"""
60+
Prepare the .py file template
61+
"""
62+
test = PYTEST_TEMPLATE
63+
test = test.replace(
64+
"$HEADLINE_COMMENT",
65+
"gentest autogenerated test with debug_traceCall of tx.hash " + tr.tr_hash,
66+
)
67+
test = test.replace("$TEST_NAME", "test_transaction_" + tr.tr_hash[2:])
68+
test = test.replace(
69+
"$TEST_COMMENT", "gentest autogenerated test for tx.hash " + tr.tr_hash[2:]
70+
)
71+
72+
# Print a nice .py storage pre
73+
pad = " "
74+
state_str = ""
75+
for address, account in state.items():
76+
if isinstance(account, dict):
77+
account_obj = Account(**account)
78+
state_str += ' "' + str(address) + '": Account(\n'
79+
state_str += pad + "balance=" + str(account_obj.balance) + ",\n"
80+
if address == tr.transaction.sender:
81+
state_str += pad + "nonce=" + str(tr.transaction.nonce) + ",\n"
82+
else:
83+
state_str += pad + "nonce=" + str(account_obj.nonce) + ",\n"
84+
85+
if account_obj.code is None:
86+
state_str += pad + 'code="0x",\n'
87+
else:
88+
state_str += pad + 'code="' + str(account_obj.code) + '",\n'
89+
state_str += pad + "storage={\n"
90+
91+
if account_obj.storage is not None:
92+
for record, value in account_obj.storage.items():
93+
state_str += pad + ' "' + str(record) + '" : "' + str(value) + '",\n'
94+
95+
state_str += pad + "}\n"
96+
state_str += " ),\n"
97+
test = test.replace("$PRE", state_str)
98+
99+
# Print legacy transaction in .py
100+
tr_str = ""
101+
tr_str += pad + "ty=" + str(tr.transaction.ty) + ",\n"
102+
tr_str += pad + "chain_id=" + str(tr.transaction.chain_id) + ",\n"
103+
tr_str += pad + "nonce=" + str(tr.transaction.nonce) + ",\n"
104+
tr_str += pad + 'to="' + str(tr.transaction.to) + '",\n'
105+
tr_str += pad + "gas_price=" + str(tr.transaction.gas_price) + ",\n"
106+
tr_str += pad + "protected=False,\n"
107+
tr_str += pad + 'data="' + str(tr.transaction.data) + '",\n'
108+
tr_str += pad + "gas_limit=" + str(tr.transaction.gas_limit) + ",\n"
109+
tr_str += pad + "value=" + str(tr.transaction.value) + ",\n"
110+
tr_str += pad + "v=" + str(tr.transaction.v) + ",\n"
111+
tr_str += pad + "r=" + str(tr.transaction.r) + ",\n"
112+
tr_str += pad + "s=" + str(tr.transaction.s) + ",\n"
113+
114+
test = test.replace("$TR", tr_str)
115+
return test
116+
117+
118+
class PyspecConfig:
119+
"""
120+
Main class to manage Pyspec config
121+
"""
122+
123+
@dataclass
124+
class RemoteNode:
125+
"""
126+
Remote node structure
127+
"""
128+
129+
name: str
130+
node_url: str
131+
client_id: str
132+
secret: str
133+
134+
remote_nodes: List["PyspecConfig.RemoteNode"]
135+
136+
def __init__(self, config_path: str):
137+
"""
138+
Initialize pyspec config from file
139+
"""
140+
with open(config_path, "r") as file:
141+
data = json.load(file)
142+
self.remote_nodes = [self._json_to_remote_node(node) for node in data["remote_nodes"]]
143+
144+
def _json_to_remote_node(self, d):
145+
return PyspecConfig.RemoteNode(
146+
name=d["name"],
147+
node_url=d["node_url"],
148+
client_id=d["client_id"],
149+
secret=d["secret"],
150+
)
151+
152+
153+
class RequestManager:
154+
"""
155+
Interface for the RPC interaction with remote node
156+
"""
157+
158+
@dataclass()
159+
class RemoteTransaction:
160+
"""
161+
Remote transaction structure
162+
"""
163+
164+
block_number: str
165+
tr_hash: str
166+
transaction: Transaction
167+
168+
node_url: str
169+
headers: dict[str, str]
170+
171+
def __init__(self, node_config: PyspecConfig.RemoteNode):
172+
"""
173+
Initialize the RequestManager with specific client config.
174+
"""
175+
self.node_url = node_config.node_url
176+
self.headers = {
177+
"CF-Access-Client-Id": node_config.client_id,
178+
"CF-Access-Client-Secret": node_config.secret,
179+
"Content-Type": "application/json",
180+
}
181+
182+
def eth_get_transaction_by_hash(self, transaction_hash: str) -> RemoteTransaction:
183+
"""
184+
Get transaction data.
185+
"""
186+
data = {
187+
"jsonrpc": "2.0",
188+
"method": "eth_getTransactionByHash",
189+
"params": [f"{transaction_hash}"],
190+
"id": 1,
191+
}
192+
response = requests.post(self.node_url, headers=self.headers, data=json.dumps(data))
193+
res = response.json().get("result", None)
194+
195+
return RequestManager.RemoteTransaction(
196+
block_number=res["blockNumber"],
197+
tr_hash=res["hash"],
198+
transaction=Transaction(
199+
ty=int(res["type"], 16),
200+
gas_limit=int(res["gas"], 16),
201+
gas_price=int(res["gasPrice"], 16),
202+
data=res["input"],
203+
nonce=int(res["nonce"], 16),
204+
sender=res["from"],
205+
to=res["to"],
206+
value=int(res["value"], 16),
207+
v=int(res["v"], 16),
208+
r=int(res["r"], 16),
209+
s=int(res["s"], 16),
210+
),
211+
)
212+
213+
def eth_get_block_by_number(self, block_number: str):
214+
"""
215+
Get block by number
216+
"""
217+
data = {
218+
"jsonrpc": "2.0",
219+
"method": "eth_getBlockByNumber",
220+
"params": [f"{block_number}", False],
221+
"id": 1,
222+
}
223+
response = requests.post(self.node_url, headers=self.headers, data=json.dumps(data))
224+
return response.json().get("result", None)
225+
226+
def debug_trace_call(self, tr: RemoteTransaction) -> Dict[Address, Account]:
227+
"""
228+
Get pre state required for transaction
229+
"""
230+
data = {
231+
"jsonrpc": "2.0",
232+
"method": "debug_traceCall",
233+
"params": [
234+
{
235+
"from": f"{tr.transaction.sender}",
236+
"to": f"{tr.transaction.to}",
237+
"data": f"{tr.transaction.data}",
238+
},
239+
f"{tr.block_number}",
240+
{"tracer": "prestateTracer"},
241+
],
242+
"id": 1,
243+
}
244+
245+
response = requests.post(self.node_url, headers=self.headers, data=json.dumps(data))
246+
return response.json().get("result", None)
247+
248+
249+
PYTEST_TEMPLATE = """
250+
\"\"\"
251+
$HEADLINE_COMMENT
252+
\"\"\"
253+
254+
import pytest
255+
256+
from ethereum_test_tools import (
257+
Account,
258+
Address,
259+
Block,
260+
Environment,
261+
BlockchainTestFiller,
262+
Transaction,
263+
)
264+
265+
REFERENCE_SPEC_GIT_PATH = "EIPS/eip-6780.md"
266+
REFERENCE_SPEC_VERSION = "2f8299df31bb8173618901a03a8366a3183479b0"
267+
268+
269+
@pytest.fixture
270+
def env(): # noqa: D103
271+
return Environment(
272+
coinbase="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
273+
difficulty=3402669764409613,
274+
#gas_limit=12469470,
275+
gas_limit=0x016345785d8a0000,
276+
number=11114732,
277+
timestamp="0x5f933d46",
278+
)
279+
280+
281+
@pytest.mark.valid_from("Paris")
282+
def $TEST_NAME(
283+
env: Environment,
284+
blockchain_test: BlockchainTestFiller,
285+
):
286+
\"\"\"
287+
$TEST_COMMENT
288+
\"\"\"
289+
290+
pre = {
291+
$PRE
292+
}
293+
294+
post = {
295+
}
296+
297+
tx = Transaction(
298+
$TR
299+
)
300+
301+
blockchain_test(genesis_environment=env, pre=pre, post=post, blocks=[Block(txs=[tx])])
302+
303+
"""

whitelist.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func
111111
gaslimit
112112
gasprice
113113
GeneralStateTestsFiller
114+
gentest
114115
geth
115116
geth's
116117
getitem
@@ -204,6 +205,7 @@ Pre
204205
precompile
205206
prepend
206207
PrevRandao
208+
prestateTracer
207209
programmatically
208210
px
209211
py
@@ -212,6 +214,7 @@ pytest
212214
Pytest
213215
pytest's
214216
pytestArgs
217+
Pyspec
215218
qGpsxSA
216219
quickstart
217220
radd
@@ -535,4 +538,4 @@ modexp
535538

536539
fi
537540
url
538-
gz
541+
gz

0 commit comments

Comments
 (0)