-
Notifications
You must be signed in to change notification settings - Fork 151
test(zkevm): add coverage for CALLDATACOPY, CODECOPY, RETURNDATACOPY and MCOPY #1800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jsign
wants to merge
21
commits into
main
Choose a base branch
from
jsign-mem-opcodes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+335
−0
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ea69be1
zkevm: add CALLDATACOPY test
jsign be16b5d
add parameter naming
jsign ca97c66
zkevm: add CODECOPY test
jsign bab9b13
benchmarks: add RETURNDATACOPY test
jsign e174384
benchmarks: add MCOPY test
jsign 1d4e5f1
nits
jsign b662bf8
nit
jsign b1adec3
use helper func
jsign 9ae729e
add missing parameter value
jsign 2b042ea
Apply suggestions from code review
jsign 06e4135
add comment
jsign 640b687
nit
jsign 6b8e2da
Update tests/zkevm/test_worst_memory.py
jsign e5914a2
nit
jsign bcc2555
add src non fixed targets
jsign 13e17dd
accept non-zero data in calldatacopy
jsign 0644cf2
refresh returned data in returndatacopy bench
jsign 0ac739c
improve mcopy
jsign 815d7d9
returndatacopy cannot be out of bounds
jsign 297fcd1
nit
jsign a983e60
nit
jsign File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
""" | ||
abstract: Tests worst-case scenarios for memory related opcodes. | ||
|
||
Tests running worst-case memory related opcodes. | ||
""" | ||
|
||
import pytest | ||
|
||
from ethereum_test_base_types.base_types import Bytes | ||
from ethereum_test_forks import Fork | ||
from ethereum_test_tools import ( | ||
Alloc, | ||
Bytecode, | ||
Environment, | ||
StateTestFiller, | ||
Transaction, | ||
) | ||
from ethereum_test_tools.vm.opcode import Opcodes as Op | ||
|
||
from .helpers import code_loop_precompile_call | ||
|
||
REFERENCE_SPEC_GIT_PATH = "TODO" | ||
REFERENCE_SPEC_VERSION = "TODO" | ||
|
||
|
||
class CallDataOrigin: | ||
"""Enum for calldata origins.""" | ||
|
||
TRANSACTION = 1 | ||
CALL = 2 | ||
|
||
|
||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.parametrize( | ||
"origin", | ||
[ | ||
pytest.param(CallDataOrigin.TRANSACTION, id="transaction"), | ||
pytest.param(CallDataOrigin.CALL, id="call"), | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"size", | ||
[ | ||
pytest.param(0, id="0 bytes"), | ||
pytest.param(10, id="10 bytes"), | ||
pytest.param(100, id="100 bytes"), | ||
pytest.param(1 * 1024, id="1KiB"), | ||
pytest.param(10 * 1024, id="10KiB"), | ||
pytest.param(100 * 1024, id="100KiB"), | ||
pytest.param(1024 * 1024, id="1MiB"), | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"fixed_src_dst", | ||
[ | ||
True, | ||
False, | ||
], | ||
) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@pytest.mark.parametrize( | ||
"non_zero_data", | ||
[ | ||
True, | ||
False, | ||
], | ||
) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def test_worst_calldatacopy( | ||
state_test: StateTestFiller, | ||
pre: Alloc, | ||
fork: Fork, | ||
origin: CallDataOrigin, | ||
size: int, | ||
fixed_src_dst: bool, | ||
non_zero_data: bool, | ||
): | ||
"""Test running a block filled with CALLDATACOPY executions.""" | ||
env = Environment() | ||
|
||
if size == 0 and non_zero_data: | ||
pytest.skip("Non-zero data with size 0 is not applicable.") | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# We create the contract that will be doing the CALLDATACOPY multiple times. | ||
# | ||
# If `non_zero_data` is True, we leverage CALLDATASIZE for the copy length. Otherwise, since we | ||
# don't send zero data explicitly via calldata, PUSH the target size and use DUP1 to copy it. | ||
prefix = Bytecode() if non_zero_data or size == 0 else Op.PUSH3(size) | ||
src_dst = 0 if fixed_src_dst else Op.MOD(Op.GAS, 7) | ||
attack_block = Op.CALLDATACOPY( | ||
src_dst, src_dst, Op.CALLDATASIZE if non_zero_data or size == 0 else Op.DUP1 | ||
) | ||
code = code_loop_precompile_call(prefix, attack_block, fork) | ||
code_address = pre.deploy_contract(code=code) | ||
|
||
tx_target = code_address | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# If the origin is CALL, we need to create a contract that will call the target contract with | ||
# the calldata. | ||
if origin == CallDataOrigin.CALL: | ||
# If `non_zero_data` is False we leverage just using zeroed memory. Otherwise, we | ||
# copy the calldata received from the transaction. | ||
prefix = ( | ||
Op.CALLDATACOPY(Op.PUSH0, Op.PUSH0, Op.CALLDATASIZE) if non_zero_data else Bytecode() | ||
) | ||
arg_size = Op.CALLDATASIZE if non_zero_data else size | ||
code = prefix + Op.STATICCALL( | ||
address=code_address, args_offset=Op.PUSH0, args_size=arg_size | ||
) | ||
tx_target = pre.deploy_contract(code=code) | ||
|
||
# If `non_zero_data` is True, we fill the calldata with deterministic random data. | ||
# Note that if `size == 0` and `non_zero_data` is a skipped case. | ||
data = Bytes([i % 256 for i in range(size)]) if non_zero_data else Bytes() | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
tx = Transaction( | ||
to=tx_target, | ||
gas_limit=env.gas_limit, | ||
data=data, | ||
sender=pre.fund_eoa(), | ||
) | ||
|
||
state_test( | ||
genesis_environment=env, | ||
pre=pre, | ||
post={}, | ||
tx=tx, | ||
) | ||
|
||
|
||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.parametrize( | ||
"max_code_size_ratio", | ||
[ | ||
pytest.param(0, id="0 bytes"), | ||
pytest.param(0.25, id="0.25x max code size"), | ||
pytest.param(0.50, id="0.50x max code size"), | ||
pytest.param(0.75, id="0.75x max code size"), | ||
pytest.param(1.00, id="max code size"), | ||
], | ||
) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@pytest.mark.parametrize( | ||
"fixed_src_dst", | ||
[ | ||
True, | ||
False, | ||
], | ||
) | ||
def test_worst_codecopy( | ||
state_test: StateTestFiller, | ||
pre: Alloc, | ||
fork: Fork, | ||
max_code_size_ratio: float, | ||
fixed_src_dst: bool, | ||
): | ||
"""Test running a block filled with CODECOPY executions.""" | ||
env = Environment() | ||
max_code_size = fork.max_code_size() | ||
|
||
size = int(max_code_size * max_code_size_ratio) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
code_prefix = Op.PUSH32(size) | ||
src_dst = 0 if fixed_src_dst else Op.MOD(Op.GAS, 7) | ||
attack_block = Op.CODECOPY(src_dst, src_dst, Op.DUP1) # DUP1 copies size. | ||
code = code_loop_precompile_call(code_prefix, attack_block, fork) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# The code generated above is not guaranteed to be of max_code_size, so we pad it since | ||
# a test parameter targets CODECOPYing a contract with max code size. Padded bytecode values | ||
# are not relevant. | ||
code = code + Op.INVALID * (max_code_size - len(code)) | ||
assert len(code) == max_code_size, ( | ||
f"Code size {len(code)} is not equal to max code size {max_code_size}." | ||
) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
tx = Transaction( | ||
to=pre.deploy_contract(code=code), | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
gas_limit=env.gas_limit, | ||
sender=pre.fund_eoa(), | ||
) | ||
|
||
state_test( | ||
genesis_environment=env, | ||
pre=pre, | ||
post={}, | ||
tx=tx, | ||
) | ||
|
||
|
||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.parametrize( | ||
"size", | ||
[ | ||
pytest.param(0, id="0 bytes"), | ||
pytest.param(10, id="10 bytes"), | ||
pytest.param(100, id="100 bytes"), | ||
pytest.param(1 * 1024, id="1KiB"), | ||
pytest.param(10 * 1024, id="10KiB"), | ||
pytest.param(100 * 1024, id="100KiB"), | ||
pytest.param(1024 * 1024, id="1MiB"), | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"fixed_dst", | ||
[ | ||
True, | ||
False, | ||
], | ||
) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def test_worst_returndatacopy( | ||
state_test: StateTestFiller, | ||
pre: Alloc, | ||
fork: Fork, | ||
size: int, | ||
fixed_dst: bool, | ||
): | ||
"""Test running a block filled with RETURNDATACOPY executions.""" | ||
env = Environment() | ||
max_code_size = fork.max_code_size() | ||
|
||
# Create the contract that will RETURN the data that will be used for RETURNDATACOPY. | ||
# Random-ish data is injected at different points in memory to avoid making the content | ||
# predictable. If `size` is 0, this helper contract won't be used. | ||
code = ( | ||
Op.MSTORE8(0, Op.GAS) | ||
+ Op.MSTORE8(size // 2, Op.GAS) | ||
+ Op.MSTORE8(size - 1, Op.GAS) | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
+ Op.RETURN(0, size) | ||
) | ||
helper_contract = pre.deploy_contract(code=code) | ||
|
||
# We create the contract that will be doing the RETURNDATACOPY multiple times. | ||
returndata_gen = Op.STATICCALL(address=helper_contract) if size > 0 else Bytecode() | ||
dst = 0 if fixed_dst else Op.MOD(Op.GAS, 7) | ||
attack_iter = Op.RETURNDATACOPY(dst, Op.PUSH0, Op.RETURNDATASIZE) | ||
|
||
jumpdest = Op.JUMPDEST | ||
jump_back = Op.JUMP(len(returndata_gen)) | ||
# The attack loop is constructed as: | ||
# ``` | ||
# JUMPDEST(#) | ||
# RETURNDATACOPY(...) | ||
# RETURNDATACOPY(...) | ||
# ... | ||
# STATICCALL(address=helper_contract) | ||
# JUMP(#) | ||
# ``` | ||
# The goal is that once per (big) loop iteration, the helper contract is called to | ||
# generate fresh returndata to continue calling RETURNDATACOPY. | ||
max_iters_loop = ( | ||
max_code_size - 2 * len(returndata_gen) - len(jumpdest) - len(jump_back) | ||
) // len(attack_iter) | ||
code = ( | ||
returndata_gen | ||
+ jumpdest | ||
+ sum([attack_iter] * max_iters_loop) | ||
+ returndata_gen | ||
+ jump_back | ||
) | ||
assert len(code) <= max_code_size, ( | ||
f"Code size {len(code)} is not equal to max code size {max_code_size}." | ||
) | ||
|
||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tx = Transaction( | ||
to=pre.deploy_contract(code=code), | ||
gas_limit=env.gas_limit, | ||
sender=pre.fund_eoa(), | ||
) | ||
|
||
state_test( | ||
genesis_environment=env, | ||
pre=pre, | ||
post={}, | ||
tx=tx, | ||
) | ||
|
||
|
||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.parametrize( | ||
"size", | ||
[ | ||
pytest.param(0, id="0 bytes"), | ||
pytest.param(10, id="10 bytes"), | ||
pytest.param(100, id="100 bytes"), | ||
pytest.param(1 * 1024, id="1KiB"), | ||
pytest.param(10 * 1024, id="10KiB"), | ||
pytest.param(100 * 1024, id="100KiB"), | ||
pytest.param(1024 * 1024, id="1MiB"), | ||
], | ||
) | ||
@pytest.mark.parametrize( | ||
"fixed_src_dst", | ||
[ | ||
True, | ||
False, | ||
], | ||
) | ||
def test_worst_mcopy( | ||
jsign marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state_test: StateTestFiller, | ||
pre: Alloc, | ||
fork: Fork, | ||
size: int, | ||
fixed_src_dst: bool, | ||
): | ||
"""Test running a block filled with MCOPY executions.""" | ||
env = Environment() | ||
max_code_size = fork.max_code_size() | ||
|
||
mem_touch = ( | ||
Op.MSTORE8(0, Op.GAS) + Op.MSTORE8(size // 2, Op.GAS) + Op.MSTORE8(size - 1, Op.GAS) | ||
if size > 0 | ||
else Bytecode() | ||
) | ||
src_dst = 0 if fixed_src_dst else Op.MOD(Op.GAS, 7) | ||
attack_block = Op.MCOPY(src_dst, src_dst, size) | ||
|
||
jumpdest = Op.JUMPDEST | ||
jump_back = Op.JUMP(len(mem_touch)) | ||
max_iters_loop = (max_code_size - 2 * len(mem_touch) - len(jumpdest) - len(jump_back)) // len( | ||
attack_block | ||
) | ||
code = mem_touch + jumpdest + sum([attack_block] * max_iters_loop) + mem_touch + jump_back | ||
assert len(code) <= max_code_size, ( | ||
f"Code size {len(code)} is not equal to max code size {max_code_size}." | ||
) | ||
|
||
tx = Transaction( | ||
to=pre.deploy_contract(code=code), | ||
gas_limit=env.gas_limit, | ||
sender=pre.fund_eoa(), | ||
) | ||
|
||
state_test( | ||
genesis_environment=env, | ||
pre=pre, | ||
post={}, | ||
tx=tx, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.