Skip to content

Commit 2da165f

Browse files
authored
Merge pull request #2023 from fselmo/erase-return-data-on-appropriate-exceptions
Erase return data on appropriate exceptions
2 parents 79159be + 01b93d9 commit 2da165f

File tree

6 files changed

+146
-36
lines changed

6 files changed

+146
-36
lines changed

eth/vm/computation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ def __exit__(self,
489489
)),
490490
)
491491

492+
# When we raise an exception that erases return data, erase the return data.
493+
if self.should_erase_return_data:
494+
self.return_data = b''
495+
492496
# suppress VM exceptions
493497
return True
494498
elif exc_type is None and self.logger.show_debug2:

eth/vm/logic/system.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,19 @@ def __call__(self, computation: ComputationAPI) -> None:
154154

155155
computation.extend_memory(stack_data.memory_start, stack_data.memory_length)
156156

157-
insufficient_funds = computation.state.get_balance(
158-
computation.msg.storage_address
159-
) < stack_data.endowment
157+
storage_address_balance = computation.state.get_balance(computation.msg.storage_address)
158+
159+
insufficient_funds = storage_address_balance < stack_data.endowment
160160
stack_too_deep = computation.msg.depth + 1 > constants.STACK_DEPTH_LIMIT
161161

162162
if insufficient_funds or stack_too_deep:
163163
computation.stack_push_int(0)
164+
computation.return_data = b''
165+
if insufficient_funds:
166+
err_msg = f"Insufficient Funds: {storage_address_balance} < {stack_data.endowment}"
167+
elif stack_too_deep:
168+
err_msg = "Stack limit reached"
169+
self.logger.debug2("%s failure: %s", self.mnemonic, err_msg,)
164170
return
165171

