Skip to content

Commit 2a7a6d7

Browse files
jsignmarioevz
andauthored
feat(benchmark): add coverage for CREATE and CREATE2 (#1847)
* benchmark: add CREATE coverage Signed-off-by: Ignacio Hagopian <[email protected]> * benchmark: add CREATE2 coverage Signed-off-by: Ignacio Hagopian <[email protected]> * benchmark: add CREATE2 collisions coverage Signed-off-by: Ignacio Hagopian <[email protected]> * fix msize problem Signed-off-by: Ignacio Hagopian <[email protected]> * benchmark: add CREATE collisions coverage Signed-off-by: Ignacio Hagopian <[email protected]> * typo Signed-off-by: Ignacio Hagopian <[email protected]> * nit Signed-off-by: Ignacio Hagopian <[email protected]> * Update tests/benchmark/test_worst_bytecode.py Co-authored-by: Mario Vega <[email protected]> * improvements Signed-off-by: Ignacio Hagopian <[email protected]> * improvements Signed-off-by: Ignacio Hagopian <[email protected]> * improvements Signed-off-by: Ignacio Hagopian <[email protected]> * lints Signed-off-by: Ignacio Hagopian <[email protected]> --------- Signed-off-by: Ignacio Hagopian <[email protected]> Co-authored-by: Mario Vega <[email protected]>
1 parent 0fe4295 commit 2a7a6d7

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed

tests/benchmark/test_worst_bytecode.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
compute_create2_address,
2525
)
2626
from ethereum_test_tools.vm.opcode import Opcodes as Op
27+
from ethereum_test_types.helpers import compute_create_address
28+
from tests.benchmark.helpers import code_loop_precompile_call
2729

