Skip to content

Commit 041fe5f

Browse files
authored
Merge pull request #239 from marioevz/conditional-generator
tools/code: Add conditional bytecode generator
2 parents 7244b20 + 211715f commit 041fe5f

File tree

4 files changed

+75
-3
lines changed

4 files changed

+75
-3
lines changed

src/ethereum_test_tools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
tests.
44
"""
55

6-
from .code import Code, CodeGasMeasure, Initcode, Yul, YulCompiler
6+
from .code import Code, CodeGasMeasure, Conditional, Initcode, Yul, YulCompiler
77
from .common import (
88
AccessList,
99
Account,
@@ -59,6 +59,7 @@
5959
"BlockchainTestFiller",
6060
"Code",
6161
"CodeGasMeasure",
62+
"Conditional",
6263
"EngineAPIError",
6364
"Environment",
6465
"Fixture",

src/ethereum_test_tools/code/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
Code related utilities and classes.
33
"""
44
from .code import Code
5-
from .generators import CodeGasMeasure, Initcode
5+
from .generators import CodeGasMeasure, Conditional, Initcode
66
from .yul import Yul, YulCompiler
77

88
__all__ = (
99
"Code",
1010
"CodeGasMeasure",
11+
"Conditional",
1112
"Initcode",
1213
"Yul",
1314
"YulCompiler",

src/ethereum_test_tools/code/generators.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from ..common.conversions import to_bytes
99
from ..common.helpers import ceiling_division
10+
from ..vm.opcode import Opcodes as Op
1011
from .code import Code
1112

1213
GAS_PER_DEPLOYED_CODE_BYTE = 0xC8
@@ -181,3 +182,51 @@ def __post_init__(self):
181182
]
182183
)
183184
self.bytecode = res
185+
186+
187+
@dataclass(kw_only=True)
188+
class Conditional(Code):
189+
"""
190+
Helper class used to generate conditional bytecode.
191+
"""
192+
193+
condition: str | bytes | SupportsBytes
194+
"""
195+
Condition bytecode which must return the true or false condition of the conditional statement.
196+
"""
197+
198+
if_true: str | bytes | SupportsBytes
199+
"""
200+
Bytecode to execute if the condition is true.
201+
"""
202+
203+
if_false: str | bytes | SupportsBytes
204+
"""
205+
Bytecode to execute if the condition is false.
206+
"""
207+
208+
def __post_init__(self):
209+
"""
210+
Assemble the conditional bytecode by generating the necessary jumps and
211+
jumpdests surrounding the condition and the two possible execution
212+
paths.
213+
214+
In the future, PC usage should be replaced by using RJUMP and RJUMPI
215+
"""
216+
condition_bytes = to_bytes(self.condition)
217+
if_true_bytes = to_bytes(self.if_true)
218+
if_false_bytes = to_bytes(self.if_false)
219+
220+
# First we append a jumpdest to the start of the true branch
221+
if_true_bytes = Op.JUMPDEST + if_true_bytes
222+
223+
# Then we append the unconditional jump to the end of the false branch, used to skip the
224+
# true branch
225+
if_false_bytes += Op.JUMP(Op.ADD(Op.PC, len(if_true_bytes) + 3))
226+
227+
# Then we need to do the conditional jump by skipping the false branch
228+
condition_bytes = Op.JUMPI(Op.ADD(Op.PC, len(if_false_bytes) + 3), condition_bytes)
229+
230+
# Finally we append the true and false branches, and the condition, plus the jumpdest at
231+
# the very end
232+
self.bytecode = condition_bytes + if_false_bytes + if_true_bytes + Op.JUMPDEST

src/ethereum_test_tools/tests/test_code.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
from ethereum_test_forks import Fork, Homestead, Shanghai, forks_from_until, get_deployed_forks
1212

13-
from ..code import Code, Initcode, Yul
13+
from ..code import Code, Conditional, Initcode, Yul
14+
from ..vm.opcode import Opcodes as Op
1415

1516

1617
@pytest.mark.parametrize(
@@ -239,3 +240,23 @@ def test_yul(
239240
)
240241
def test_initcode(initcode: Initcode, bytecode: bytes):
241242
assert bytes(initcode) == bytecode
243+
244+
245+
@pytest.mark.parametrize(
246+
"conditional_bytecode,expected",
247+
[
248+
(
249+
Conditional(
250+
condition=Op.CALLDATALOAD(0),
251+
if_true=Op.MSTORE(0, Op.SLOAD(0)) + Op.RETURN(0, 32),
252+
if_false=Op.SSTORE(0, 69),
253+
),
254+
bytes.fromhex("600035600d5801576045600055600f5801565b60005460005260206000f35b"),
255+
),
256+
],
257+
)
258+
def test_opcodes_if(conditional_bytecode: bytes, expected: bytes):
259+
"""
260+
Test that the if opcode macro is transformed into bytecode as expected.
261+
"""
262+
assert bytes(conditional_bytecode) == expected

0 commit comments

Comments
 (0)