Skip to content

test(osaka): add edge case test vectors for EIP-7883 MODEXP gas calculation #1993

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
merged 2 commits into from
Aug 7, 2025

Conversation

bshastry
Copy link
Contributor

@bshastry bshastry commented Aug 6, 2025

πŸ—’οΈ Description

This PR adds 16 test vectors for EIP-7883 (MODEXP gas increase) to address gaps in edge case coverage. The existing test suite only covers standard cryptographic parameter sizes but misses important corner cases in the gas calculation formula that could lead to consensus differences between EVM implementations.

Key additions:

  • Zero exponent cases: Tests for both ≀32 byte and >32 byte zero exponents
  • Unequal base/modulus lengths: Tests the max(base_length, modulus_length) calculation
  • Small inputs (<32 bytes): Tests the multiplication_complexity = 16 threshold
  • Boundary cases: Tests at critical 32-byte threshold (31, 32, 33 bytes)
  • Word boundary cases: Tests at 7, 9 byte boundaries for ceil(length/8) calculation
  • Large exponent edge cases: Tests for MSB position and the 16*(exp_len-32) formula component

All gas values have been calculated using the exact formulas from spec.py to ensure accuracy.

πŸ”— Related Issues or PRs

N/A.

βœ… Checklist

  • All: Ran fast tox checks to avoid unnecessary CI fails, see also
    https://eest.ethereum.org/main/getting_started/code_standards/ and
    https://eest.ethereum.org/main/dev/precommit/:
    uvx --with=tox-uv tox -e lint,typecheck,spellcheck,markdownlint
  • All: PR title adheres to the https://eest.ethereum.org/main/getting_started/contributing/?h=
    contri#commit-messages-issue-and-pr-titles - it will be used as the squash commit message and
    should start type(scope):.
  • All: Considered adding an entry to
    /ethereum/execution-spec-tests/blob/main/docs/CHANGELOG.md.
  • All: Considered updating the online docs in the
    /ethereum/execution-spec-tests/blob/main/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
    https://eest.ethereum.org/main/tests/ are correctly formatted.
  • Tests: For PRs implementing a missed test case, update the
    /ethereum/execution-spec-tests/blob/main/docs/writing_tests/post_mortems.md to add an entry
    the list.
  • Ported Tests: All converted JSON/YML tests from /ethereum/tests or
    /ethereum/execution-spec-tests/blob/main/tests/static have been assigned @ported_from marker.

@LouisTsai-Csie
Copy link
Collaborator

Thanks for the PR! I’ve reviewed the CI failure, and it looks like the current gas calculation is incorrect. IMO this is related to an issue in our testing helper. We’re refactoring eip-7883 tests in a separate PR to resolve the issue.

We’ve also added some additional test cases for the gas formula that might be relevant, feel free to take a look here.

@marioevz marioevz mentioned this pull request Aug 6, 2025
6 tasks
@bshastry
Copy link
Contributor Author

bshastry commented Aug 6, 2025

Thanks for the PR! I’ve reviewed the CI failure, and it looks like the current gas calculation is incorrect. IMO this is related to an issue in our testing helper. We’re refactoring eip-7883 tests in a separate PR to resolve the issue.

We’ve also added some additional test cases for the gas formula that might be relevant, feel free to take a look here.

Thank you for the pointers, I will check them out.

In the meantime, I realized that the 7883 spec calculates iteration count slightly differently than

if bits_part > 0:
bits_part -= 1

Essentially, the spec says subtract 1 from exponent bit length whether the bit length is zero or not, but your impl interprets it as "do this only if bit length is positive". This would result in off-by-one iteration counts for large exponents (e.g., 512 byte) whose value is zero. Is this accurate and something that was agreed upon?

@spencer-tb
Copy link
Contributor

Essentially, the spec says subtract 1 from exponent bit length whether the bit length is zero or not, but your impl interprets it as "do this only if bit length is positive". This would result in off-by-one iteration counts for large exponents (e.g., 512 byte) whose value is zero. Is this accurate and something that was agreed upon?

Nice! That a bug in our spec I believe. Will be prioritizing this tmo. Do you have the code you used to generate these vectors?

@spencer-tb
Copy link
Contributor

spencer-tb commented Aug 6, 2025

For more context here is the EELS spec we are using to fill the tests so there could be inconsistencies there:
https://github.com/ethereum/execution-specs/blob/forks/osaka/src/ethereum/osaka/vm/precompiled_contracts/modexp.py

I see the same within the EELS spec:

 if bits_part > 0: 
     bits_part -= 1 

Copy link
Member

@marioevz marioevz left a comment

Choose a reason for hiding this comment

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

Thanks for the tests.

I get a lot of gas price mismatches, and some of the inputs do not totally make sense to me, could you please double check if the inputs of the values are actually intended?

@LouisTsai-Csie
Copy link
Collaborator

If you want to test the vectors, make sure the 0x prefix is removed from the input. You can refer to the other vectors as examples.

And you can run the command for test:
uv run fill -v tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py::test_vectors_from_file --fork Osaka --clean

@bshastry
Copy link
Contributor Author

bshastry commented Aug 7, 2025

For more context here is the EELS spec we are using to fill the tests so there could be inconsistencies there: https://github.com/ethereum/execution-specs/blob/forks/osaka/src/ethereum/osaka/vm/precompiled_contracts/modexp.py

I see the same within the EELS spec:

 if bits_part > 0: 
     bits_part -= 1 

Thanks for the pointer. One more possibly minor issue in spec wording:

From EIP 7883

