Skip to content
Merged
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
204 changes: 178 additions & 26 deletions tests/benchmark/stateful/bloatnet/test_multi_opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@
# 4. Attack rapidly accesses all contracts, stressing client's state handling


@pytest.mark.parametrize(
"balance_first",
[True, False],
ids=["balance_extcodesize", "extcodesize_balance"],
)
@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodesize(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
balance_first: bool,
):
"""
BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
Expand All @@ -63,7 +69,7 @@ def test_bloatnet_balance_extcodesize(
This test:
1. Assumes contracts are already deployed via the factory (salt 0 to N-1)
2. Generates CREATE2 addresses dynamically during execution
3. Calls BALANCE (cold) then EXTCODESIZE (warm) on each
3. Calls BALANCE and EXTCODESIZE (order controlled by balance_first param)
4. Maximizes cache eviction by accessing many contracts
"""
gas_costs = fork.gas_costs()
Expand All @@ -75,11 +81,11 @@ def test_bloatnet_balance_extcodesize(
cost_per_contract = (
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
+ gas_costs.G_BASE # POP balance (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100)
+ gas_costs.G_BASE # POP code size (2)
+ gas_costs.G_BASE # DUP1 before BALANCE (3)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
+ gas_costs.G_BASE # POP first result (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100)
+ gas_costs.G_BASE # POP second result (2)
+ gas_costs.G_BASE # DUP1 before first op (3)
+ gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
+ gas_costs.G_LOW # MLOAD for salt (3)
+ gas_costs.G_VERY_LOW # ADD for increment (3)
Expand Down Expand Up @@ -108,6 +114,13 @@ def test_bloatnet_balance_extcodesize(
f"Factory storage will be checked during execution."
)

# Define operations that differ based on parameter
balance_op = Op.POP(Op.BALANCE)
extcodesize_op = Op.POP(Op.EXTCODESIZE)
benchmark_ops = (
(balance_op + extcodesize_op) if balance_first else (extcodesize_op + balance_op)
)

# Build attack contract that reads config from factory and performs attack
attack_code = (
# Call getConfig() on factory to get num_deployed and init_code_hash
Expand Down Expand Up @@ -143,9 +156,8 @@ def test_bloatnet_balance_extcodesize(
# Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash)
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
# The address is now on the stack
+ Op.DUP1 # Duplicate for EXTCODESIZE
+ Op.POP(Op.BALANCE) # Cold access
+ Op.POP(Op.EXTCODESIZE) # Warm access
+ Op.DUP1 # Duplicate for second operation
+ benchmark_ops # Execute operations in specified order
# Increment salt for next iteration
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
),
Expand Down Expand Up @@ -177,12 +189,18 @@ def test_bloatnet_balance_extcodesize(
)


@pytest.mark.parametrize(
"balance_first",
[True, False],
ids=["balance_extcodecopy", "extcodecopy_balance"],
)
@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodecopy(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
balance_first: bool,
):
"""
BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
Expand All @@ -191,8 +209,8 @@ def test_bloatnet_balance_extcodecopy(
This test forces actual bytecode reads from disk by:
1. Assumes contracts are already deployed via the factory
2. Generating CREATE2 addresses dynamically during execution
3. Using BALANCE (cold) to warm the account
4. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode
3. Using BALANCE and EXTCODECOPY (order controlled by balance_first param)
4. Reading 1 byte from the END of the bytecode to force full contract load
"""
gas_costs = fork.gas_costs()
max_contract_size = fork.max_code_size()
Expand All @@ -204,16 +222,16 @@ def test_bloatnet_balance_extcodecopy(
cost_per_contract = (
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600)
+ gas_costs.G_BASE # POP balance (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
+ gas_costs.G_BASE # POP first result (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access base (100)
+ gas_costs.G_COPY * 1 # Copy cost for 1 byte (3)
+ gas_costs.G_BASE * 2 # DUP1 before BALANCE, DUP4 for address (6)
+ gas_costs.G_BASE * 2 # DUP1 before first op, DUP4 for address (6)
+ gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24)
+ gas_costs.G_LOW * 2 # MLOAD for salt twice (6)
+ gas_costs.G_VERY_LOW * 2 # ADD operations (6)
+ gas_costs.G_LOW # MSTORE salt back (3)
+ gas_costs.G_BASE # POP after EXTCODECOPY (2)
+ gas_costs.G_BASE # POP after second op (2)
+ 10 # While loop overhead
)

Expand All @@ -238,6 +256,20 @@ def test_bloatnet_balance_extcodecopy(
f"Factory storage will be checked during execution."
)

# Define operations that differ based on parameter
balance_op = Op.POP(Op.BALANCE)
extcodecopy_op = (
Op.PUSH1(1) # size (1 byte)
+ Op.PUSH2(max_contract_size - 1) # code offset (last byte)
+ Op.ADD(Op.MLOAD(32), 96) # unique memory offset
+ Op.DUP4 # address (duplicated earlier)
+ Op.EXTCODECOPY
+ Op.POP # clean up address
)
benchmark_ops = (
(balance_op + extcodecopy_op) if balance_first else (extcodecopy_op + balance_op)
)

# Build attack contract that reads config from factory and performs attack
attack_code = (
# Call getConfig() on factory to get num_deployed and init_code_hash
Expand Down Expand Up @@ -274,16 +306,7 @@ def test_bloatnet_balance_extcodecopy(
Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96]
# The address is now on the stack
+ Op.DUP1 # Duplicate for later operations
+ Op.POP(Op.BALANCE) # Cold access
# EXTCODECOPY(addr, mem_offset, last_byte_offset, 1)
# Read the LAST byte to force full contract load
+ Op.PUSH1(1) # size (1 byte)
+ Op.PUSH2(max_contract_size - 1) # code offset (last byte)
# Use salt as memory offset to avoid overlap
+ Op.ADD(Op.MLOAD(32), 96) # Add base memory offset for unique position
+ Op.DUP4 # address (duplicated earlier)
+ Op.EXTCODECOPY
+ Op.POP # Clean up address
+ benchmark_ops # Execute operations in specified order
# Increment salt for next iteration
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
),
Expand Down Expand Up @@ -313,3 +336,132 @@ def test_bloatnet_balance_extcodecopy(
blocks=[Block(txs=[attack_tx])],
post=post,
)


@pytest.mark.parametrize(
"balance_first",
[True, False],
ids=["balance_extcodehash", "extcodehash_balance"],
)
@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodehash(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
balance_first: bool,
):
"""
BloatNet test using BALANCE + EXTCODEHASH with on-the-fly CREATE2
address generation.

This test:
1. Assumes contracts are already deployed via the factory
2. Generates CREATE2 addresses dynamically during execution
3. Calls BALANCE and EXTCODEHASH (order controlled by balance_first param)
4. Forces client to compute code hash for 24KB bytecode
"""
gas_costs = fork.gas_costs()

# Calculate gas costs
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"")

# Cost per contract access with CREATE2 address generation
cost_per_contract = (
gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30)
+ gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6)
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Cold access (2600)
+ gas_costs.G_BASE # POP first result (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm access (100)
+ gas_costs.G_BASE # POP second result (2)
+ gas_costs.G_BASE # DUP1 before first op (3)
+ gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3)
+ gas_costs.G_LOW # MLOAD for salt (3)
+ gas_costs.G_VERY_LOW # ADD for increment (3)
+ gas_costs.G_LOW # MSTORE salt back (3)
+ 10 # While loop overhead
)

