Skip to content

zkevm: keccak worst-case #1497

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

Draft
wants to merge 4 commits into
base: jsign-zkvm-bytecode-worstcase
Choose a base branch
from

Conversation

jsign
Copy link

@jsign jsign commented Apr 25, 2025

This PR adds a test that builds a block to maximize the number of keccak permutations for a given gas limit (considering the max contract size).

There are two main aspects of the test design.

Find the optimal input size

The metric we care about is gas per permutation, not simply max opcode calls. Although the dynamic cost of KECCAK256 is linear in input length, memory expansion is quadratic, so there is a sweet spot for minimal cost per permutation.

Instead of baking magic numbers, the test does a scan for the optimal cost and then does the attack with it. This is much more transparent on how this optimal length is found, plus it sounds also more future-proof.

Another way of understanding the above is by looking at this chart I created from that same script, with a more detailed output:
image

The “attack” loop

Once the optimal length is known, I create a loop that drives as many permutations as the block gas limit allows. The loop shouldn't be a tight loop, but quite the opposite -- run as many calls as possible within the max contract size, so the "JUMP"-like gas overhead is amortized as much as possible.

The general structure is:

JUMPDEST
 (PUSH+PUSH+KECCAK256+POP)
 ...
PUSH0
JUMP

Note that for 36M you can fit the (....) without a loop since the max you can add fits within 24KiB contract limit. But for bigger gas limits, you run short with 24KiB and need the loop!

Max permutations per gas limit

The test runs with different gas limits. Here are the numbers of permutations generated for each limit:

  • 30M -> 1_408_440 perms
  • 60M -> 2_348_622 perms
  • 100M -> 3_915_972 perms
  • 300M -> 11_754_358 perms

Note: The input now is 0x00s, but it could be different. I think ideal test filling should be deterministic, so if any implementation wants to overoptimize, it can always do it. We can wait and adjust if necessary.


zkVM cycles:

  • 36m gas -> 623 million cycles
  • 60m gas -> 1 billion cycles
  • 100m gas -> 1.7 billion cycles
  • 300m gas -> 5.2 billion cycles

Signed-off-by: Ignacio Hagopian <[email protected]>
Comment on lines +77 to +78
# TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the attack,
# whenever this is fixed adjust accordingly.
Copy link
Author

Choose a reason for hiding this comment

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

To clarify a bit more here. When doing Op.SHA3(0, ..) the generated opcodes include a PUSH1 0 which could save 1 gas if used PUSH0.

Note a huge deal, but just noticed that.

jsign and others added 3 commits April 25, 2025 16:34
Signed-off-by: Ignacio Hagopian <[email protected]>
Signed-off-by: Ignacio Hagopian <[email protected]>
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.

2 participants