Skip to content

Commit 724131d

Browse files
chore(fw): raise an exception if an opcode recieves an invalid keyword argument (#1739)
* chore(fw): raise an exception if an opcode recieves an invalid keyword argument * Add kwargs validation in Opcode.__call__ method in opcode.py * Add test coverage in test_vm.py for validation behavior * fix(tests): Incorrect kwargs usages * fix(vm): tox, mypy * fix(tests): kwargs fix * fix: mypy * fix(tests): Fix coverage * Update docs/CHANGELOG.md --------- Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent fbbabc6 commit 724131d

File tree

13 files changed

+199
-61
lines changed

13 files changed

+199
-61
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Users can select any of the artifacts depending on their testing needs for their
2828
#### 🔀 Refactoring
2929

3030
- 🔀 Move `TransactionType` enum from test file to proper module location in `ethereum_test_types.transaction_types` for better code organization and reusability.
31+
- ✨ Opcode classes now validate keyword arguments and raise `ValueError` with clear error messages.
3132

3233
#### `fill`
3334

src/ethereum_test_vm/opcode.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Opcode(Bytecode):
8787
data_portion_length: int
8888
data_portion_formatter: Optional[Callable[[Any], bytes]]
8989
stack_properties_modifier: Optional[Callable[[Any], tuple[int, int, int, int]]]
90-
kwargs: List[str] | None
90+
kwargs: List[str]
9191
kwargs_defaults: KW_ARGS_DEFAULTS_TYPE
9292
unchecked_stack: bool = False
9393

@@ -137,7 +137,10 @@ def __new__(
137137
obj.data_portion_formatter = data_portion_formatter
138138
obj.stack_properties_modifier = stack_properties_modifier
139139
obj.unchecked_stack = unchecked_stack
140-
obj.kwargs = kwargs
140+
if kwargs is None:
141+
obj.kwargs = []
142+
else:
143+
obj.kwargs = kwargs
141144
obj.kwargs_defaults = kwargs_defaults
142145
return obj
143146
raise TypeError("Opcode constructor '__new__' didn't return an instance!")
@@ -254,6 +257,16 @@ def __call__(
254257

255258
if self.kwargs is not None and len(kwargs) > 0:
256259
assert len(args) == 0, f"Cannot mix positional and keyword arguments {args} {kwargs}"
260+
261+
# Validate that all provided kwargs are valid
262+
invalid_kwargs = set(kwargs.keys()) - set(self.kwargs)
263+
if invalid_kwargs:
264+
raise ValueError(
265+
f"Invalid keyword argument(s) {list(invalid_kwargs)} for opcode "
266+
f"{self._name_}. Valid arguments are: {self.kwargs}"
267+
f"Valid arguments are: {self.kwargs}"
268+
)
269+
257270
for kw in self.kwargs:
258271
args.append(kwargs[kw] if kw in kwargs else self.kwargs_defaults.get(kw, 0))
259272

src/ethereum_test_vm/tests/test_vm.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,38 @@ def test_bytecode_concatenation_with_bytes():
431431
assert code.max_stack_height == base.max_stack_height
432432
assert code.min_stack_height == base.min_stack_height
433433
assert code.terminating == base.terminating
434+
435+
436+
def test_opcode_kwargs_validation():
437+
"""Test that invalid keyword arguments raise ValueError."""
438+
# Test valid kwargs work
439+
Op.MSTORE(offset=0, value=1)
440+
Op.CALL(gas=1, address=2, value=3, args_offset=4, args_size=5, ret_offset=6, ret_size=7)
441+
442+
# Test invalid kwargs raise ValueError
443+
with pytest.raises(
444+
ValueError, match=r"Invalid keyword argument\(s\) \['offest'\] for opcode MSTORE"
445+
):
446+
Op.MSTORE(offest=0, value=1) # codespell:ignore offest
447+
448+
with pytest.raises(
449+
ValueError, match=r"Invalid keyword argument\(s\) \['wrong_arg'\] for opcode MSTORE"
450+
):
451+
Op.MSTORE(offset=0, value=1, wrong_arg=2)
452+
453+
with pytest.raises(
454+
ValueError, match=r"Invalid keyword argument\(s\) \['addres'\] for opcode CALL"
455+
):
456+
Op.CALL(
457+
gas=1,
458+
addres=2, # codespell:ignore
459+
value=3,
460+
args_offset=4,
461+
args_size=5,
462+
ret_offset=6,
463+
ret_size=7,
464+
)
465+
466+
# Test multiple invalid kwargs
467+
with pytest.raises(ValueError, match=r"Invalid keyword argument\(s\).*for opcode MSTORE"):
468+
Op.MSTORE(offest=0, valu=1, extra=2) # codespell:ignore offest,valu

tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,16 @@ def creator_contract_code( # noqa: D102
155155
+ Op.TSTORE(1, 0x0200)
156156
+ Op.TSTORE(2, 0x0300)
157157
+ Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE)
158-
+ Op.SSTORE(4, Op.CALL(address=opcode(size=Op.CALLDATASIZE, salt=create2_salt)))
158+
+ Op.SSTORE(
159+
4,
160+
Op.CALL(
161+
address=(
162+
opcode(size=Op.CALLDATASIZE, salt=create2_salt)
163+
if opcode == Op.CREATE2
164+
else opcode(size=Op.CALLDATASIZE)
165+
)
166+
),
167+
)
159168
# Save the state of transient storage following call to storage; the transient
160169
# storage should not have been overwritten
161170
+ Op.SSTORE(0, Op.TLOAD(0))

tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ def deploy_contract(
132132
create_opcode = Op.CREATE if self == BlobhashContext.CREATE else Op.CREATE2
133133
create_bytecode = Op.EXTCODECOPY(
134134
address=initcode_address, dest_offset=0, offset=0, size=len(initcode)
135-
) + Op.POP(create_opcode(value=0, offset=0, size=len(initcode), salt=0))
135+
) + Op.POP(
136+
create_opcode(value=0, offset=0, size=len(initcode), salt=0)
137+
if create_opcode == Op.CREATE2
138+
else create_opcode(value=0, offset=0, size=len(initcode))
139+
)
136140
return pre.deploy_contract(create_bytecode)
137141

138142
case _:

tests/cancun/eip6780_selfdestruct/test_reentrancy_selfdestruct_revert.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ def executor_contract_bytecode(
5353
) -> Bytecode:
5454
"""Contract code that performs a selfdestruct call then revert."""
5555
return (
56-
Op.SSTORE(1, first_suicide(address=selfdestruct_contract_address, value=0))
56+
Op.SSTORE(
57+
1,
58+
(
59+
first_suicide(address=selfdestruct_contract_address, value=0)
60+
if first_suicide in [Op.CALL, Op.CALLCODE]
61+
else first_suicide(address=selfdestruct_contract_address)
62+
),
63+
)
5764
+ Op.SSTORE(2, Op.CALL(address=revert_contract_address))
5865
+ Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE())
5966
+ Op.SSTORE(3, Op.MLOAD(0))
@@ -95,7 +102,11 @@ def revert_contract_bytecode(
95102
selfdestruct_contract_address: Address,
96103
) -> Bytecode:
97104
"""Contract code that performs a call and then reverts."""
98-
call_op = second_suicide(address=selfdestruct_contract_address, value=100)
105+
call_op = (
106+
second_suicide(address=selfdestruct_contract_address, value=100)
107+
if second_suicide in [Op.CALL, Op.CALLCODE]
108+
else second_suicide(address=selfdestruct_contract_address)
109+
)
99110
return Op.MSTORE(0, Op.ADD(15, call_op)) + Op.REVERT(0, 32)
100111

101112

tests/constantinople/eip1014_create2/test_create_returndata.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ def test_create2_return_data(
4949
slot_begin_memory_after_create = 8
5050

5151
# CREATE2 Initcode
52-
create2_salt = 1
5352
return_data_in_create = 0xFFFAFB
5453
initcode = Op.MSTORE(0, return_data_in_create) + return_type_in_create(0, 32)
5554
call_return_data_value = 0x1122334455667788991011121314151617181920212223242526272829303132
@@ -79,7 +78,7 @@ def test_create2_return_data(
7978
+ Op.SSTORE(slot_return_data_hash_before_create, Op.SHA3(0, call_return_size))
8079
#
8180
#
82-
+ create_type(offset=0x100, size=Op.CALLDATASIZE(), salt=create2_salt)
81+
+ create_type(offset=0x100, size=Op.CALLDATASIZE())
8382
+ Op.SSTORE(slot_returndatasize_after_create, Op.RETURNDATASIZE())
8483
+ Op.RETURNDATACOPY(0x300, 0, Op.RETURNDATASIZE())
8584
+ Op.SSTORE(slot_returndatacopy_after_create, Op.MLOAD(0x300))

tests/frontier/create/test_create_one_byte.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_create_one_byte(
5050
# make a subcontract that deploys code, because deploy 0xef eats ALL gas
5151
create_contract = pre.deploy_contract(
5252
code=Op.MSTORE(0, Op.CALLDATALOAD(0))
53-
+ Op.MSTORE(32, create_opcode(offset=32 - initcode_length, salt=0, size=initcode_length))
53+
+ Op.MSTORE(32, create_opcode(offset=32 - initcode_length, size=initcode_length))
5454
+ Op.RETURN(32, 32)
5555
)
5656
code = pre.deploy_contract(

tests/frontier/scenarios/scenarios/call_combinations.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,23 @@ def _generate_one_call_scenario(self, first_call: Opcode) -> Scenario:
9393

9494
scenario_contract = pre.deploy_contract(
9595
code=Op.MSTORE(32, 1122334455)
96-
+ first_call(
97-
gas=Op.SUB(Op.GAS, self.keep_gas),
98-
address=operation_contract,
99-
args_offset=32,
100-
args_size=40,
101-
ret_size=32,
102-
value=balance.first_call_value,
96+
+ (
97+
first_call(
98+
gas=Op.SUB(Op.GAS, self.keep_gas),
99+
address=operation_contract,
100+
args_offset=32,
101+
args_size=40,
102+
ret_size=32,
103+
value=balance.first_call_value,
104+
)
105+
if first_call not in [Op.DELEGATECALL, Op.STATICCALL]
106+
else first_call(
107+
gas=Op.SUB(Op.GAS, self.keep_gas),
108+
address=operation_contract,
109+
args_offset=32,
110+
args_size=40,
111+
ret_size=32,
112+
)
103113
)
104114
+ Op.RETURN(0, 32),
105115
balance=balance.scenario_contract_balance,
@@ -230,23 +240,41 @@ def _compute_callvalue() -> int:
230240
)
231241
sub_contract = pre.deploy_contract(
232242
code=Op.MSTORE(32, 1122334455)
233-
+ second_call(
234-
gas=Op.SUB(Op.GAS, self.keep_gas),
235-
address=operation_contract,
236-
args_size=40,
237-
args_offset=32,
238-
ret_size=32,
239-
value=second_call_value,
243+
+ (
244+
second_call(
245+
gas=Op.SUB(Op.GAS, self.keep_gas),
246+
address=operation_contract,
247+
args_size=40,
248+
args_offset=32,
249+
ret_size=32,
250+
value=second_call_value,
251+
)
252+
if second_call not in [Op.DELEGATECALL, Op.STATICCALL]
253+
else second_call(
254+
gas=Op.SUB(Op.GAS, self.keep_gas),
255+
address=operation_contract,
256+
args_size=40,
257+
args_offset=32,
258+
ret_size=32,
259+
)
240260
)
241261
+ Op.RETURN(0, 32),
242262
balance=balance.sub_contract_balance,
243263
)
244264
scenario_contract = pre.deploy_contract(
245-
code=first_call(
246-
gas=Op.SUB(Op.GAS, self.keep_gas),
247-
address=sub_contract,
248-
ret_size=32,
249-
value=balance.first_call_value,
265+
code=(
266+
first_call(
267+
gas=Op.SUB(Op.GAS, self.keep_gas),
268+
address=sub_contract,
269+
ret_size=32,
270+
value=balance.first_call_value,
271+
)
272+
if first_call not in [Op.DELEGATECALL, Op.STATICCALL]
273+
else first_call(
274+
gas=Op.SUB(Op.GAS, self.keep_gas),
275+
address=sub_contract,
276+
ret_size=32,
277+
)
250278
)
251279
+ Op.RETURN(0, 32),
252280
balance=balance.scenario_contract_balance,

tests/frontier/scenarios/scenarios/create_combinations.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,25 @@ def _compute_selfbalance() -> int:
109109
+ Op.MSTORE(32, create(balance.create_value, 0, deploy_code_size, *salt))
110110
+ Op.MSTORE(0, 0)
111111
+ Op.MSTORE(64, 1122334455)
112-
+ call(
113-
gas=Op.SUB(Op.GAS, keep_gas),
114-
address=Op.MLOAD(32),
115-
args_offset=64,
116-
args_size=40,
117-
ret_offset=0,
118-
ret_size=32,
119-
value=balance.call_value,
112+
+ (
113+
call(
114+
gas=Op.SUB(Op.GAS, keep_gas),
115+
address=Op.MLOAD(32),
116+
args_offset=64,
117+
args_size=40,
118+
ret_offset=0,
119+
ret_size=32,
120+
value=balance.call_value,
121+
)
122+
if call not in [Op.DELEGATECALL, Op.STATICCALL]
123+
else call(
124+
gas=Op.SUB(Op.GAS, keep_gas),
125+
address=Op.MLOAD(32),
126+
args_offset=64,
127+
args_size=40,
128+
ret_offset=0,
129+
ret_size=32,
130+
)
120131
)
121132
+ Op.RETURN(0, 32),
122133
)

0 commit comments

Comments
 (0)