Skip to content

Commit 6b31a95

Browse files
committed
feat(tests): add EXTCODEHASH tests and inverted opcode order variants
Add comprehensive BloatNet multi-opcode benchmark tests: - test_bloatnet_balance_extcodehash: BALANCE → EXTCODEHASH - test_bloatnet_extcodesize_balance: EXTCODESIZE → BALANCE (inverted) - test_bloatnet_extcodecopy_balance: EXTCODECOPY → BALANCE (inverted) - test_bloatnet_extcodehash_balance: EXTCODEHASH → BALANCE (inverted) All tests follow the same pattern: - Dynamic CREATE2 address generation - Precise gas cost calculations - One cold access followed by one warm access - Factory stub pattern for pre-deployed contracts
1 parent d1b868d commit 6b31a95

File tree

2 files changed

+677
-0
lines changed

2 files changed

+677
-0
lines changed

CLAUDE.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,203 @@ jq -r '.opName' debug_output/**/*.jsonl
196196
5. Commit with semantic format
197197

198198
**Critical**: Always run linting and type checking. Use `--clean` when filling. Never use hardcoded addresses.
199+
200+
## 🎯 BloatNet Multi-Opcode Benchmarks
201+
202+
### Overview
203+
204+
BloatNet benchmarks stress-test Ethereum clients by rapidly accessing many large (24KB) contracts to measure state-handling performance. The multi-opcode variants test specific EVM opcodes (BALANCE, EXTCODESIZE, EXTCODECOPY) against pre-deployed contracts.
205+
206+
### Architecture
207+
208+
```text
209+
┌─────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
210+
│ Initcode Contract │ │ Factory Contract │ │ 24KB Contracts │
211+
│ (~9.5KB) │ │ (116B) │ │ (N × 24KB each) │
212+
└──────────┬──────────┘ └────────┬─────────┘ └──────────────────┘
213+
│ │ │
214+
│ EXTCODECOPY │ CREATE2(salt++) │
215+
└─────────────────────────►├─────────────────────► Contract_0
216+
├─────────────────────► Contract_1
217+
├─────────────────────► Contract_2
218+
└─────────────────────► Contract_N
219+
220+
┌──────────────────┐
221+
│ Attack Contract │──STATICCALL──► Factory.getConfig()
222+
│ │ returns: (N, hash)
223+
└────────┬─────────┘
224+
225+
└─► Loop(i=0 to N):
226+
1. Generate CREATE2 addr: keccak256(0xFF|factory|i|hash)[12:]
227+
2. BALANCE(addr) → 2600 gas (cold access)
228+
3. EXTCODESIZE(addr) → 100 gas (warm access)
229+
```
230+
231+
### Key Components
232+
233+
#### 1. Initcode Contract (`deploy_initcode.py`)
234+
235+
- **Purpose**: Stores the template initcode that generates unique 24KB contracts
236+
- **Size**: ~9.5KB (uses `While` loop expansion)
237+
- **Uniqueness**: Uses `ADDRESS` opcode as seed, making each deployed contract unique
238+
- **Algorithm**:
239+
- Store `ADDRESS` as initial seed (different per CREATE2 deployment)
240+
- Loop: Expand to 24KB using SHA3 + XOR operations with 256-entry XOR table
241+
- Set first byte to `0x00` (STOP) for efficient CALL handling
242+
- Return 24KB bytecode
243+
244+
#### 2. Factory Contract (`deploy_create2_factory.py`)
245+
246+
- **Purpose**: Deploys contracts via CREATE2 and provides config info to tests
247+
- **Storage Layout**:
248+
- Slot 0: Counter (number of deployed contracts)
249+
- Slot 1: Init code hash (for CREATE2 address calculation)
250+
- Slot 2: Initcode contract address
251+
- **Interface**:
252+
- `CALLDATASIZE == 0`: Returns `(num_deployed_contracts, init_code_hash)` (64 bytes)
253+
- `CALLDATASIZE > 0`: Deploys new contract via CREATE2 with counter as salt
254+
- **Deployment Flow**:
255+
1. Load initcode from stored address via EXTCODECOPY
256+
2. CREATE2 with current counter as salt
257+
3. Increment counter
258+
4. Return deployed address
259+
260+
#### 3. Attack Contract (in test files)
261+
262+
- **Purpose**: Rapidly accesses all deployed contracts to stress state handling
263+
- **Flow**:
264+
1. STATICCALL factory.getConfig() to get N and hash
265+
2. Setup memory for CREATE2 address generation: `0xFF + factory_addr + salt + hash`
266+
3. Loop N times:
267+
- Generate CREATE2 address: `keccak256(memory[11:96])`
268+
- Execute opcode tests (BALANCE, EXTCODESIZE, EXTCODECOPY)
269+
- Increment salt
270+
- **Gas Calculation**: Each test calculates exact gas cost per contract iteration
271+
272+
### Test Implementation Pattern
273+
274+
Tests in [test_multi_opcode.py](tests/benchmark/bloatnet/test_multi_opcode.py):
275+
276+
```python
277+
@pytest.mark.valid_from("Prague")
278+
def test_bloatnet_balance_extcodesize(
279+
blockchain_test: BlockchainTestFiller,
280+
pre: Alloc,
281+
fork: Fork,
282+
gas_benchmark_value: int,
283+
):
284+
# 1. Calculate cost per contract access
285+
cost_per_contract = (
286+
gas_costs.G_KECCAK_256 + gas_costs.G_KECCAK_256_WORD * 3 # Address gen
287+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # BALANCE (cold)
288+
+ gas_costs.G_WARM_ACCOUNT_ACCESS # EXTCODESIZE (warm)
289+
+ /* stack operations */
290+
)
291+
292+
# 2. Deploy factory as stub (must be pre-deployed externally)
293+
factory_address = pre.deploy_contract(
294+
code=Bytecode(),
295+
stub="bloatnet_factory",
296+
)
297+
298+
# 3. Build attack contract that:
299+
# - Calls factory.getConfig() to get N and hash
300+
# - Loops N times, generating CREATE2 addresses and accessing contracts
301+
attack_code = (
302+
Op.STATICCALL(factory_address, ret_offset=96, ret_size=64)
303+
+ /* setup CREATE2 address generation */
304+
+ While(
305+
body=(
306+
Op.SHA3(11, 85) # Generate CREATE2 address
307+
+ Op.DUP1
308+
+ Op.POP(Op.BALANCE) # Cold access
309+
+ Op.POP(Op.EXTCODESIZE) # Warm access
310+
+ /* increment salt */
311+
),
312+
condition=/* counter > 0 */
313+
)
314+
)
315+
```
316+
317+
### Deployment Workflow
318+
319+
1. **Deploy Initcode** (`deploy_initcode.py`):
320+
```bash
321+
python3 deploy_initcode.py
322+
# Outputs: initcode_address.json with address and hash
323+
```
324+
325+
2. **Deploy Factory** (`deploy_create2_factory.py`):
326+
```bash
327+
python3 deploy_create2_factory.py
328+
# Outputs: stubs.json with factory address
329+
```
330+
331+
3. **Deploy Contracts** (`deploy_bloatnet_contracts.py`):
332+
```bash
333+
python3 deploy_bloatnet_contracts.py 1000 --stubs stubs.json
334+
# Deploys 1000 contracts via factory (each uses ~8.6M gas)
335+
```
336+
337+
4. **Run Tests**:
338+
```bash
339+
uv run execute remote \
340+
--rpc-endpoint http://127.0.0.1:8545 \
341+
--address-stubs stubs.json \
342+
-- --fork Prague --gas-benchmark-values 5 \
343+
tests/benchmark/bloatnet/test_multi_opcode.py
344+
```
345+
346+
### Critical Design Decisions
347+
348+
1. **Stub Contracts**: Tests use `stub="bloatnet_factory"` to reference pre-deployed factory
349+
- Avoids hardcoded addresses
350+
- Factory address provided via `--address-stubs` flag
351+
- Tests read config dynamically via `getConfig()`
352+
353+
2. **Dynamic Address Generation**: Attack contracts generate CREATE2 addresses on-the-fly
354+
- Uses same formula: `keccak256(0xFF | factory | salt | hash)[12:]`
355+
- No pre-computation or storage of addresses needed
356+
- Minimal memory footprint
357+
358+
3. **Gas Precision**: Each test calculates exact gas cost per iteration
359+
- Uses `fork.gas_costs()` for accurate opcode costs
360+
- Includes SHA3 word costs, stack operations, loop overhead
361+
- Ensures tests consume exactly the target gas amount
362+
363+
4. **Unique Contracts**: Each deployed contract has unique bytecode
364+
- Uses `ADDRESS` opcode in initcode (different per CREATE2 deployment)
365+
- Forces clients to store distinct contract code
366+
- Prevents deduplication optimizations
367+
368+
### Available Tests
369+
370+
- `test_bloatnet_balance_extcodesize`: BALANCE (cold) + EXTCODESIZE (warm)
371+
- `test_bloatnet_balance_extcodecopy`: BALANCE (cold) + EXTCODECOPY (warm, reads last byte)
372+
373+
### Common Patterns
374+
375+
**Reading factory config in attack contract**:
376+
```python
377+
Op.STATICCALL(
378+
gas=Op.GAS,
379+
address=factory_address,
380+
args_offset=0,
381+
args_size=0,
382+
ret_offset=96,
383+
ret_size=64, # Returns 2 × 32 bytes
384+
)
385+
+ Op.MLOAD(96) # num_deployed_contracts
386+
+ Op.MLOAD(128) # init_code_hash
387+
```
388+
389+
**CREATE2 address generation in memory**:
390+
```python
391+
# Memory layout: 0xFF(1) + factory(20) + salt(32) + hash(32) = 85 bytes
392+
Op.MSTORE(0, factory_address)
393+
+ Op.MSTORE8(11, 0xFF) # Prefix at position 32-20-1
394+
+ Op.MSTORE(32, 0) # Salt
395+
+ Op.MSTORE(64, hash) # Init code hash
396+
# Generate address:
397+
Op.SHA3(11, 85) # Hash from byte 11 for 85 bytes
398+
```

0 commit comments

Comments
 (0)