Skip to content

Conversation

@marioevz
Copy link
Member

@marioevz marioevz commented Feb 4, 2026

🗒️ Description

Enhancement 1: Use account hashes to determine contract addresses

TL;DR

Reduce account duplication during pre-alloc grouping by making contract addresses a function of the account’s contents.

Details

Normally, account collisions are undesirable because once a test touches an account, its nonce increases, its storage may be modified, and its balance can change.

This is particularly relevant during test execution, where we deploy fresh contracts on a live network to start each test from the most deterministic state possible.

However, this constraint does not apply to tests that are filled and then consumed, since test execution always starts from a clean state.

In the case of Engine X tests, we always perform a forkchoice-update back to genesis. As a result, it does not matter if the same contract is used by multiple tests, because reverting to genesis rolls back all state changes. We can therefore safely reuse the same account across tests, provided it has the same initial state.

This PR modifies the behavior of Pre to calculate a contract’s address as a hash of the account’s contents, including its initial balance, nonce, storage, and code.

We also add a salt to the calculated hash to support cases where a single test requires multiple identical copies of the same account in the state.

Examples

Same contract, two different tests:

def test_1(pre: Alloc) -> None:
    contract_test_1 = pre.deploy_contract(code=Op.STOP)

def test_2(pre: Alloc) -> None:
    contract_test_2 = pre.deploy_contract(code=Op.STOP)
Outcomes
  • contract_test_1 == contract_test_2

Same contract, two different tests, multiple times:

def test_1(pre: Alloc) -> None:
    contract_test_1_a = pre.deploy_contract(code=Op.STOP)
    contract_test_1_b = pre.deploy_contract(code=Op.STOP)

def test_2(pre: Alloc) -> None:
    contract_test_2_a = pre.deploy_contract(code=Op.STOP)
    contract_test_2_a = pre.deploy_contract(code=Op.STOP)
Outcomes
  • contract_test_1_a == contract_test_2_a
  • contract_test_1_b == contract_test_2_b
  • contract_test_1_a != contract_test_1_b
  • contract_test_2_a != contract_test_2_b

Same contract, two different tests, different properties:

def test_1(pre: Alloc) -> None:
    contract_test_1 = pre.deploy_contract(code=Op.STOP, storage={0: 1})

def test_2(pre: Alloc) -> None:
    contract_test_2 = pre.deploy_contract(code=Op.STOP, storage={0: 2})
Outcomes
  • contract_test_1 != contract_test_2

EOA, no arguments, two different tests:

def test_1(pre: Alloc) -> None:
    sender_1 = pre.fund_eoa()

def test_2(pre: Alloc) -> None:
    sender_2 = pre.fund_eoa()
Outcomes
  • sender_1 == sender_2

EOA, same fund amount, two different tests:

def test_1(pre: Alloc) -> None:
    sender_1 = pre.fund_eoa(amount=0)

def test_2(pre: Alloc) -> None:
    sender_2 = pre.fund_eoa(amount=0)
Outcomes
  • sender_1 == sender_2

EOA, different fund amount, two different tests:

def test_1(pre: Alloc) -> None:
    sender_1 = pre.fund_eoa(amount=0)

def test_2(pre: Alloc) -> None:
    sender_2 = pre.fund_eoa(amount=1)
Outcomes
  • sender_1 != sender_2

Enhancement 2: Detect Grouping Separation Automatically

TL;DR

Automatically detect changes in the pre that would result in conflicts with other tests and put the test into a different group without having to rely on pytest.mark.pre_alloc_group.

Details

Keeps track of the addresses that were modified in the following ways:

  • pre[address] = Account(...)
  • pre.fund_address(...)
  • pre.deploy_contract(..., address=0x1234..., ...)
  • del pre[address]

All of these actions allow the test to directly modify an account at an specific address' contents, which would affect other tests.

We keep track of the addresses that were modified this way and hash the contents of each account to add it to the pre-alloc group hashing.

Reasoning behind pre.fund_address(...) in particular is that most tests expect other addresses (such as system addresses or precompiles, or even created contracts) to start with zero balance. This is different from specifying a non-zero balance in the pre.deploy_contract because this modifies the resulting address due to the hashing, which makes the collision impossible.

All usages of pytest.mark.pre_alloc_group have been removed from tests and pre-alloc grouping test execution of all Amsterdam test did not result in any issues. The marker itself was not removed just to retain the ability to group tests manually.

🔗 Related Issues or PRs

N/A.

✅ 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 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-->

@danceratopz
Copy link
Member

danceratopz commented Feb 5, 2026

In the case of Engine X tests, we always perform a forkchoice-update back to genesis. As a result, it does not matter if the same contract is used by multiple tests, because reverting to genesis rolls back all state changes. We can therefore safely reuse the same account across tests, provided it has the same initial state.

This is not strictly necessary and is currently skipped in #1964 see https://github.com/danceratopz/execution-specs/blob/6fe382c0370ce94a935f65c89eac1073bc6324b6/packages/testing/src/execution_testing/cli/pytest_commands/plugins/consume/simulators/simulator_logic/test_via_engine.py#L43-L76.

We can certainly benchmark to see how much time is saved by avoiding the initial FCU!

But the initial plan was to skip it unless it was considered necessary for intermittent garbage collection (e.g. benchmarking efforts hinted that this would be necessary for Besu). A follow-up PR was to add --enginex-initial-fcu-freq to enable this; as-is it is entirely skipped.

Edit: We don't FCU post-genesis, but the effect is the same (as we don't FCU after the test either) - we're always building on top of genesis in engine x :)

Copy link
Contributor

@spencer-tb spencer-tb left a comment

Choose a reason for hiding this comment

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

