Skip to content

Commit f2b2b9f

Browse files
committed
zkevm: add selfdestruct existing contracts bench
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
1 parent 9cbb5b8 commit f2b2b9f

File tree

1 file changed

+139
-4
lines changed

1 file changed

+139
-4
lines changed

tests/zkevm/test_worst_stateful_opcodes.py

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
Tests running worst-case stateful opcodes for zkEVMs.
66
"""
77

8+
import math
9+
810
import pytest
911

1012
from ethereum_test_forks import Fork
@@ -16,17 +18,19 @@
1618
BlockchainTestFiller,
1719
Bytecode,
1820
Environment,
21+
Hash,
1922
StateTestFiller,
2023
Transaction,
2124
While,
25+
compute_create2_address,
2226
compute_create_address,
2327
)
2428
from ethereum_test_tools.vm.opcode import Opcodes as Op
2529

2630
REFERENCE_SPEC_GIT_PATH = "TODO"
2731
REFERENCE_SPEC_VERSION = "TODO"
2832

29-
MAX_CODE_SIZE = 24 * 1024
33+
MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property
3034

3135

3236
@pytest.mark.valid_from("Cancun")
@@ -157,13 +161,13 @@ def test_worst_address_state_warm(
157161
jumpdest = Op.JUMPDEST
158162
jump_back = Op.JUMP(len(prep))
159163
iter_block = Op.POP(opcode(address=Op.MLOAD(0)))
160-
max_iters_loop = (MAX_CODE_SIZE - len(prep) - len(jumpdest) - len(jump_back)) // len(
164+
max_iters_loop = (MAX_CONTRACT_SIZE - len(prep) - len(jumpdest) - len(jump_back)) // len(
161165
iter_block
162166
)
163167
op_code = prep + jumpdest + sum([iter_block] * max_iters_loop) + jump_back
164-
if len(op_code) > MAX_CODE_SIZE:
168+
if len(op_code) > MAX_CONTRACT_SIZE:
165169
# Must never happen, but keep it as a sanity check.
166-
raise ValueError(f"Code size {len(op_code)} exceeds maximum code size {MAX_CODE_SIZE}")
170+
raise ValueError(f"Code size {len(op_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}")
167171
op_address = pre.deploy_contract(code=op_code)
168172
tx = Transaction(
169173
to=op_address,
@@ -475,3 +479,134 @@ def test_worst_extcodecopy_warm(
475479
post={},
476480
tx=tx,
477481
)
482+
483+
484+
@pytest.mark.valid_from("Cancun")
485+
@pytest.mark.parametrize("value_bearing", [True, False])
486+
def test_worst_selfdestruct_existing(
487+
blockchain_test: BlockchainTestFiller,
488+
fork: Fork,
489+
pre: Alloc,
490+
value_bearing: bool,
491+
):
492+
"""Test running a block with as SELFDESTRUCT calls as possible for existing contracts."""
493+
env = Environment(gas_limit=100_000_000_000)
494+
attack_gas_limit = Environment().gas_limit
495+
496+
# Create an account that will be used as the beneficiary of the SELFDESTRUCT calls, to avoid
497+
# account creation costs. All SELFDESTRUCT calls will target the same account to avoid
498+
# cold costs.
499+
selfdestruct_beneficiary = pre.fund_eoa(amount=100)
500+
501+
# Template code that will be used to deploy a large number of contracts.
502+
selfdestructable_contract_addr = pre.deploy_contract(
503+
code=Op.SELFDESTRUCT(selfdestruct_beneficiary)
504+
)
505+
initcode = Op.EXTCODECOPY(
506+
address=selfdestructable_contract_addr,
507+
dest_offset=0,
508+
offset=0,
509+
size=Op.EXTCODESIZE(selfdestructable_contract_addr),
510+
) + Op.RETURN(0, Op.EXTCODESIZE(selfdestructable_contract_addr))
511+
initcode_address = pre.deploy_contract(code=initcode)
512+
513+
# Create a factory that deployes a new SELFDESTRUCT contract instance pre-funded depending on
514+
# the value_bearing parameter. We use CREATE2 so the caller contract can easily reproduce
515+
# the addresses in a loop for CALLs.
516+
factory_code = (
517+
Op.EXTCODECOPY(
518+
address=initcode_address,
519+
dest_offset=0,
520+
offset=0,
521+
size=Op.EXTCODESIZE(initcode_address),
522+
)
523+
+ Op.MSTORE(
524+
0,
525+
Op.CREATE2(
526+
value=1 if value_bearing else 0,
527+
offset=0,
528+
size=Op.EXTCODESIZE(initcode_address),
529+
salt=Op.SLOAD(0),
530+
),
531+
)
532+
+ Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
533+
+ Op.RETURN(0, 32)
534+
)
535+
factory_address = pre.deploy_contract(code=factory_code, balance=10**18)
536+
537+
factory_caller_code = Op.CALLDATALOAD(0) + While(
538+
body=Op.POP(Op.CALL(address=factory_address)),
539+
condition=Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
540+
)
541+
factory_caller_address = pre.deploy_contract(code=factory_caller_code)
542+
543+
gas_costs = fork.gas_costs()
544+
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
545+
loop_cost = (
546+
gas_costs.G_KECCAK_256 # KECCAK static cost
547+
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
548+
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
549+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # CALL to self-destructing contract
550+
+ gas_costs.G_SELF_DESTRUCT
551+
+ 30 # ~Gluing opcodes
552+
)
553+
num_contracts = (
554+
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
555+
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
556+
) // loop_cost
557+
558+
contracts_deployment_tx = Transaction(
559+
to=factory_caller_address,
560+
gas_limit=env.gas_limit,
561+
gas_price=10**9,
562+
data=Hash(num_contracts),
563+
sender=pre.fund_eoa(),
564+
)
565+
566+
post = {}
567+
deployed_contract_addresses = []
568+
for i in range(num_contracts):
569+
deployed_contract_address = compute_create2_address(
570+
address=factory_address,
571+
salt=i,
572+
initcode=initcode,
573+
)
574+
post[deployed_contract_address] = Account(nonce=1)
575+
deployed_contract_addresses.append(deployed_contract_address)
576+
577+
attack_call = Op.POP(Op.CALL(address=Op.SHA3(32 - 20 - 1, 85)))
578+
attack_code = (
579+
# Setup memory for later CREATE2 address generation loop.
580+
# 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
581+
Op.MSTORE(0, factory_address)
582+
+ Op.MSTORE8(32 - 20 - 1, 0xFF)
583+
+ Op.MSTORE(32, 0)
584+
+ Op.MSTORE(64, initcode.keccak256())
585+
# Main loop
586+
+ While(
587+
body=attack_call + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
588+
# The condition is having enough gas for at least one more iteration.
589+
# 2+2600+5000 is the `attack_call` gas cost, and 2* is a rough safety margin.
590+
condition=Op.GT(Op.GAS, 2 * (2 + 2600 + 5000)),
591+
)
592+
)
593+
assert len(attack_code) <= MAX_CONTRACT_SIZE
594+
595+
attack_code_addr = pre.deploy_contract(code=attack_code)
596+
opcode_tx = Transaction(
597+
to=attack_code_addr,
598+
gas_limit=attack_gas_limit,
599+
gas_price=10**9,
600+
sender=pre.fund_eoa(),
601+
)
602+
603+
blockchain_test(
604+
genesis_environment=env,
605+
pre=pre,
606+
post=post,
607+
blocks=[
608+
Block(txs=[contracts_deployment_tx]),
609+
Block(txs=[opcode_tx]),
610+
],
611+
exclude_full_post_state_in_output=True,
612+
)

0 commit comments

Comments
 (0)