-
Notifications
You must be signed in to change notification settings - Fork 177
feat(tests): multi opcode bloatnet ext cases #2186
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
Merged
spencer-tb
merged 55 commits into
ethereum:main
from
CPerezz:feat/multi-opcode-bloatnet-EXT-cases
Oct 1, 2025
Merged
Changes from all commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
a1f2153
Add BloatNet tests
gballet 02d65b4
try building the contract
gballet e721cc6
fix: SSTORE 0 -> 1 match all values in the state
gballet d1cad25
add the tx for 0 -> 1 and 1 -> 2
gballet 16f6d30
fix: linter issues
gballet 374e08a
remove more whitespaces
gballet 333c876
fix formatting
gballet 79a95b8
move to benchmarks
gballet 8131e98
fix linter value
gballet 5f805fd
use the gas limit from the environment
gballet 090a400
parameterize the written value in SSTORE
gballet cd02a02
fix linter issues
gballet 1f3c381
update CHANGELOG.md
gballet f6def7e
fix format
gballet 7e20a50
simplify syntax
gballet c24ad35
fix: start with an empty contract storage
gballet fc27e53
more fixes, but the result is still incorrect
gballet 7d87262
fix: finally fix the tests
gballet 8556014
linter fix
gballet 326915e
add SLOAD tests
gballet 1f8e62a
test(benchmark): implement CREATE2 addressing for bloatnet tests
CPerezz 8babb13
refactor(benchmark): optimize gas calculations in bloatnet tests
CPerezz e70132b
refactor(benchmark): bloatnet tests with unique bytecode for I/O optβ¦
CPerezz 0e889d7
refactor(benchmark): replace custom CREATE2 address calculation with β¦
CPerezz e4583b6
CREATE2 factory approach working
CPerezz 06f9a63
Version with EIP-7997 model working
CPerezz 49c1343
refactor(benchmark): imrpove contract deployment script with interactβ¦
CPerezz 2875cf4
delete: remove obsolete test_create2.py script
CPerezz b634ca3
refactor(benchmark): optimize gas calculations for BALANCE + EXTCODECβ¦
CPerezz 774c56c
refactor(benchmark): support non-fixed max_codesize
CPerezz 6e6863a
chore: Remove all 24kB "hardcoded" refs
CPerezz f2cd5f9
fix: pre-commit lint hooks
CPerezz cf2c7c6
push updated deploy_create2_factory refactored with EEST as dep
CPerezz a862f76
refactor(benchmark): enhance CREATE2 factory deployment and testing
CPerezz 55396fb
remove: old_deploy_factory script
CPerezz 240c042
chore: address PR review fixes
CPerezz 03f6595
Merge branch 'main' into feat/multi-opcode-bloatnet-EXT-cases
CPerezz aead28c
fix(benchmark): correct import path for ethereum_test_vm
CPerezz a4fac3b
chore(benchmark): update according to review comments
CPerezz 93a3e06
refactor(benchmark): remove hardcoded parameters storing inside factoβ¦
CPerezz e0742a9
chore: update pyproject.toml configuration
CPerezz d3be627
refactor: rename test_mutiopcode.py to test_muti_opcode.py for consisβ¦
CPerezz 164ad8b
fix: correct import sorting in test_muti_opcode.py to fix CI lint error
CPerezz ee29d20
fix(benchmark): rename test file to fix typo
CPerezz 97c4efc
fix(benchmark): update BloatNet tests to use factory's getConfig() meβ¦
CPerezz 4dc4876
refactor(benchmark): enhance BloatNet test documentation and gas costβ¦
CPerezz d7c79f0
revert: restore pyproject.toml to match main branch
CPerezz d3671fa
fix(benchmark): resolve W505 doc line length issues in test_multi_opcβ¦
CPerezz 8590357
refactor(benchmark): simplify STATICCALL usage in BloatNet tests.
CPerezz c044cb3
feat(benchmark): add gas exhaustion validation using expected_receipt
CPerezz ad6b424
fix(benchmark): restore skip_gas_used_validation flag
CPerezz 2947788
refactor(benchmark): improve readability using kwargs syntax for opcodes
CPerezz bf665c5
fix(benchmark): shorten comment lines to meet doc length limit
CPerezz 6841f09
fix(benchmark): correct MSTORE operation to store init_code_hash propβ¦
CPerezz d1b868d
fix(benchmark): address review comments - remove redundant validationβ¦
CPerezz 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
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 @@ | ||
"""BloatNet benchmark tests for Ethereum execution spec tests.""" |
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,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 | ||
CPerezz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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 | ||
CPerezz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
# 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={}), | ||
} | ||
CPerezz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
blockchain_test( | ||
pre=pre, | ||
blocks=[Block(txs=[attack_tx])], | ||
post=post, | ||
) |
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.