2830
REFERENCE_SPEC_GIT_PATH = "TODO"
2931
REFERENCE_SPEC_VERSION = "TODO"
@@ -315,3 +317,187 @@ def test_worst_initcode_jumpdest_analysis(
315317
post={},
316318
tx=tx,
317319
)
320+
321+
322+
@pytest.mark.valid_from("Cancun")
323+
@pytest.mark.parametrize(
324+
"opcode",
325+
[
326+
Op.CREATE,
327+
Op.CREATE2,
328+
],
329+
)
330+
@pytest.mark.parametrize(
331+
"max_code_size_ratio, non_zero_data, value",
332+
[
333+
# To avoid a blowup of combinations, the value dimension is only explored for
334+
# the non-zero data case, so isn't affected by code size influence.
335+
pytest.param(0, False, 0, id="0 bytes without value"),
336+
pytest.param(0, False, 1, id="0 bytes with value"),
337+
pytest.param(0.25, True, 0, id="0.25x max code size with non-zero data"),
338+
pytest.param(0.25, False, 0, id="0.25x max code size with zero data"),
339+
pytest.param(0.50, True, 0, id="0.50x max code size with non-zero data"),
340+
pytest.param(0.50, False, 0, id="0.50x max code size with zero data"),
341+
pytest.param(0.75, True, 0, id="0.75x max code size with non-zero data"),
342+
pytest.param(0.75, False, 0, id="0.75x max code size with zero data"),
343+
pytest.param(1.00, True, 0, id="max code size with non-zero data"),
344+
pytest.param(1.00, False, 0, id="max code size with zero data"),
345+
],
346+
)
347+
def test_worst_create(
348+
state_test: StateTestFiller,
349+
pre: Alloc,
350+
fork: Fork,
351+
opcode: Op,
352+
max_code_size_ratio: float,
353+
non_zero_data: bool,
354+
value: int,
355+
):
356+
"""Test the CREATE and CREATE2 performance with different configurations."""
357+
env = Environment()
358+
max_code_size = fork.max_code_size()
359+
360+
code_size = int(max_code_size * max_code_size_ratio)
361+
362+
# Deploy the initcode template which has following design:
363+
# ```
364+
# PUSH3(code_size)
365+
# [CODECOPY(DUP1) -- Conditional that non_zero_data is True]
366+
# RETURN(0, DUP1)
367+
# [<pad to code_size>] -- Conditional that non_zero_data is True]
368+
# ```
369+
code = (
370+
Op.PUSH3(code_size)
371+
+ (Op.CODECOPY(size=Op.DUP1) if non_zero_data else Bytecode())
372+
+ Op.RETURN(0, Op.DUP1)
373+
)
374+
if non_zero_data: # Pad to code_size.
375+
code += bytes([i % 256 for i in range(code_size - len(code))])
376+
377+
initcode_template_contract = pre.deploy_contract(code=code)
378+
379+
# Create the benchmark contract which has the following design:
380+
# ```
381+
# PUSH(value)
382+
# [EXTCODECOPY(full initcode_template_contract) -- Conditional that non_zero_data is True]`
383+
# JUMPDEST (#)
384+
# (CREATE|CREATE2)
385+
# (CREATE|CREATE2)
386+
# ...
387+
# JUMP(#)
388+
# ```
389+
code_prefix = (
390+
Op.PUSH3(code_size)
391+
+ Op.PUSH1(value)
392+
+ Op.EXTCODECOPY(
393+
address=initcode_template_contract,
394+
size=Op.DUP2, # DUP2 refers to the EXTCODESIZE value above.
395+
)
396+
)
397+
398+
if opcode == Op.CREATE2:
399+
# For CREATE2, we provide an initial salt.
400+
code_prefix = code_prefix + Op.PUSH1(42)
401+
402+
attack_block = (
403+
# For CREATE:
404+
# - DUP2 refers to the EXTOCODESIZE value pushed in code_prefix.
405+
# - DUP3 refers to PUSH1(value) above.
406+
Op.POP(Op.CREATE(value=Op.DUP3, offset=0, size=Op.DUP2))
407+
if opcode == Op.CREATE
408+
# For CREATE2: we manually push the arguments because we leverage the return value of
409+
# previous CREATE2 calls as salt for the next CREATE2 call.
410+
# - DUP4 is targeting the PUSH1(value) from the code_prefix.
411+
# - DUP3 is targeting the EXTCODESIZE value pushed in code_prefix.
412+
else Op.DUP3 + Op.PUSH0 + Op.DUP4 + Op.CREATE2
413+
)
414+
code = code_loop_precompile_call(code_prefix, attack_block, fork)
415+
416+
tx = Transaction(
417+
# Set enough balance in the pre-alloc for `value > 0` configurations.
418+
to=pre.deploy_contract(code=code, balance=1_000_000_000 if value > 0 else 0),
419+
gas_limit=env.gas_limit,
420+
sender=pre.fund_eoa(),
421+
)
422+
423+
state_test(
424+
env=env,
425+
pre=pre,
426+
post={},
427+
tx=tx,
428+
)
429+
430+
431+
@pytest.mark.valid_from("Cancun")
432+
@pytest.mark.parametrize(
433+
"opcode",
434+
[
435+
Op.CREATE,
436+
Op.CREATE2,
437+
],
438+
)
439+
def test_worst_creates_collisions(
440+
state_test: StateTestFiller,
441+
pre: Alloc,
442+
fork: Fork,
443+
opcode: Op,
444+
):
445+
"""Test the CREATE and CREATE2 collisions performance."""
446+
env = Environment()
447+
448+
# We deploy a "proxy contract" which is the contract that will be called in a loop
449+
# using all the gas in the block. This "proxy contract" is the one executing CREATE2
450+
# failing with a collision.
451+
# The reason why we need a "proxy contract" is that CREATE(2) failing with a collision will
452+
# consume all the available gas. If we try to execute the CREATE(2) directly without being
453+
# wrapped **and capped in gas** in a previous CALL, we would run out of gas very fast!
454+
#
455+
# The proxy contract calls CREATE(2) with empty initcode. The current call frame gas will
456+
# be exhausted because of the collision. For this reason the caller will carefully give us
457+
# the minimal gas necessary to execute the CREATE(2) and not waste any extra gas in the
458+
# CREATE(2)-failure.
459+
#
460+
# Note that these CREATE(2) calls will fail because in (**) below we pre-alloc contracts
461+
# with the same address as the ones that CREATE(2) will try to create.
462+
proxy_contract = pre.deploy_contract(
463+
code=Op.CREATE2(value=Op.PUSH0, salt=Op.PUSH0, offset=Op.PUSH0, size=Op.PUSH0)
464+
if opcode == Op.CREATE2
465+
else Op.CREATE(value=Op.PUSH0, offset=Op.PUSH0, size=Op.PUSH0)
466+
)
467+
468+
gas_costs = fork.gas_costs()
469+
# The CALL to the proxy contract needs at a minimum gas corresponding to the CREATE(2)
470+
# plus extra required PUSH0s for arguments.
471+
min_gas_required = gas_costs.G_CREATE + gas_costs.G_BASE * (3 if opcode == Op.CREATE else 4)
472+
code_prefix = Op.PUSH20(proxy_contract) + Op.PUSH3(min_gas_required)
473+
attack_block = Op.POP(
474+
# DUP7 refers to the PUSH3 above.
475+
# DUP7 refers to the proxy contract address.
476+
Op.CALL(gas=Op.DUP7, address=Op.DUP7)
477+
)
478+
code = code_loop_precompile_call(code_prefix, attack_block, fork)
479+
tx_target = pre.deploy_contract(code=code)
480+
481+
# (**) We deploy the contract that CREATE(2) will attempt to create so any attempt will fail.
482+
if opcode == Op.CREATE2:
483+
addr = compute_create2_address(address=proxy_contract, salt=0, initcode=[])
484+
pre.deploy_contract(address=addr, code=Op.INVALID)
485+
else:
486+
# Heuristic to have an upper bound.
487+
max_contract_count = 2 * env.gas_limit // gas_costs.G_CREATE
488+
for nonce in range(max_contract_count):
489+
addr = compute_create_address(address=proxy_contract, nonce=nonce)
490+
pre.deploy_contract(address=addr, code=Op.INVALID)
491+
492+
tx = Transaction(
493+
to=tx_target,
494+
gas_limit=env.gas_limit,
495+
sender=pre.fund_eoa(),
496+
)
497+
498+
state_test(
499+
env=env,
500+
pre=pre,
501+
post={},
502+
tx=tx,
503+
)

0 commit comments

Comments
 (0)