Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a1f2153
Add BloatNet tests
gballet Aug 14, 2025
02d65b4
try building the contract
gballet Aug 14, 2025
e721cc6
fix: SSTORE 0 -> 1 match all values in the state
gballet Aug 14, 2025
d1cad25
add the tx for 0 -> 1 and 1 -> 2
gballet Aug 14, 2025
16f6d30
fix: linter issues
gballet Aug 14, 2025
374e08a
remove more whitespaces
gballet Aug 14, 2025
333c876
fix formatting
gballet Aug 15, 2025
79a95b8
move to benchmarks
gballet Aug 21, 2025
8131e98
fix linter value
gballet Aug 22, 2025
5f805fd
use the gas limit from the environment
gballet Aug 22, 2025
090a400
parameterize the written value in SSTORE
gballet Aug 26, 2025
cd02a02
fix linter issues
gballet Aug 26, 2025
1f3c381
update CHANGELOG.md
gballet Aug 26, 2025
f6def7e
fix format
gballet Aug 26, 2025
7e20a50
simplify syntax
gballet Aug 26, 2025
c24ad35
fix: start with an empty contract storage
gballet Aug 26, 2025
fc27e53
more fixes, but the result is still incorrect
gballet Aug 26, 2025
7d87262
fix: finally fix the tests
gballet Aug 26, 2025
8556014
linter fix
gballet Aug 27, 2025
326915e
add SLOAD tests
gballet Aug 27, 2025
1f8e62a
test(benchmark): implement CREATE2 addressing for bloatnet tests
CPerezz Aug 29, 2025
8babb13
refactor(benchmark): optimize gas calculations in bloatnet tests
CPerezz Sep 11, 2025
e70132b
refactor(benchmark): bloatnet tests with unique bytecode for I/O opt…
CPerezz Sep 11, 2025
0e889d7
refactor(benchmark): replace custom CREATE2 address calculation with …
CPerezz Sep 11, 2025
e4583b6
CREATE2 factory approach working
CPerezz Sep 17, 2025
06f9a63
Version with EIP-7997 model working
CPerezz Sep 17, 2025
49c1343
refactor(benchmark): imrpove contract deployment script with interact…
CPerezz Sep 17, 2025
2875cf4
delete: remove obsolete test_create2.py script
CPerezz Sep 18, 2025
b634ca3
refactor(benchmark): optimize gas calculations for BALANCE + EXTCODEC…
CPerezz Sep 18, 2025
774c56c
refactor(benchmark): support non-fixed max_codesize
CPerezz Sep 18, 2025
6e6863a
chore: Remove all 24kB "hardcoded" refs
CPerezz Sep 18, 2025
f2cd5f9
fix: pre-commit lint hooks
CPerezz Sep 18, 2025
cf2c7c6
push updated deploy_create2_factory refactored with EEST as dep
CPerezz Sep 18, 2025
a862f76
refactor(benchmark): enhance CREATE2 factory deployment and testing
CPerezz Sep 19, 2025
55396fb
remove: old_deploy_factory script
CPerezz Sep 19, 2025
240c042
chore: address PR review fixes
CPerezz Sep 22, 2025
03f6595
Merge branch 'main' into feat/multi-opcode-bloatnet-EXT-cases
CPerezz Sep 22, 2025
aead28c
fix(benchmark): correct import path for ethereum_test_vm
CPerezz Sep 22, 2025
a4fac3b
chore(benchmark): update according to review comments
CPerezz Sep 22, 2025
93a3e06
refactor(benchmark): remove hardcoded parameters storing inside facto…
CPerezz Sep 23, 2025
e0742a9
chore: update pyproject.toml configuration
CPerezz Sep 23, 2025
d3be627
refactor: rename test_mutiopcode.py to test_muti_opcode.py for consis…
CPerezz Sep 23, 2025
164ad8b
fix: correct import sorting in test_muti_opcode.py to fix CI lint error
CPerezz Sep 23, 2025
ee29d20
fix(benchmark): rename test file to fix typo
CPerezz Sep 23, 2025
97c4efc
fix(benchmark): update BloatNet tests to use factory's getConfig() me…
CPerezz Sep 25, 2025
4dc4876
refactor(benchmark): enhance BloatNet test documentation and gas cost…
CPerezz Sep 25, 2025
d7c79f0
revert: restore pyproject.toml to match main branch
CPerezz Sep 25, 2025
d3671fa
fix(benchmark): resolve W505 doc line length issues in test_multi_opc…
CPerezz Sep 25, 2025
8590357
refactor(benchmark): simplify STATICCALL usage in BloatNet tests.
CPerezz Sep 26, 2025
c044cb3
feat(benchmark): add gas exhaustion validation using expected_receipt
CPerezz Sep 26, 2025
ad6b424
fix(benchmark): restore skip_gas_used_validation flag
CPerezz Sep 26, 2025
2947788
refactor(benchmark): improve readability using kwargs syntax for opcodes
CPerezz Sep 26, 2025
bf665c5
fix(benchmark): shorten comment lines to meet doc length limit
CPerezz Sep 26, 2025
6841f09
fix(benchmark): correct MSTORE operation to store init_code_hash prop…
CPerezz Sep 27, 2025
d1b868d
fix(benchmark): address review comments - remove redundant validation…
CPerezz Sep 27, 2025
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
3 changes: 2 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ Users can select any of the artifacts depending on their benchmarking or testing

