|
24 | 24 | compute_create2_address,
|
25 | 25 | )
|
26 | 26 | 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 |
27 | 29 |
|
28 | 30 | REFERENCE_SPEC_GIT_PATH = "TODO"
|
29 | 31 | REFERENCE_SPEC_VERSION = "TODO"
|
@@ -315,3 +317,187 @@ def test_worst_initcode_jumpdest_analysis(
|
315 | 317 | post={},
|
316 | 318 | tx=tx,
|
317 | 319 | )
|
| 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