Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
class JumpLoopGenerator(BenchmarkCodeGenerator):
"""Generates bytecode that loops execution using JUMP operations."""

contract_balance: int = 0

def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
"""Deploy the looping contract."""
# Benchmark Test Structure:
Expand All @@ -28,7 +30,9 @@ def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
cleanup=self.cleanup,
fork=fork,
)
self._contract_address = pre.deploy_contract(code=code)
self._contract_address = pre.deploy_contract(
code=code, balance=self.contract_balance
)
return self._contract_address


Expand All @@ -49,20 +53,36 @@ def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
# but not loop (e.g. PUSH)
# 2. The loop contract that calls the target contract in a loop

pushed_stack_items = self.attack_block.pushed_stack_items
popped_stack_items = self.attack_block.popped_stack_items
stack_delta = pushed_stack_items - popped_stack_items
attack_block_stack_delta = (
self.attack_block.pushed_stack_items
- self.attack_block.popped_stack_items
)
assert attack_block_stack_delta >= 0, (
"attack block stack delta must be non-negative"
)

setup_stack_delta = (
self.setup.pushed_stack_items - self.setup.popped_stack_items
)
assert setup_stack_delta >= 0, "setup stack delta must be non-negative"

max_iterations = fork.max_code_size() // len(self.attack_block)
max_stack_height = fork.max_stack_height() - setup_stack_delta

if stack_delta > 0:
if attack_block_stack_delta > 0:
max_iterations = min(
fork.max_stack_height() // stack_delta, max_iterations
max_stack_height // attack_block_stack_delta, max_iterations
)

code = self.setup + self.attack_block * max_iterations
# Pad the code to the maximum code size.
code += Op.STOP * (fork.max_code_size() - len(code))

self._validate_code_size(code, fork)

# Deploy target contract that contains the actual attack block
self._target_contract_address = pre.deploy_contract(
code=self.setup + self.attack_block * max_iterations,
code=code,
balance=self.contract_balance,
)

Expand All @@ -74,11 +94,22 @@ def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
# setup + JUMPDEST + attack + attack + ... + attack +
# JUMP(setup_length)
code_sequence = Op.POP(
Op.STATICCALL(Op.GAS, self._target_contract_address, 0, 0, 0, 0)
Op.STATICCALL(
Op.GAS,
self._target_contract_address,
Op.PUSH0,
Op.CALLDATASIZE,
Op.PUSH0,
Op.PUSH0,
)
)

caller_code = self.generate_repeated_code(
repeated_code=code_sequence, cleanup=self.cleanup, fork=fork
setup=Op.CALLDATACOPY(Op.PUSH0, Op.PUSH0, Op.CALLDATASIZE),
repeated_code=code_sequence,
cleanup=self.cleanup,
fork=fork,
)