elif exponent_length > 32: iteration_count = (16 * (exponent_length - 32)) + ((exponent & (2**256 - 1)).bit_length() - 1)

does not make it explicit that we are to take the most significant word (first 32 bytes) of the exponent while computing the U256 masked bit length. My first interpretation was actually to compute the bit length of the last 32 bytes of the exponent. However EELS and clients consistently seem to use the first 32 byte logic.

https://github.com/ethereum/execution-specs/blob/87a9e65034572843b60ea717218811de0c952402/src/ethereum/osaka/vm/precompiled_contracts/modexp.py#L45-L47

@bshastry
Copy link
Contributor Author

bshastry commented Aug 7, 2025

Thank for your your detailed review @marioevz and others! I have updated the test vectors accordingly. Hope they are okay.

Here is the validation script and summary:

Validation script: https://gist.github.com/bshastry/323aacbe95044a9fb31212ccfaf7ec2d
Validation summary: https://gist.github.com/bshastry/b691814dca3cd153178c9049f14d2b56
Motivation for these test vectors: https://gist.github.com/bshastry/9860aeccea0b215bda2954de1f96b413

@bshastry bshastry requested a review from marioevz August 7, 2025 12:13
@bshastry
Copy link
Contributor Author

bshastry commented Aug 7, 2025

If you want to test the vectors, make sure the 0x prefix is removed from the input. You can refer to the other vectors as examples.

And you can run the command for test: uv run fill -v tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py::test_vectors_from_file --fork Osaka --clean

Curious what the fill command actually does? Does it run the test input against multiple EL clients? I am asking because I got the expected output wrong in an earlier iteration of this PR but the fill command finished successfully leading me to believe the vectors were sound :-)

@spencer-tb
Copy link
Contributor

spencer-tb commented Aug 7, 2025

In the meantime, I realized that the 7883 spec calculates iteration count slightly differently than

if bits_part > 0:
bits_part -= 1

Essentially, the spec says subtract 1 from exponent bit length whether the bit length is zero or not, but your impl interprets it as "do this only if bit length is positive". This would result in off-by-one iteration counts for large exponents (e.g., 512 byte) whose value is zero. Is this accurate and something that was agreed upon?

Following up on this. It seems that this is acutally correct in EELS and the test framework, based on consensus. The existing gas calculations and tests against clients agree here too.

Geth implementation:

// Calculate the adjusted exponent length
var msb int
if bitlen := expHead.BitLen(); bitlen > 0 {
    msb = bitlen - 1
}

Nethermind implementation:

  uint bitLength = (uint)exponent.BitLen;
  if (bitLength > 0)
  {
      bitLength--;
  }

The original EIP-198 additionally states this:

ADJUSTED_EXPONENT_LENGTH is defined as follows.

  • If length_of_EXPONENT <= 32, and all bits in EXPONENT are 0, return 0
  • If length_of_EXPONENT <= 32, then return the index of the highest bit in EXPONENT (eg. 1 -> 0, 2 -> 1, 3 -> 1, 255 -> 7, 256 -> 8).

So we should consider updating the EIP-7883/2565 psuedocode to be more consitent with the original EIP-198 spec definition.

I think in the EIP-2565 spec we should update:

elif exponent_length <= 32: iteration_count = exponent.bit_length() - 1
elif exponent_length > 32: iteration_count = (8 * (exponent_length - 32)) + ((exponent & (2**256 - 1)).bit_length() - 1)

To the following:

elif exponent_length <= 32: 
    bit_length = exponent.bit_length()
    iteration_count = bit_length - 1 if bit_length > 0 else 0
elif exponent_length > 32: 
    exponent_head_bit_length = (exponent & (2**256 - 1)).bit_length()
    iteration_count = (8 * (exponent_length - 32)) + (exponent_head_bit_length - 1 if exponent_head_bit_length > 0 else 0)

And for EIP-7883:

elif exponent_length <= 32: iteration_count = exponent.bit_length() - 1
elif exponent_length > 32: iteration_count = (16 * (exponent_length - 32)) + ((exponent & (2**256 - 1)).bit_length() - 1)

To the following:

elif exponent_length <= 32: 
    bit_length = exponent.bit_length()
    iteration_count = bit_length - 1 if bit_length > 0 else 0
elif exponent_length > 32: 
    exponent_head_bit_length = (exponent & (2**256 - 1)).bit_length()
    iteration_count = (16 * (exponent_length - 32)) + (exponent_head_bit_length - 1 if exponent_head_bit_length > 0 else 0)

@spencer-tb
Copy link
Contributor

If you want to test the vectors, make sure the 0x prefix is removed from the input. You can refer to the other vectors as examples.
And you can run the command for test: uv run fill -v tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py::test_vectors_from_file --fork Osaka --clean

Curious what the fill command actually does? Does it run the test input against multiple EL clients? I am asking because I got the expected output wrong in an earlier iteration of this PR but the fill command finished successfully leading me to believe the vectors were sound :-)

The fill command only generates the test fixtures using a reference client implementation (EELS is the default for us). We have some checks within the test framework when generating the tests. That's all!

These generated json fixtures must then be executed against an EL client. Consume engine via hive is the defacto standard we use. More context in the docs: https://eest.ethereum.org/main/running_tests/hive/common_options/

@LouisTsai-Csie
Copy link
Collaborator

I've checked it on my PR and it works as expected now

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.

LGTM! All filling fine from my side.

@spencer-tb
Copy link
Contributor

Nice! These caught an issue in Reth/Besu (Osaka/7883) with zero-length-base-mod <3

@marioevz marioevz merged commit 2f97840 into ethereum:main Aug 7, 2025
15 checks passed
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