166172
call_data = computation.memory_read_bytes(

newsfragments/2023.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Erase return data for exceptions with `erases_return_data` flag set to True and for CREATE / CREATE2 computations with insufficient funds

tests/core/vm/test_base_computation.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,31 +346,39 @@ def test_get_gas_used_with_revert(computation):
346346

347347
def test_should_burn_gas_with_vm_error(computation):
348348
assert computation.get_gas_remaining() == 100
349-
# Trigger an out of gas error causing get gas remaining to be 0
349+
# Trigger an out of gas error causing remaining gas to be 0
350350
with computation:
351351
raise VMError('Triggered VMError for tests')
352352
assert computation.should_burn_gas
353+
assert computation.get_gas_used() == 100
354+
assert computation.get_gas_remaining() == 0
353355

354356

355357
def test_should_burn_gas_with_revert(computation):
356358
assert computation.get_gas_remaining() == 100
357-
# Trigger an out of gas error causing get gas remaining to be 0
359+
# Trigger a Revert which should not burn gas
358360
with computation:
359361
raise Revert('Triggered VMError for tests')
360362
assert not computation.should_burn_gas
363+
assert computation.get_gas_used() == 0
364+
assert computation.get_gas_remaining() == 100
361365

362366

363367
def test_should_erase_return_data_with_vm_error(computation):
364368
assert computation.get_gas_remaining() == 100
365-
# Trigger an out of gas error causing get gas remaining to be 0
369+
computation.return_data = b'\x1337'
370+
# Trigger a VMError which should erase the return data
366371
with computation:
367372
raise VMError('Triggered VMError for tests')
368373
assert computation.should_erase_return_data
374+
assert computation.return_data == b''
369375

370376

371377
def test_should_erase_return_data_with_revert(computation):
372378
assert computation.get_gas_remaining() == 100
373-
# Trigger an out of gas error causing get gas remaining to be 0
379+
computation.return_data = b'\x1337'
380+
# Trigger a Revert which should not erase return data
374381
with computation:
375382
raise Revert('Triggered VMError for tests')
376383
assert not computation.should_erase_return_data
384+
assert computation.return_data == b'\x1337'

tests/core/vm/test_computation.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# test computation class behavior across VMs
2+
import pytest
3+
from eth_typing import Address
4+
5+
from eth_utils import decode_hex
6+
7+
from eth.chains.base import MiningChain
8+
from eth.chains.mainnet import MAINNET_VMS
9+
10+
from eth import constants
11+
from eth.consensus import NoProofConsensus
12+
from eth.exceptions import (
13+
InvalidInstruction,
14+
)
15+
16+
17+
def _configure_mining_chain(starting_vm, vm_under_test):
18+
return MiningChain.configure(
19+
__name__="AllVMs",
20+
vm_configuration=(
21+
(
22+
constants.GENESIS_BLOCK_NUMBER,
23+
starting_vm.configure(consensus_class=NoProofConsensus),
24+
),
25+
(
26+
constants.GENESIS_BLOCK_NUMBER + 1,
27+
vm_under_test.configure(consensus_class=NoProofConsensus),
28+
),
29+
),
30+
chain_id=1337,
31+
)
32+
33+
34+
# CREATE, RETURNDATASIZE, and RETURNDATACOPY opcodes not added until Byzantium
35+
@pytest.fixture(params=MAINNET_VMS[4:])
36+
def byzantium_plus_miner(request, base_db, genesis_state):
37+
byzantium_vm = MAINNET_VMS[4]
38+
vm_under_test = request.param
39+
40+
klass = _configure_mining_chain(byzantium_vm, vm_under_test)
41+
header_fields = dict(
42+
difficulty=1,
43+
gas_limit=100000, # arbitrary, just enough for testing
44+
)
45+
return klass.from_genesis(base_db, header_fields, genesis_state)
46+
47+
48+
@pytest.mark.parametrize(
49+
"code",
50+
(
51+
# generate some return data, then call CREATE (third-to-last opcode - 0xf0)
52+
decode_hex('0x5b595958333d5859858585858585858585f195858585858585858485858585f195858585858585f1f03d30'), # noqa: E501
53+
# generate some return data, then call CREATE2 (third-to-last opcode - 0xf5)
54+
decode_hex('0x5b595958333d5859858585858585858585f195858585858585858485858585f195858585858585f1f53d30'), # noqa: E501
55+
)
56+
)
57+
def test_CREATE_and_CREATE2_resets_return_data_if_account_has_insufficient_funds(
58+
byzantium_plus_miner,
59+
canonical_address_a,
60+
transaction_context,
61+
code,
62+
):
63+
chain = byzantium_plus_miner
64+
vm = chain.get_vm()
65+
state = vm.state
66+
67+
assert state.get_balance(canonical_address_a) == 0
68+
69+
computation = vm.execute_bytecode(
70+
origin=canonical_address_a,
71+
to=canonical_address_a,
72+
sender=Address(
73+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x34'
74+
),
75+
value=0,
76+
code=code,
77+
data=code,
78+
gas=40000,
79+
gas_price=1,
80+
)
81+
82+
if computation.is_error:
83+
assert isinstance(computation.error, InvalidInstruction)
84+
# only test CREATE case for byzantium as the CREATE2 opcode (0xf5) was not yet introduced
85+
assert vm.fork == "byzantium"
86+
assert "0xf5" in repr(computation.error).lower()
87+
88+
else:
89+
# We provide 40000 gas and a simple create uses 32000 gas. This test doesn't particularly
90+
# care (and isn't testing for) the exact gas, we just want to make sure not all the gas
91+
# is burned since if, say, a VMError were to be raised it would burn all the gas
92+
# (burns_gas = True).
93+
assert 34000 < computation.get_gas_used() < 39000
94+
assert computation.return_data == b''

tests/core/vm/test_london.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from eth.tools.factories.transaction import (
1515
new_dynamic_fee_transaction, new_transaction,
1616
)
17-
from eth.vm.message import Message
1817

1918
FOUR_TXN_GAS_LIMIT = 21000 * 4
2019

@@ -57,17 +56,17 @@
5756
)
5857

5958

60-
def _configure_mining_chain(name, request):
59+
def _configure_mining_chain(name, genesis_vm, vm_under_test):
6160
return MiningChain.configure(
6261
__name__=name,
6362
vm_configuration=(
6463
(
6564
constants.GENESIS_BLOCK_NUMBER,
66-
BerlinVM.configure(consensus_class=NoProofConsensus),
65+
genesis_vm.configure(consensus_class=NoProofConsensus),
6766
),
6867
(
6968
constants.GENESIS_BLOCK_NUMBER + 1,
70-
request.param.configure(consensus_class=NoProofConsensus),
69+
vm_under_test.configure(consensus_class=NoProofConsensus),
7170
),
7271
),
7372
chain_id=1337,
@@ -77,7 +76,9 @@ def _configure_mining_chain(name, request):
7776
# VMs starting at London
7877
@pytest.fixture(params=MAINNET_VMS[9:])
7978
def london_plus_miner(request, base_db, genesis_state):
80-
klass = _configure_mining_chain('LondonAt1', request)
79+
vm_under_test = request.param
80+
81+
klass = _configure_mining_chain('LondonAt1', BerlinVM, vm_under_test)
8182
header_fields = dict(
8283
difficulty=1,
8384
gas_limit=21000 * 2, # block limit is hit with two transactions
@@ -90,7 +91,9 @@ def london_plus_miner(request, base_db, genesis_state):
9091
# VMs up to, but not including, London
9192
@pytest.fixture(params=MAINNET_VMS[0:9])
9293
def pre_london_miner(request, base_db, genesis_state):
93-
klass = _configure_mining_chain('EndsBeforeLondon', request)
94+
vm_under_test = request.param
95+
96+
klass = _configure_mining_chain('EndsBeforeLondon', MAINNET_VMS[0], vm_under_test)
9497
header_fields = dict(
9598
difficulty=1,
9699
gas_limit=100000, # arbitrary, just enough for testing
@@ -145,41 +148,37 @@ def test_base_fee_evolution(
145148
EIP_3541_CREATE_AND_CREATE2_REVERT_TEST_CASES
146149
)
147150
def test_revert_on_reserved_0xEF_byte_for_CREATE_and_CREATE2_post_london(
148-
london_plus_miner, transaction_context, funded_address, code, data,
151+
london_plus_miner, funded_address, code, data,
149152
):
150153
chain = london_plus_miner
151-
state = chain.get_vm().state
154+
vm = chain.get_vm()
152155

153156
# test positive case from https://eips.ethereum.org/EIPS/eip-3541#test-cases
154-
create_message = Message(
157+
successful_create_computation = vm.execute_bytecode(
158+
origin=funded_address,
155159
to=funded_address,
156160
sender=funded_address,
157161
value=0,
158162
code=code,
159163
data=decode_hex("0x60fe60005360016000f3"),
160164
gas=400000,
161-
)
162-
163-
successful_create_computation = state.computation_class.apply_create_message(
164-
state, create_message, transaction_context
165+
gas_price=1,
165166
)
166167

167168
assert successful_create_computation.is_success
168-
# gas used varies between 32261 and 32270 depending on test case
169+
# assert only the appropriate gas is consumed, not all the gas. This falls within a range
169170
assert 32261 <= successful_create_computation.get_gas_used() <= 32270
170171

171172
# test parameterized negative cases
172-
reverted_create_message = Message(
173+
revert_create_computation = vm.execute_bytecode(
174+
origin=funded_address,
173175
to=funded_address,
174176
sender=funded_address,
175177
value=0,
176178
code=code,
177179
data=data,
178180
gas=40000,
179-
)
180-
181-
revert_create_computation = state.computation_class.apply_create_message(
182-
state, reverted_create_message, transaction_context
181+
gas_price=1,
183182
)
184183

185184
assert revert_create_computation.is_error
@@ -275,32 +274,30 @@ def test_state_revert_on_reserved_0xEF_byte_for_create_transaction_post_london(
275274
assert "0xef" in repr(reverted_computation.error).lower()
276275

277276
assert reverted_computation.get_nonce(funded_address) == 1 # assert nonce is still 1
278-
# reverted txn only consumes gas:
279-
assert end_balance == new_balance - mined_header.gas_used
277+
# reverted txn consumes the gas:
278+
assert mined_header.gas_used == 60000
279+
assert end_balance == new_balance - 60000
280280

281281

282282
@pytest.mark.parametrize(
283283
"code, data",
284284
EIP_3541_CREATE_AND_CREATE2_REVERT_TEST_CASES
285285
)
286286
def test_state_does_not_revert_on_reserved_0xEF_byte_for_CREATE_and_CREATE2_pre_london(
287-
pre_london_miner, transaction_context, funded_address, code, data,
287+
pre_london_miner, funded_address, code, data,
288288
):
289289
chain = pre_london_miner
290290
vm = chain.get_vm()
291-
state = vm.state
292291

293-
create_message = Message(
292+
computation = vm.execute_bytecode(
293+
origin=funded_address,
294294
to=funded_address,
295295
sender=funded_address,
296296
value=0,
297297
code=code,
298298
data=data,
299299
gas=40000,
300-
)
301-
302-
computation = state.computation_class.apply_create_message(
303-
state, create_message, transaction_context
300+
gas_price=1,
304301
)
305302

306303
if computation.is_error:
@@ -311,7 +308,7 @@ def test_state_does_not_revert_on_reserved_0xEF_byte_for_CREATE_and_CREATE2_pre_
311308

312309
else:
313310
assert computation.is_success
314-
# gas used varies between 32670 and 38470 depending on test case
311+
# assert only the appropriate gas is consumed, not all the gas. This falls within a range
315312
assert 32261 <= computation.get_gas_used() <= 38470
316313
assert computation.get_gas_refund() == 0
317314

0 commit comments

Comments
 (0)