# Calculate how many contracts to access based on available gas
available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup
contracts_needed = int(available_gas // cost_per_contract)

# Deploy factory using stub contract
factory_address = pre.deploy_contract(
code=Bytecode(),
stub="bloatnet_factory",
)

# Log test requirements
print(
f"Test needs {contracts_needed} contracts for "
f"{gas_benchmark_value / 1_000_000:.1f}M gas. "
f"Factory storage will be checked during execution."
)

# Define operations that differ based on parameter
balance_op = Op.POP(Op.BALANCE)
extcodehash_op = Op.POP(Op.EXTCODEHASH)
benchmark_ops = (
(balance_op + extcodehash_op) if balance_first else (extcodehash_op + balance_op)
)

# Build attack contract that reads config from factory and performs attack
attack_code = (
# Call getConfig() on factory to get num_deployed and init_code_hash
Op.STATICCALL(
gas=Op.GAS,
address=factory_address,
args_offset=0,
args_size=0,
ret_offset=96,
ret_size=64,
)
# Check if call succeeded
+ Op.ISZERO
+ Op.PUSH2(0x1000) # Jump to error handler if failed
+ Op.JUMPI
# Load results from memory
+ Op.MLOAD(96) # Load num_deployed_contracts
+ Op.MLOAD(128) # Load init_code_hash
# Setup memory for CREATE2 address generation
+ Op.MSTORE(0, factory_address)
+ Op.MSTORE8(11, 0xFF)
+ Op.MSTORE(32, 0) # Initial salt
+ Op.PUSH1(64)
+ Op.MSTORE # Store init_code_hash
# Main attack loop
+ While(
body=(
# Generate CREATE2 address
Op.SHA3(11, 85)
+ Op.DUP1 # Duplicate for second operation
+ benchmark_ops # Execute operations in specified order
# Increment salt
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1))
),
condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
)
+ Op.POP # Clean up counter
)

# Deploy attack contract
attack_address = pre.deploy_contract(code=attack_code)

# Run the attack
attack_tx = Transaction(
to=attack_address,
gas_limit=gas_benchmark_value,
sender=pre.fund_eoa(),
)

# Post-state
post = {
attack_address: Account(storage={}),
}

blockchain_test(
pre=pre,
blocks=[Block(txs=[attack_tx])],
post=post,
)
Loading