Skip to content

Commit a4739f3

Browse files
jsignjochem-brouwer
authored andcommitted
zkevm: add SELFDESTRUCT coverage (ethereum#1678)
* zkevm: add selfdestruct existing contracts bench Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * zkevm: add selfdestruct of contracts deployed in tx Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * improvements Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * Update tests/zkevm/test_worst_stateful_opcodes.py Co-authored-by: Jochem Brouwer <jochembrouwer96@gmail.com> * improvements Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * adjust gas prices Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * Update tests/zkevm/test_worst_stateful_opcodes.py Co-authored-by: Jochem Brouwer <jochembrouwer96@gmail.com> * improvements Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * improvement Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * fix bug Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * selfdestruct in initcode Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> * feedback Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> --------- Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com> Co-authored-by: Jochem Brouwer <jochembrouwer96@gmail.com>
1 parent eadf1d3 commit a4739f3

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

tests/zkevm/test_worst_stateful_opcodes.py

Lines changed: 231 additions & 0 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,9 +18,11 @@
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
@@ -475,3 +479,230 @@ 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 many SELFDESTRUCTs as possible for existing contracts."""
493+
env = Environment(gas_limit=100_000_000_000)
494+
attack_gas_limit = Environment().gas_limit
495+
pre.fund_address(env.fee_recipient, 1)
496+
497+
# Template code that will be used to deploy a large number of contracts.
498+
selfdestructable_contract_addr = pre.deploy_contract(code=Op.SELFDESTRUCT(Op.COINBASE))
499+
initcode = Op.EXTCODECOPY(
500+
address=selfdestructable_contract_addr,
501+
dest_offset=0,
502+
offset=0,
503+
size=Op.EXTCODESIZE(selfdestructable_contract_addr),
504+
) + Op.RETURN(0, Op.EXTCODESIZE(selfdestructable_contract_addr))
505+
initcode_address = pre.deploy_contract(code=initcode)
506+
507+
# Create a factory that deployes a new SELFDESTRUCT contract instance pre-funded depending on
508+
# the value_bearing parameter. We use CREATE2 so the caller contract can easily reproduce
509+
# the addresses in a loop for CALLs.
510+
factory_code = (
511+
Op.EXTCODECOPY(
512+
address=initcode_address,
513+
dest_offset=0,
514+
offset=0,
515+
size=Op.EXTCODESIZE(initcode_address),
516+
)
517+
+ Op.MSTORE(
518+
0,
519+
Op.CREATE2(
520+
value=1 if value_bearing else 0,
521+
offset=0,
522+
size=Op.EXTCODESIZE(initcode_address),
523+
salt=Op.SLOAD(0),
524+
),
525+
)
526+
+ Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
527+
+ Op.RETURN(0, 32)
528+
)
529+
factory_address = pre.deploy_contract(code=factory_code, balance=10**18)
530+
531+
factory_caller_code = Op.CALLDATALOAD(0) + While(
532+
body=Op.POP(Op.CALL(address=factory_address)),
533+
condition=Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
534+
)
535+
factory_caller_address = pre.deploy_contract(code=factory_caller_code)
536+
537+
gas_costs = fork.gas_costs()
538+
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
539+
loop_cost = (
540+
gas_costs.G_KECCAK_256 # KECCAK static cost
541+
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
542+
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
543+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # CALL to self-destructing contract
544+
+ gas_costs.G_SELF_DESTRUCT
545+
+ 30 # ~Gluing opcodes
546+
)
547+
num_contracts = (
548+
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
549+
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
550+
) // loop_cost
551+
552+
contracts_deployment_tx = Transaction(
553+
to=factory_caller_address,
554+
gas_limit=env.gas_limit,
555+
gas_price=10**9,
556+
data=Hash(num_contracts),
557+
sender=pre.fund_eoa(),
558+
)
559+
560+
code = (
561+
# Setup memory for later CREATE2 address generation loop.
562+
# 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
563+
Op.MSTORE(0, factory_address)
564+
+ Op.MSTORE8(32 - 20 - 1, 0xFF)
565+
+ Op.MSTORE(32, 0)
566+
+ Op.MSTORE(64, initcode.keccak256())
567+
# Main loop
568+
+ While(
569+
body=Op.POP(Op.CALL(address=Op.SHA3(32 - 20 - 1, 85)))
570+
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
571+
# Stop before we run out of gas for the whole tx execution.
572+
# The value was found by trial-error rounded to the next 1000 multiple.
573+
condition=Op.GT(Op.GAS, 12_000),
574+
)
575+
+ Op.SSTORE(0, 42) # Done for successful tx execution assertion below.
576+
)
577+
assert len(code) <= fork.max_code_size()
578+
579+
# The 0 storage slot is initialize to avoid creation costs in SSTORE above.
580+
code_addr = pre.deploy_contract(code=code, storage={0: 1})
581+
opcode_tx = Transaction(
582+
to=code_addr,
583+
gas_limit=attack_gas_limit,
584+
gas_price=10,
585+
sender=pre.fund_eoa(),
586+
)
587+
588+
post = {
589+
code_addr: Account(storage={0: 42}) # Check for successful execution.
590+
}
591+
deployed_contract_addresses = []
592+
for i in range(num_contracts):
593+
deployed_contract_address = compute_create2_address(
594+
address=factory_address,
595+
salt=i,
596+
initcode=initcode,
597+
)
598+
post[deployed_contract_address] = Account(nonce=1)
599+
deployed_contract_addresses.append(deployed_contract_address)
600+
601+
blockchain_test(
602+
genesis_environment=env,
603+
pre=pre,
604+
post=post,
605+
blocks=[
606+
Block(txs=[contracts_deployment_tx]),
607+
Block(txs=[opcode_tx]),
608+
],
609+
exclude_full_post_state_in_output=True,
610+
)
611+
612+
613+
@pytest.mark.valid_from("Cancun")
614+
@pytest.mark.parametrize("value_bearing", [True, False])
615+
def test_worst_selfdestruct_created(
616+
state_test: StateTestFiller,
617+
pre: Alloc,
618+
value_bearing: bool,
619+
):
620+
"""
621+
Test running a block with as many SELFDESTRUCTs as possible for deployed contracts in
622+
the same transaction.
623+
"""
624+
env = Environment()
625+
pre.fund_address(env.fee_recipient, 1)
626+
627+
# SELFDESTRUCT(COINBASE) contract deployment
628+
initcode = Op.MSTORE8(0, 0x41) + Op.MSTORE8(1, 0xFF) + Op.RETURN(0, 2)
629+
code = (
630+
Op.MSTORE(0, initcode.hex())
631+
+ While(
632+
body=Op.POP(
633+
Op.CALL(
634+
address=Op.CREATE(
635+
value=1 if value_bearing else 0,
636+
offset=32 - len(initcode),
637+
size=len(initcode),
638+
)
639+
)
640+
),
641+
# Stop before we run out of gas for the whole tx execution.
642+
# The value was found by trial-error rounded to the next 1000 multiple.
643+
condition=Op.GT(Op.GAS, 10_000),
644+
)
645+
+ Op.SSTORE(0, 42) # Done for successful tx execution assertion below.
646+
)
647+
# The 0 storage slot is initialize to avoid creation costs in SSTORE above.
648+
code_addr = pre.deploy_contract(code=code, balance=100_000, storage={0: 1})
649+
code_tx = Transaction(
650+
to=code_addr,
651+
gas_limit=env.gas_limit,
652+
gas_price=10,
653+
sender=pre.fund_eoa(),
654+
)
655+
656+
post = {code_addr: Account(storage={0: 42})} # Check for successful execution.
657+
state_test(
658+
env=env,
659+
pre=pre,
660+
post=post,
661+
tx=code_tx,
662+
)
663+
664+
665+
@pytest.mark.valid_from("Cancun")
666+
@pytest.mark.parametrize("value_bearing", [True, False])
667+
def test_worst_selfdestruct_initcode(
668+
state_test: StateTestFiller,
669+
pre: Alloc,
670+
value_bearing: bool,
671+
):
672+
"""Test running a block with as many SELFDESTRUCTs as possible executed in initcode."""
673+
env = Environment()
674+
pre.fund_address(env.fee_recipient, 1)
675+
676+
initcode = Op.SELFDESTRUCT(Op.COINBASE)
677+
code = (
678+
Op.MSTORE(0, initcode.hex())
679+
+ While(
680+
body=Op.POP(
681+
Op.CREATE(
682+
value=1 if value_bearing else 0,
683+
offset=32 - len(initcode),
684+
size=len(initcode),
685+
)
686+
),
687+
# Stop before we run out of gas for the whole tx execution.
688+
# The value was found by trial-error rounded to the next 1000 multiple.
689+
condition=Op.GT(Op.GAS, 12_000),
690+
)
691+
+ Op.SSTORE(0, 42) # Done for successful tx execution assertion below.
692+
)
693+
# The 0 storage slot is initialize to avoid creation costs in SSTORE above.
694+
code_addr = pre.deploy_contract(code=code, balance=100_000, storage={0: 1})
695+
code_tx = Transaction(
696+
to=code_addr,
697+
gas_limit=env.gas_limit,
698+
gas_price=10,
699+
sender=pre.fund_eoa(),
700+
)
701+
702+
post = {code_addr: Account(storage={0: 42})} # Check for successful execution.
703+
state_test(
704+
env=env,
705+
pre=pre,
706+
post=post,
707+
tx=code_tx,
708+
)

0 commit comments

Comments
 (0)