### πŸ§ͺ Test Cases

- ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): Add additional test cases for modular comparison and initcode context ([#2023](https://github.com/ethereum/execution-spec-tests/pull/2023), & [#2068](https://github.com/ethereum/execution-spec-tests/pull/2068)).
- ✨ [BloatNet](https://bloatnet.info)/Multidimensional Metering: Add benchmarks to be used as part of the BloatNet project and also for Multidimensional Metering.
- ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): Add additional test cases for modular comparison.
- πŸ”€ Refactored `BLOBHASH` opcode context tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1637](https://github.com/ethereum/execution-spec-tests/pull/1637)).
- πŸ”€ Refactored `SELFDESTRUCT` opcode collision tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1643](https://github.com/ethereum/execution-spec-tests/pull/1643)).
- ✨ EIP-7594: Sanity test cases to send blob transactions and verify `engine_getBlobsVX` using the `execute` command ([#1644](https://github.com/ethereum/execution-spec-tests/pull/1644),[#1884](https://github.com/ethereum/execution-spec-tests/pull/1884)).
Expand Down
1 change: 1 addition & 0 deletions tests/benchmark/bloatnet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""BloatNet benchmark tests for Ethereum execution spec tests."""
315 changes: 315 additions & 0 deletions tests/benchmark/bloatnet/test_multi_opcode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
"""
abstract: BloatNet bench cases extracted from https://hackmd.io/9icZeLN7R0Sk5mIjKlZAHQ.

The idea of all these tests is to stress client implementations to find out
where the limits of processing are focusing specifically on state-related
operations.
"""

import pytest

from ethereum_test_forks import Fork
from ethereum_test_tools import (
Account,
Alloc,
Block,
BlockchainTestFiller,
Transaction,
While,
)
from ethereum_test_vm import Bytecode
from ethereum_test_vm import Opcodes as Op

REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md"
REFERENCE_SPEC_VERSION = "1.0"


# BLOATNET ARCHITECTURE:
#
# [Initcode Contract] [Factory Contract] [24KB Contracts]
# (9.5KB) (116B) (N x 24KB each)
# β”‚ β”‚ β”‚
# β”‚ EXTCODECOPY β”‚ CREATE2(salt++) β”‚
# └──────────────► β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_0
# β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_1
# β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_2
# └──────────────────► Contract_N
#
# [Attack Contract] ──STATICCALL──► [Factory.getConfig()]
# β”‚ returns: (N, hash)
# └─► Loop(i=0 to N):
# 1. Generate CREATE2 addr: keccak256(0xFF|factory|i|hash)[12:]
# 2. BALANCE(addr) β†’ 2600 gas (cold access)
# 3. EXTCODESIZE(addr) β†’ 100 gas (warm access)
#
# HOW IT WORKS:
# 1. Factory uses EXTCODECOPY to load initcode, avoiding PC-relative jumps
# 2. Each CREATE2 deployment produces unique 24KB bytecode (via ADDRESS)
# 3. All contracts share same initcode hash for deterministic addresses
# 4. Attack rapidly accesses all contracts, stressing client's state handling


@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodesize(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
):
"""
BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2
address generation.

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
4. Maximizes cache eviction by accessing many contracts
"""
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 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_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 - NO HARDCODED VALUES
# The stub "bloatnet_factory" must be provided via --address-stubs flag
# The factory at that address MUST have:
# - Slot 0: Number of deployed contracts
# - Slot 1: Init code hash for CREATE2 address calculation
factory_address = pre.deploy_contract(
code=Bytecode(), # Required parameter, but will be ignored for stubs
stub="bloatnet_factory",
)

# Log test requirements - deployed count read from factory storage
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."
)

# 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 (far jump)
+ Op.JUMPI
# Load results from memory
# Memory[96:128] = num_deployed_contracts
# Memory[128:160] = init_code_hash
+ Op.MLOAD(96) # Load num_deployed_contracts
+ Op.MLOAD(128) # Load init_code_hash
# Setup memory for CREATE2 address generation
# Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32)
+ Op.MSTORE(0, factory_address) # Store factory address at memory position 0
+ Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1)
+ Op.MSTORE(32, 0) # Store salt at position 32
# Stack now has: [num_contracts, init_code_hash]
+ Op.PUSH1(64) # Push memory position
+ Op.MSTORE # Store init_code_hash at memory[64]
# Stack now has: [num_contracts]
# Main attack loop - iterate through all deployed contracts
+ While(
body=(
# 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
# Increment salt for next iteration
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
),
# Continue while we haven't reached the limit
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: just verify attack contract exists
post = {
attack_address: Account(storage={}),
}

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


@pytest.mark.valid_from("Prague")
def test_bloatnet_balance_extcodecopy(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
gas_benchmark_value: int,
):
"""
BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2
address generation.

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
"""
gas_costs = fork.gas_costs()
max_contract_size = fork.max_code_size()

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

# Cost per contract with EXTCODECOPY and 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 BALANCE (2600)
+ gas_costs.G_BASE # POP balance (2)
+ gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY 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_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)
+ 10 # While loop overhead
)

# Calculate how many contracts to access
available_gas = gas_benchmark_value - intrinsic_gas - 1000
contracts_needed = int(available_gas // cost_per_contract)

# Deploy factory using stub contract - NO HARDCODED VALUES
# The stub "bloatnet_factory" must be provided via --address-stubs flag
# The factory at that address MUST have:
# - Slot 0: Number of deployed contracts
# - Slot 1: Init code hash for CREATE2 address calculation
factory_address = pre.deploy_contract(
code=Bytecode(), # Required parameter, but will be ignored for stubs
stub="bloatnet_factory",
)

# Log test requirements - deployed count read from factory storage
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."
)

# 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 (far jump)
+ Op.JUMPI
# Load results from memory
# Memory[96:128] = num_deployed_contracts
# Memory[128:160] = init_code_hash
+ Op.MLOAD(96) # Load num_deployed_contracts
+ Op.MLOAD(128) # Load init_code_hash
# Setup memory for CREATE2 address generation
# Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32)
+ Op.MSTORE(0, factory_address) # Store factory address at memory position 0
+ Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1)
+ Op.MSTORE(32, 0) # Store salt at position 32
# Stack now has: [num_contracts, init_code_hash]
+ Op.PUSH1(64) # Push memory position
+ Op.MSTORE # Store init_code_hash at memory[64]
# Stack now has: [num_contracts]
# Main attack loop - iterate through all deployed contracts
+ While(
body=(
# Generate CREATE2 address
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
# Increment salt for next iteration
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt
),
# Continue while counter > 0
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