Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
217 changes: 1 addition & 216 deletions tests/benchmark/compute/instruction/test_account_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
Alloc,
BenchmarkTestFiller,
Block,
BlockchainTestFiller,
Bytecode,
Environment,
ExtCallGenerator,
Fork,
Hash,
Expand All @@ -33,11 +31,6 @@
TestPhaseManager,
Transaction,
While,
compute_create2_address,
)

from tests.benchmark.compute.helpers import (
XOR_TABLE,
)


Expand Down Expand Up @@ -136,215 +129,7 @@ def test_codecopy_benchmark(
)


@pytest.mark.parametrize(
"opcode",
[
Op.EXTCODESIZE,
Op.EXTCODEHASH,
Op.EXTCODECOPY,
],
)
def test_extcode_ops(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
opcode: Op,
env: Environment,
gas_benchmark_value: int,
) -> None:
"""
Benchmark a block execution where a single opcode is executed.
"""
# The attack gas limit is the gas limit which the target tx will use The
# test will scale the block gas limit to setup the contracts accordingly to
# be able to pay for the contract deposit. This has to take into account
# the 200 gas per byte, but also the quadratic memory expansion costs which
# have to be paid each time the memory is being setup
attack_gas_limit = gas_benchmark_value
max_contract_size = fork.max_code_size()

gas_costs = fork.gas_costs()

# Calculate the absolute minimum gas costs to deploy the contract This does
# not take into account setting up the actual memory (using KECCAK256 and
# XOR) so the actual costs of deploying the contract is higher
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
memory_gas_minimum = memory_expansion_gas_calculator(
new_bytes=len(bytes(max_contract_size))
)
code_deposit_gas_minimum = (
fork.gas_costs().G_CODE_DEPOSIT_BYTE * max_contract_size
+ memory_gas_minimum
)

intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
# Calculate the loop cost of the attacker to query one address
loop_cost = (
gas_costs.G_KECCAK_256 # KECCAK static cost
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic
# cost for CREATE2
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Opcode cost
+ 30 # ~Gluing opcodes
)
# Calculate the number of contracts to be targeted
num_contracts = (
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
) // loop_cost

# Set the block gas limit to a relative high value to ensure the code
# deposit tx fits in the block (there is enough gas available in the block
# to execute this)
minimum_gas_limit = code_deposit_gas_minimum * 2 * num_contracts
if env.gas_limit < minimum_gas_limit:
raise Exception(
f"`BENCHMARKING_MAX_GAS` ({env.gas_limit}) is no longer enough to"
f" support this test, which requires {minimum_gas_limit} gas for "
"its setup. Update the value or consider optimizing gas usage "
"during the setup phase of this test."
)

# The initcode will take its address as a starting point to the input to
# the keccak hash function. It will reuse the output of the hash function
# in a loop to create a large amount of seemingly random code, until it
# reaches the maximum contract size.
initcode = (
Op.MSTORE(0, Op.ADDRESS)
+ While(
body=(
Op.SHA3(Op.SUB(Op.MSIZE, 32), 32)
# Use a xor table to avoid having to call the "expensive" sha3
# opcode as much
+ sum(
(
Op.PUSH32[xor_value]
+ Op.XOR
+ Op.DUP1
+ Op.MSIZE
+ Op.MSTORE
)
for xor_value in XOR_TABLE
)
+ Op.POP
),
condition=Op.LT(Op.MSIZE, max_contract_size),
)
# Despite the whole contract has random bytecode, we make the first
# opcode be a STOP so CALL-like attacks return as soon as possible,
# while EXTCODE(HASH|SIZE) work as intended.
+ Op.MSTORE8(0, 0x00)
+ Op.RETURN(0, max_contract_size)
)
initcode_address = pre.deploy_contract(code=initcode)

# The factory contract will simply use the initcode that is already
# deployed, and create a new contract and return its address if successful.
factory_code = (
Op.EXTCODECOPY(
address=initcode_address,
dest_offset=0,
offset=0,
size=Op.EXTCODESIZE(initcode_address),
)
+ Op.MSTORE(
0,
Op.CREATE2(
value=0,
offset=0,
size=Op.EXTCODESIZE(initcode_address),
salt=Op.SLOAD(0),
),
)
+ Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
+ Op.RETURN(0, 32)
)
factory_address = pre.deploy_contract(code=factory_code)

# The factory caller will call the factory contract N times, creating N new
# contracts. Calldata should contain the N value.
factory_caller_code = Op.CALLDATALOAD(0) + While(
body=Op.POP(Op.CALL(address=factory_address)),
condition=Op.PUSH1(1)
+ Op.SWAP1
+ Op.SUB
+ Op.DUP1
+ Op.ISZERO
+ Op.ISZERO,
)
factory_caller_address = pre.deploy_contract(code=factory_caller_code)

with TestPhaseManager.setup():
contracts_deployment_tx = Transaction(
to=factory_caller_address,
gas_limit=env.gas_limit,
gas_price=10**6,
data=Hash(num_contracts),
sender=pre.fund_eoa(),
)

post = {}
deployed_contract_addresses = []
for i in range(num_contracts):
deployed_contract_address = compute_create2_address(
address=factory_address,
salt=i,
initcode=initcode,
)
post[deployed_contract_address] = Account(nonce=1)
deployed_contract_addresses.append(deployed_contract_address)

attack_call = Bytecode()
if opcode == Op.EXTCODECOPY:
attack_call = Op.EXTCODECOPY(
address=Op.SHA3(32 - 20 - 1, 85), dest_offset=96, size=1000
)
else:
# For the rest of the opcodes, we can use the same generic attack call
# since all only minimally need the `address` of the target.
attack_call = Op.POP(opcode(address=Op.SHA3(32 - 20 - 1, 85)))
attack_code = (
# Setup memory for later CREATE2 address generation loop.
# 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
Op.MSTORE(0, factory_address)
+ Op.MSTORE8(32 - 20 - 1, 0xFF)
+ Op.MSTORE(32, 0)
+ Op.MSTORE(64, initcode.keccak256())
# Main loop
+ While(
body=attack_call + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
)
)

if len(attack_code) > max_contract_size:
# TODO: A workaround could be to split the opcode code into multiple
# contracts and call them in sequence.
raise ValueError(
f"Code size {len(attack_code)} exceeds maximum "
f"code size {max_contract_size}"
)
opcode_address = pre.deploy_contract(code=attack_code)

with TestPhaseManager.execution():
opcode_tx = Transaction(
to=opcode_address,
gas_limit=attack_gas_limit,
gas_price=10**9,
sender=pre.fund_eoa(),
)

blockchain_test(
pre=pre,
post=post,
blocks=[
Block(txs=[contracts_deployment_tx]),
Block(txs=[opcode_tx]),
],
exclude_full_post_state_in_output=True,
)


@pytest.mark.repricing
@pytest.mark.repricing(copied_size=512)
@pytest.mark.parametrize(
"copy_size",
[0, 32, 256, 512, 1024],
Expand Down
Loading
Loading