self._contract_address = pre.deploy_contract(code=caller_code)
return self._contract_address
2 changes: 2 additions & 0 deletions packages/testing/src/execution_testing/specs/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ def generate_repeated_code(
# TODO: Unify the PUSH0 and PUSH1 usage.
code = setup + Op.JUMPDEST + repeated_code * max_iterations + cleanup
code += Op.JUMP(len(setup)) if len(setup) > 0 else Op.PUSH0 + Op.JUMP
# Pad the code to the maximum code size.
code += Op.STOP * (max_code_size - len(code))
self._validate_code_size(code, fork)

return code
Expand Down
72 changes: 43 additions & 29 deletions tests/benchmark/compute/instruction/test_account_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import math
from typing import Dict

import pytest
from execution_testing import (
Expand All @@ -24,6 +25,7 @@
While,
compute_create2_address,
)
from execution_testing.base_types.conversions import NumberConvertible

from tests.benchmark.compute.helpers import (
XOR_TABLE,
Expand Down Expand Up @@ -72,7 +74,6 @@ def test_codesize(
)
def test_codecopy(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
fork: Fork,
max_code_size_ratio: float,
fixed_src_dst: bool,
Expand All @@ -86,26 +87,12 @@ def test_codecopy(
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 = JumpLoopGenerator(
setup=setup, attack_block=attack_block
).generate_repeated_code(
repeated_code=attack_block, setup=setup, fork=fork
)

# Pad the generated code to ensure the contract size matches the maximum
# The content of the padding bytes is arbitrary.
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}."
)

tx = Transaction(
to=pre.deploy_contract(code=code),
sender=pre.fund_eoa(),
benchmark_test(
code_generator=JumpLoopGenerator(
setup=setup, attack_block=attack_block
)
)

benchmark_test(tx=tx)


@pytest.mark.parametrize(
"opcode",
Expand Down Expand Up @@ -361,7 +348,21 @@ def test_extcodecopy_warm(
],
)
@pytest.mark.parametrize(
"absent_target",
"empty_account",
[
True,
False,
],
)
@pytest.mark.parametrize(
"initial_balance",
[
True,
False,
],
)
@pytest.mark.parametrize(
"initial_storage",
[
True,
False,
Expand All @@ -371,27 +372,40 @@ def test_ext_account_query_warm(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
opcode: Op,
absent_target: bool,
empty_account: bool,
initial_balance: bool,
initial_storage: bool,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new test scenario for gas repricing effort:

  • Accessing empty / non-empty account
  • Accessing account contains zero / non-zero balance
  • Accessing account contains zero / non-zero storage

) -> None:
"""
Test running a block with as many stateful opcodes doing warm access
for an account.
"""
# Setup
target_addr = pre.empty_account()
target_addr = pre.empty_account() if initial_balance else pre.fund_eoa()
post = {}
if not absent_target:
if not empty_account:
code = Op.STOP + Op.JUMPDEST * 100
target_addr = pre.deploy_contract(balance=100, code=code)
post[target_addr] = Account(balance=100, code=code)

# Execution
setup = Op.MSTORE(0, target_addr)
attack_block = Op.POP(opcode(address=Op.MLOAD(0)))
storage: Dict[NumberConvertible, NumberConvertible] = (
{0: 0x1337} if initial_storage else {0: 0}
)
target_addr = pre.deploy_contract(
balance=initial_balance,
code=code,
storage=storage,
)

post[target_addr] = Account(
balance=initial_balance,
code=code,
storage=storage,
)

benchmark_test(
post=post,
code_generator=JumpLoopGenerator(
setup=setup, attack_block=attack_block
setup=Op.MSTORE(0, target_addr),
attack_block=Op.POP(opcode(address=Op.MLOAD(0))),
),
)

Expand Down
17 changes: 16 additions & 1 deletion tests/benchmark/compute/instruction/test_block_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,29 @@ def test_block_context_ops(
)


@pytest.mark.parametrize(
"index",
[
0,
1,
256,
257,
None,
],
)
def test_blockhash(
benchmark_test: BenchmarkTestFiller,
index: int | None,
) -> None:
"""Benchmark BLOCKHASH instruction accessing oldest allowed block."""
# Create 256 dummy blocks to fill the blockhash window.
blocks = [Block()] * 256

block_number = Op.AND(Op.GAS, 0xFF) if index is None else index
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding extra BLOCKHASH operation for benchmarking, as it was one of the slowest operations.
Cases:

  • Valid block index
  • Invalid block index
  • Dynamic block index


benchmark_test(
setup_blocks=blocks,
code_generator=ExtCallGenerator(attack_block=Op.BLOCKHASH(1)),
code_generator=ExtCallGenerator(
attack_block=Op.BLOCKHASH(block_number)
),
)
10 changes: 2 additions & 8 deletions tests/benchmark/compute/instruction/test_call_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def test_calldatacopy(
size: int,
fixed_src_dst: bool,
non_zero_data: bool,
gas_benchmark_value: int,
tx_gas_limit: int,
) -> None:
"""Benchmark CALLDATACOPY instruction."""
if size == 0 and non_zero_data:
Expand All @@ -162,7 +162,7 @@ def test_calldatacopy(

intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
min_gas = intrinsic_gas_calculator(calldata=data)
if min_gas > gas_benchmark_value:
if min_gas > tx_gas_limit:
pytest.skip(
"Minimum gas required for calldata ({min_gas}) is greater "
"than the gas limit"
Expand Down Expand Up @@ -210,7 +210,6 @@ def test_calldatacopy(

tx = Transaction(
to=tx_target,
gas_limit=gas_benchmark_value,
data=data,
sender=pre.fund_eoa(),
)
Expand Down Expand Up @@ -311,11 +310,6 @@ def test_returndatacopy(
)
dst = 0 if fixed_dst else Op.MOD(Op.GAS, 7)

# We create the contract that will be doing the RETURNDATACOPY multiple
# times.
returndata_gen = (
Op.STATICCALL(address=helper_contract) if size > 0 else Bytecode()
)
attack_block = Op.RETURNDATACOPY(dst, Op.PUSH0, Op.RETURNDATASIZE)

benchmark_test(
Expand Down
9 changes: 9 additions & 0 deletions tests/benchmark/compute/instruction/test_control_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ def test_gas_op(
)


def test_pc_op(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We miss the benchmark for PC opcode

benchmark_test: BenchmarkTestFiller,
) -> None:
"""Benchmark PC instruction."""
benchmark_test(
code_generator=ExtCallGenerator(attack_block=Op.PC),
)


def test_jumps(
benchmark_test: BenchmarkTestFiller,
pre: Alloc,
Expand Down
28 changes: 23 additions & 5 deletions tests/benchmark/compute/instruction/test_keccak.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import math

import pytest
from execution_testing import (
BenchmarkTestFiller,
Fork,
Expand All @@ -15,15 +16,15 @@
KECCAK_RATE = 136


def test_keccak(
def test_keccak_max_permutations(
benchmark_test: BenchmarkTestFiller,
fork: Fork,
gas_benchmark_value: int,
tx_gas_limit: int,
) -> None:
"""Benchmark KECCAK256 instruction."""
"""Benchmark KECCAK256 instruction to maximize permutations per block."""
# Intrinsic gas cost is paid once.
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
available_gas = gas_benchmark_value - intrinsic_gas_calculator()
available_gas = tx_gas_limit - intrinsic_gas_calculator()

gsc = fork.gas_costs()
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
Expand Down Expand Up @@ -60,7 +61,24 @@ def test_keccak(

benchmark_test(
code_generator=JumpLoopGenerator(
setup=Op.PUSH20[optimal_input_length],
setup=Op.PUSH20(optimal_input_length),
attack_block=Op.POP(Op.SHA3(Op.PUSH0, Op.DUP1)),
),
)


@pytest.mark.parametrize("mem_alloc", [b"", b"ff", b"ff" * 32])
@pytest.mark.parametrize("offset", [0, 31, 1024])
def test_keccak(
Comment on lines +70 to +72
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of finding the optimal length, add a new case that parametrized initial memory layout and access offset. Request by gas repricing effort.

benchmark_test: BenchmarkTestFiller,
offset: int,
mem_alloc: bytes,
) -> None:
"""Benchmark KECCAK256 instruction with diff input data and offsets."""
benchmark_test(
code_generator=JumpLoopGenerator(
setup=Op.CALLDATACOPY(offset, Op.PUSH0, Op.CALLDATASIZE),
attack_block=Op.POP(Op.SHA3(offset, Op.CALLDATASIZE)),
tx_kwargs={"data": mem_alloc},
),
)
10 changes: 3 additions & 7 deletions tests/benchmark/compute/instruction/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,10 @@ def test_msize(
benchmark_test: BenchmarkTestFiller,
mem_size: int,
) -> None:
"""
Benchmark MSIZE instruction.

- mem_size: by how much the memory is expanded.
"""
"""Benchmark MSIZE instruction."""
benchmark_test(
code_generator=ExtCallGenerator(
setup=Op.MLOAD(Op.SELFBALANCE) + Op.POP,
setup=Op.POP(Op.MLOAD(Op.SELFBALANCE)),
attack_block=Op.MSIZE,
contract_balance=mem_size,
),
Expand Down Expand Up @@ -99,6 +95,6 @@ def test_mcopy(
)
benchmark_test(
code_generator=JumpLoopGenerator(
setup=mem_touch, attack_block=attack_block, cleanup=mem_touch
attack_block=attack_block, cleanup=mem_touch
),
)
Loading
Loading