Smart optimization. Assuming this will save memory during phase 1 and intial stage of phase 2 filling. The salt mechanism is cool.

Comment on lines 403 to 404
If the address is already present in the pre-alloc the amount will be
added to its existing balance.
Copy link
Contributor

Choose a reason for hiding this comment

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

Docstring update suggestion

Suggested change
If the address is already present in the pre-alloc the amount will be
added to its existing balance.
Add a funded account to the pre-allocation.
The address must not already exist in the pre-allocation. To set the
balance of an account, use the `amount` parameter in `fund_eoa()` or
the `balance` parameter in `deploy_contract()` at creation time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added, thanks!

Comment on lines 351 to 352
class FrozenStorage(Storage):
"""Frozen storage."""
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming will be used later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed as it ended up not being necessary 👍

@marioevz marioevz force-pushed the minimize-pre-allocations-on-grouping branch from b9697da to 2c7576c Compare February 9, 2026 01:17
@marioevz marioevz requested a review from spencer-tb February 9, 2026 01:49
@marioevz marioevz marked this pull request as ready for review February 9, 2026 01:49
@marioevz
Copy link
Member Author

marioevz commented Feb 9, 2026

@danceratopz @spencer-tb I added another very nice enhancement that virtually makes the pytest.mark.pre_alloc_group marker obsolete (🎉 ), please take a look and let me know what you think!

@codecov
Copy link

codecov bot commented Feb 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.07%. Comparing base (342c7bc) to head (ce17f48).
⚠️ Report is 6 commits behind head on forks/amsterdam.

Additional details and impacted files
@@               Coverage Diff                @@
##           forks/amsterdam    #2139   +/-   ##
================================================
  Coverage            86.07%   86.07%           
================================================
  Files                  599      599           
  Lines                39472    39472           
  Branches              3780     3780           
================================================
  Hits                 33977    33977           
  Misses                4862     4862           
  Partials               633      633           
Flag Coverage Δ
unittests 86.07% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

) -> None:
"""Set account associated with an address."""
raise ValueError(
"Tests are not allowed to set pre-alloc items in execute mode"
Copy link
Member Author

Choose a reason for hiding this comment

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

To reviewers: This check has been moved to SharedAlloc in the form of flags (see alloc_flags fixture for the skipping logic).

)
sender = pre.fund_eoa()
tx_value = 1
pre.fund_address(sender, tx_value)
Copy link
Member Author

Choose a reason for hiding this comment

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

Note to reviewers: execute command handles this automatically since #1822 and for fill I think we should simply increment the default value if for some reason we come up with a test that requires more balance.

Comment on lines +112 to +115
if self._deleted_addresses:
buffer += b"\1"
for deleted_address in sorted(self._deleted_addresses):
buffer += deleted_address
Copy link
Member Author

Choose a reason for hiding this comment

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

Note to reviewers: We could even completely ban this behavior and make this change a bit cleaner. In cases where we "remove" system contracts from the state we don't really remove them, we instead do pre[SYSTEM_CONTRACT_ADDRESS] = Account(<empty account>) to overwrite it.

Copy link
Contributor

@fselmo fselmo left a comment

Choose a reason for hiding this comment

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

This is a lot to review but I think it looks good. Really clever change that makes a lot of sense 🔥.

It's really difficult to know though, imo, if this will create any friction with execute remote. I think this is "less crucial" of a way to test (and more rnd) and we can certainly fix any quirks that bubble up after this PR is in... but I do wonder if there aren't some basic sanity check unit tests we can write for some of these methods that guarantee we are not introducing bugs to this functionality. I think this would be pretty difficult though... curious if others have any ideas here.

I asked Claude to look at this and I think there's a potential bug with setting self[address] instead of using __internal_set_item__(address, ...). I couldn't reproduce it in kurtosis bc I think it'd only be present in some very unique scenario... but asking here to make sure.

Everything else looks pretty good to me. This will be very nice to get in 👍🏼

account = self[address]
if account is not None:
account.balance = ZeroPaddedHexNumber(new_balance)
self[address] = account.copy(balance=new_balance)
Copy link
Contributor

Choose a reason for hiding this comment

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

Clarifying q... doesn't this also need to use __internal_set_item__(address, ...) in order to actually run through the RPC funding logic?

It doesn't seem easy, but do you think it's possible to write basic, sanity checking unit tests to make sure these execute mode expectations don't break? It would make reviews easier to have confidence in imo as it's not so straightforward / quick to test execute remote things as a reviewer.

account = self[address]
if account is not None:
account.balance = ZeroPaddedHexNumber(current_balance)
self[address] = account.copy(balance=current_balance)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thought here about using __internal_setitem__()

"""
Deploy a contract to the allocation.

Warning: `address` parameter is a temporary solution to allow tests to
Copy link
Contributor

Choose a reason for hiding this comment

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

Stray thought... I actually only now realized this is supposed to be temporary. It looks like only one test uses this and it makes sense. Would this be a good time to change this to something like _address (private attr... do not use unless you know what you're doing kind of thing?). I feel like it may be easy for a test that uses this to sneak by if it's a public property on pre.deploy_contract().


model_config = {
**CamelModel.model_config,
"frozen": True,
Copy link
Contributor

Choose a reason for hiding this comment

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

👌🏼

Copy link
Contributor

@fselmo fselmo Feb 9, 2026

Choose a reason for hiding this comment

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

Nothing to add here. Just a thought that since this is frozen I think we could probably cache the hash in case we ever end up calling it more than once we don't end up hashing it multiple times. I'm pretty sure the happy path in this PR only calls it once though so consider this just a nit.

Co-authored-by: felipe <fselmo2@gmail.com>
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.

4 participants