Skip to content

Commit 53fa723

Browse files
committed
reset return data when VMError raised
VMError has a `erases_return_data` property but this never guaranteed anything. The BaseCall class somewhat accounted for this in its `__call__` method but this is sometimes not called e.g. Create(Opcode). We account for the `burns_gas` flag for the VMError on the `__exit__` call of a computation and so these changes make sure to account for the erasing of the return data for errors with the `erases_return_data` flag within this `__exit__` method as well.
1 parent e2423cd commit 53fa723

File tree

5 files changed

+123
-8
lines changed

5 files changed

+123
-8
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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from eth import constants
88
from eth.exceptions import (
99
Halt,
10+
InsufficientFunds,
1011
Revert,
12+
StackDepthLimit,
1113
WriteProtection,
1214
)
1315

@@ -154,14 +156,19 @@ def __call__(self, computation: ComputationAPI) -> None:
154156

155157
computation.extend_memory(stack_data.memory_start, stack_data.memory_length)
156158

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

162164
if insufficient_funds or stack_too_deep:
163165
computation.stack_push_int(0)
164-
return
166+
if insufficient_funds:
167+
raise InsufficientFunds(
168+
f"Insufficient funds: {storage_address_balance} < {stack_data.endowment}"
169+
)
170+
elif stack_too_deep:
171+
raise StackDepthLimit("Stack depth limit reached")
165172

166173
call_data = computation.memory_read_bytes(
167174
stack_data.memory_start, stack_data.memory_length

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

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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
InsufficientFunds,
14+
InvalidInstruction,
15+
)
16+
17+
18+
def _configure_mining_chain(starting_vm, vm_under_test):
19+
return MiningChain.configure(
20+
__name__="AllVMs",
21+
vm_configuration=(
22+
(
23+
constants.GENESIS_BLOCK_NUMBER,
24+
starting_vm.configure(consensus_class=NoProofConsensus),
25+
),
26+
(
27+
constants.GENESIS_BLOCK_NUMBER + 1,
28+
vm_under_test.configure(consensus_class=NoProofConsensus),
29+
),
30+
),
31+
chain_id=1337,
32+
)
33+
34+
35+
# CREATE, RETURNDATASIZE, and RETURNDATACOPY opcodes not added until Byzantium
36+
@pytest.fixture(params=MAINNET_VMS[4:])
37+
def byzantium_plus_miner(request, base_db, genesis_state):
38+
byzantium_vm = MAINNET_VMS[4]
39+
vm_under_test = request.param
40+
41+
klass = _configure_mining_chain(byzantium_vm, vm_under_test)
42+
header_fields = dict(
43+
difficulty=1,
44+
gas_limit=100000, # arbitrary, just enough for testing
45+
)
46+
return klass.from_genesis(base_db, header_fields, genesis_state)
47+
48+
49+
@pytest.mark.parametrize(
50+
"code",
51+
(
52+
# generate some return data, then call CREATE (third-to-last opcode - 0xf0)
53+
decode_hex('0x5b595958333d5859858585858585858585f195858585858585858485858585f195858585858585f1f03d30'), # noqa: E501
54+
# generate some return data, then call CREATE2 (third-to-last opcode - 0xf5)
55+
decode_hex('0x5b595958333d5859858585858585858585f195858585858585858485858585f195858585858585f1f53d30'), # noqa: E501
56+
)
57+
)
58+
def test_CREATE_and_CREATE2_resets_return_data_if_account_has_insufficient_funds(
59+
byzantium_plus_miner,
60+
canonical_address_a,
61+
transaction_context,
62+
code,
63+
):
64+
chain = byzantium_plus_miner
65+
vm = chain.get_vm()
66+
state = vm.state
67+
68+
assert state.get_balance(canonical_address_a) == 0
69+
70+
computation = vm.execute_bytecode(
71+
origin=canonical_address_a,
72+
to=canonical_address_a,
73+
sender=Address(
74+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x34'
75+
),
76+
value=0,
77+
code=code,
78+
data=code,
79+
gas=400000,
80+
gas_price=1,
81+
)
82+
83+
assert computation.is_error
84+
85+
if isinstance(computation.error, InvalidInstruction):
86+
# only test CREATE case for byzantium as the CREATE2 opcode (0xf5) was not yet introduced
87+
assert vm.fork == "byzantium"
88+
assert "0xf5" in repr(computation.error).lower()
89+
90+
else:
91+
assert isinstance(computation.error, InsufficientFunds)
92+
93+
assert computation.get_gas_used() == 400000
94+
assert computation.get_gas_refund() == 0
95+
assert computation.return_data == b''

0 commit comments

Comments
 (0)