Skip to content
Open
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
16 changes: 14 additions & 2 deletions .claude/commands/write-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,20 @@ Conventions and patterns for writing consensus tests. Run this skill before writ
## Fork-Aware Logic

- `fork >= Cancun` for conditional behavior based on fork
- `fork.gas_costs()` returns `GasCosts` dataclass with constants like `G_WARM_SLOAD`, `G_COLD_ACCOUNT_ACCESS`, `G_BASE`, etc.
- `fork.transaction_intrinsic_cost_calculator()` for computing tx intrinsic gas
- `fork.fork_at(timestamp=...)` gives the fork active before/after a transition boundary
- For gas amounts, see **Gas Cost Expectations** below — prefer framework cost constructs over reading `fork.gas_costs()` constants directly

## Gas Cost Expectations

Never hand-reconstruct a gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule duplicates the framework's own calculation and silently breaks when a future fork reprices. Instead:

- **Read the cost off the bytecode under test.** Set the relevant opcode metadata (`account_new`, `value_transfer`, `address_warm`, `key_warm`/`original_value`/`current_value`/`new_value`, `init_code_size`, `code_deposit_size`, `new_memory_size`, ...) and use `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior — e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`.
- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`; `fork.transaction_top_frame_state_gas(contract_creation=True)` for the created account's `NEW_ACCOUNT` (under EIP-2780 it is NOT part of the intrinsic — never subtract it from the intrinsic); `fork.transaction_data_floor_cost_calculator()`; `fork.call_value_stipend()`.
- **A single bare opcode/schedule cost** (e.g. an account-access constant) comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)`.
- **Fork-transition / cross-fork comparisons:** evaluate the same bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare `before` vs `after` costs — do not compare raw schedule constants.
- **Do not add "self-check" asserts** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork; they add no coverage over the runtime behavior the test already exercises and only break on repricing.
- **If the framework cannot express a cost, fix the framework** (wire the opcode into its gas/state map, add an accessor) rather than reconstructing it in the test. If the use case does not support the framework, the framework needs an update.
- **Exception:** a test whose *subject* is a specific schedule value (e.g. a regression that an opcode's cost is unchanged) may compare a runtime measurement (`CodeGasMeasure`) against `fork.gas_costs().OPCODE_*`. Even then, never hardcode the literal value.

## Transactions

Expand Down
9 changes: 7 additions & 2 deletions docs/writing_tests/fork_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ def test_some_feature(fork):

```python
def test_transaction_gas(fork, state_test):
gas_cost = fork.gas_costs().GAS_TX_BASE
# Derive the fork's intrinsic gas from the calculator rather than
# summing raw `gas_costs()` constants (see the Gas Parameters warning).
intrinsic_gas = fork.transaction_intrinsic_cost_calculator()()

# Create a transaction with the correct gas parameters for this fork
tx = Transaction(
gas_limit=gas_cost + 10000,
gas_limit=intrinsic_gas + 10000,
# ...
)

Expand Down Expand Up @@ -114,6 +116,9 @@ fork.memory_expansion_gas_calculator() # Returns a callable
fork.transaction_intrinsic_cost_calculator() # Returns a callable
```

!!! warning "Do not reconstruct expected gas from `gas_costs()` constants"
`fork.gas_costs()` exposes the raw schedule for framework internals. When a test needs an *expected* gas amount, derive it from a cost construct that tracks the live schedule (`bytecode.gas_cost(fork)` / `.regular_cost(fork)` / `.state_cost(fork)` / `.refund(fork)`, opcode metadata, the intrinsic/top-frame/data-floor calculators, `fork.call_value_stipend()`) rather than hand-summing constants — hand-built expectations silently break when a fork reprices. See [Opcode Metadata and Gas Calculations](opcode_metadata.md#do-not-hand-reconstruct-gas-from-constants).

### Transaction Types

Methods for determining valid transaction types:
Expand Down
15 changes: 15 additions & 0 deletions docs/writing_tests/opcode_metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ The execution testing package provides capabilities to calculate gas costs and r
- Validating gas cost calculations for specific opcode scenarios
- Future-proofing tests against breaking in upcoming forks that change gas rules

## Do Not Hand-Reconstruct Gas From Constants

Never build an expected gas amount by summing `fork.gas_costs()` constants (`NEW_ACCOUNT`, `CALL_VALUE`, `COLD_STORAGE_WRITE`, `VERY_LOW`, ...). Re-deriving the schedule by hand duplicates the framework's own calculation and silently breaks when a future fork reprices or restructures a cost. Always derive the expectation from a framework construct that tracks the live schedule:

- **The bytecode/opcode under test:** set the relevant metadata (see below) and read `bytecode.gas_cost(fork)` (regular + state), `.regular_cost(fork)`, `.state_cost(fork)`, or `.refund(fork)`. Link the exact opcode to the behavior, e.g. `Op.SELFDESTRUCT(account_new=True).state_cost(fork)`.
- **A single bare opcode/schedule cost** comes from a metadata-only opcode: `Op.BALANCE.with_metadata(address_warm=False).gas_cost(fork)` yields the cold account-access cost with no operand pushes.
- **Transaction-level costs:** `fork.transaction_intrinsic_cost_calculator()`, `fork.transaction_top_frame_state_gas(contract_creation=True)` (the created account's new-account state gas — on recent forks it is charged at the top frame, *not* in the intrinsic, so never subtract it from the intrinsic), `fork.transaction_data_floor_cost_calculator()`, and `fork.call_value_stipend()`.
- **Cross-fork / fork-transition comparisons:** evaluate the *same* bytecode or intrinsic at each fork (`before = fork.fork_at(timestamp=...)`, `after = ...`) and compare the resulting costs — do not compare raw schedule constants.

Additional rules:

- **Do not add "self-check" assertions** that compare a framework-computed value against a `fork.gas_costs()` decomposition of the same fork. They add no coverage over the runtime behavior the test already exercises and only break on repricing.
- **If the framework cannot express a cost, extend the framework** (wire the opcode into its gas/state map, add an accessor) rather than working around it in the test.
- **Exception:** a test whose *subject* is a specific schedule value — for example a regression asserting that an opcode's cost is unchanged across a fork — may compare a runtime measurement (`CodeGasMeasure`) against the fork's declared `fork.gas_costs().OPCODE_*` value. Even then, never hardcode the literal number.

## Opcode Metadata

Many opcodes accept metadata parameters that affect their gas cost calculations. Metadata represents runtime state information that influences gas consumption.
Expand Down
14 changes: 14 additions & 0 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,20 @@ def transaction_top_frame_state_gas(
del contract_creation, sends_value, recipient_type, authorizations
return 0

@classmethod
def call_value_stipend(cls) -> int:
"""
Return the gas stipend forwarded to the callee of a value-bearing
CALL/CALLCODE.

The stipend is added to the child frame's gas and returned to the
caller when the callee does not consume it, so tests that pin
value-call gas at an exact boundary subtract it from the charged
total. Exposed as a named accessor so tests need not read
``gas_costs().CALL_STIPEND`` directly.
"""
return cls.gas_costs().CALL_STIPEND

@classmethod
def system_call_gas_limit(cls) -> int:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,24 +368,27 @@ def test_block_gas_used_call_new_account(
GAS_NEW_ACCOUNT state gas) then SSTORE. Combined with a STOP tx,
the 2D max must reflect state gas from account creation.
"""
new_account_state_gas = fork.gas_costs().NEW_ACCOUNT
sstore_state_gas = Op.SSTORE(new_value=1).state_cost(fork)

target = pre.fund_eoa(amount=0)

call = Op.CALL(
gas=100_000,
address=target,
value=1,
value_transfer=True,
account_new=True,
)
parent_storage = Storage()
parent = pre.deploy_contract(
code=(
Op.CALL(gas=100_000, address=target, value=1)
+ Op.SSTORE(parent_storage.store_next(1), 1)
),
code=(call + Op.SSTORE(parent_storage.store_next(1), 1)),
balance=10**18,
)

txs = [
Transaction(
to=parent,
state_gas_reservoir=new_account_state_gas + sstore_state_gas,
state_gas_reservoir=call.state_cost(fork) + sstore_state_gas,
sender=pre.fund_eoa(),
),
] + stop_txs(pre, fork, 1)
Expand Down
Loading
Loading