Skip to content

Conversation

@CPerezz
Copy link
Contributor

@CPerezz CPerezz commented Nov 3, 2025

Implements parametrized storage benchmarks to measure gas costs for different storage state transitions:

  • Cold writes (0 -> non-zero): ~20,000 gas per slot
  • Warm updates (non-zero -> non-zero): ~2,900 gas per slot
  • Storage clearing (non-zero -> 0): ~2,900 gas + refund

The implementation uses a single contract that accepts parameters via calldata, pre-deployed with 500 filled slots to enable testing all transition types. This provides accurate gas measurements with minimal loop overhead (~39 gas/slot).

Tests are marked as stateful to avoid gas exhaustion expectations and work with the execution-specs testing framework.

✅ Checklist

  • All: Ran fast tox checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    uvx tox -e static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered adding an entry to CHANGELOG.md.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).
  • Tests: Ran mkdocs serve locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.
  • Tests: For PRs implementing a missed test case, update the post-mortem document to add an entry the list.
  • Ported Tests: All converted JSON/YML tests from ethereum/tests or tests/static have been assigned @ported_from marker.

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

Implements parametrized storage benchmarks to measure gas costs for different
storage state transitions:
- Cold writes (0 -> non-zero): ~20,000 gas per slot
- Warm updates (non-zero -> non-zero): ~2,900 gas per slot
- Storage clearing (non-zero -> 0): ~2,900 gas + refund

The implementation uses a single contract that accepts parameters via calldata,
pre-deployed with 500 filled slots to enable testing all transition types.
This provides accurate gas measurements with minimal loop overhead (~39 gas/slot).

Tests are marked as stateful to avoid gas exhaustion expectations and work
with the execution-specs testing framework.
0, # Start at slot 0
0xBEEFBEEF, # Different value
id="nonzero_to_nonzero"
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add the situation zero to zero here also? This might sound weird, but because some clients are slow on reading non-existent storage slots, this might be a bad situation for those.

pytest.param(
"nonzero_to_nonzero",
0, # Start at slot 0
0xBEEFBEEF, # Different value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For maximal load we want to use the longest values here (0xffff..ff) and to ensure clients have to hash everything, it would also help if we would change the value for each slot (so start at 0xff..ff and subtract one each time) (Can be a different PR since this needs changes to the contract code)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that changing the value will imply extra costs. Though we can pass the different values via calldata maybe. Let me see what to do here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it cost extra?

Copy link
Collaborator

@LouisTsai-Csie LouisTsai-Csie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Leave some small suggestions.

Comment on lines +50 to +52
bytecode += Op.PUSH1(0) + Op.CALLDATALOAD # num_slots at bytes 0-31
bytecode += Op.PUSH1(32) + Op.CALLDATALOAD # start_slot at bytes 32-63
bytecode += Op.PUSH1(64) + Op.CALLDATALOAD # value at bytes 64-95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small suggestion:

Suggested change
bytecode += Op.PUSH1(0) + Op.CALLDATALOAD # num_slots at bytes 0-31
bytecode += Op.PUSH1(32) + Op.CALLDATALOAD # start_slot at bytes 32-63
bytecode += Op.PUSH1(64) + Op.CALLDATALOAD # value at bytes 64-95
bytecode += Op.CALLDATALOAD(0) # num_slots at bytes 0-31
bytecode += Op.CALLDATALOAD(32) # start_slot at bytes 32-63
bytecode += Op.CALLDATALOAD(64) # value at bytes 64-95

2. nonzero to 0: Clearing existing slots (provides gas refund)
3. nonzero to nonzero: Updating warm slots (moderate cost)
"""
sender = pre.fund_eoa(10**18)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sender = pre.fund_eoa(10**18)
sender = pre.fund_eoa()

# Create transaction to call the contract
# Use a reasonable gas limit that covers the operation
# Each SSTORE costs up to 20,000 gas for cold writes plus overhead
gas_limit = 21000 + (num_slots * 50000) # Base tx cost + storage operations
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious why specifying the gas limit here, will it be affected if we make it the default value?

If necessary, i would suggest to use:

Suggested change
gas_limit = 21000 + (num_slots * 50000) # Base tx cost + storage operations
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
gas_limit = intrinsic_gas_cost_calc() + (num_slots * 50000) # Base tx cost + storage operations

Comment on lines +124 to +132
def test_storage_transitions_benchmark(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
num_slots: int,
transition_type: str,
start_offset: int,
write_value: int,
) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable

Suggested change
def test_storage_transitions_benchmark(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
num_slots: int,
transition_type: str,
start_offset: int,
write_value: int,
) -> None:
def test_storage_transitions_benchmark(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
num_slots: int,
start_offset: int,
write_value: int,
) -> None:



@pytest.mark.valid_from("Prague")
@pytest.mark.stateful # Mark as stateful instead of benchmark
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@pytest.mark.stateful # Mark as stateful instead of benchmark

All the tests under statful/ folder would be marked as stateful test! No need to specify them.

@pytest.mark.stateful # Mark as stateful instead of benchmark
@pytest.mark.parametrize("num_slots", [1, 10, 50, 100, 200])
@pytest.mark.parametrize(
"transition_type,start_offset,write_value",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we still need transition_type parametrization? It seems similar to the id, and this value is not referenced in the test implementation.

@LouisTsai-Csie
Copy link
Collaborator

Note: please run tox -e static, and make sure the line length is within the ruff rules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants