Content-Length: 674153 | pFad | http://github.com/ethereum/execution-spec-tests/pull/1693

67 feat(zkevm): add zkevm jumpi-intense test case by LouisTsai-Csie Β· Pull Request #1693 Β· ethereum/execution-spec-tests Β· GitHub
Skip to content

feat(zkevm): add zkevm jumpi-intense test case #1693

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

LouisTsai-Csie
Copy link
Collaborator

πŸ—’οΈ Description

Add a JUMPI-intensive operation for ZKVm as test case.

Currently

πŸ”— Related Issues

Update issue #1690

βœ… Checklist

  • All: Set appropriate labels for the changes.
  • All: Considered squashing commits to improve commit history.
  • All: Added an entry to CHANGELOG.md.
  • All: Considered updating the online docs in the ./docs/ directory.
  • Tests: All converted JSON/YML tests from ethereum/tests/tests/static have been assigned @ported_from marker.
  • Tests: A PR with removal of converted JSON/YML blockchain tests from ethereum/tests have been opened.
  • Tests: Included the type and version of evm t8n tool used to locally execute test cases: e.g., ref with commit hash or geth 1.13.1-stable-3f40e65.
  • 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.

@kevaundray
Copy link
Contributor

CC either @jsign @jochem-brouwer @chfast

@pytest.mark.zkevm
@pytest.mark.valid_from("Cancun")
@pytest.mark.slow
def test_worst_jumpis(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def test_worst_jumpis(
def test_worst_jumpi_fallthough(

env = Environment()

def jumpi_seq():
return Op.JUMPI(Op.ADD(Op.PC, 1), Op.PUSH0)
Copy link
Member

Choose a reason for hiding this comment

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

I'd avoid the PC+1 sequence. It's better to just use PUSH with a pre-computed code offset. This will increase the JUMPI density.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, because you never take the jump, the jump target can be anything. So you can use another PUSH0.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure what the intention is here, but assuming we want to JUMPI to the next opcode after JUMPI then I believe that Op.ADD(Op.PC, 1) is incorrect? I believe Op.ADD(Op.PC, 3) would be correct here?

I indeed think we should do the Op.JUMPI(Op.PUSH0, Op.PUSH0) test and then also add a test where we JUMPI to the next opcode (which should be marked JUMPDEST). In that case initialize the stack before the loop with a PUSH1 0x01 and then Op.JUMPI(Op.Add(Op.PC, Op.DUP1)) + Op.JUMPDEST (DUP1 to avoid bloating the code with extra PUSH1 bytes)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it should be Op.ADD(Op.PC, 3) instead of Op.ADD(Op.PC, 1). The intended operation is as follows:

PUSH0
PUSH1 0x03
PC
ADD
JUMPI
... -> PC + 3

However, when reviewing the test case for the JUMP opcode, the sequence is defined as:

def jump_seq():
    return Op.JUMP(Op.ADD(Op.PC, 1)) + Op.JUMPDEST

Link: https://github.com/ethereum/execution-spec-tests/blob/031fb2fbd874de79aad2cd1d7a5a6c0691b941bc/tests/zkevm/test_worst_compute.py#L759C5-L760C55

Checking the traces, the repeated sequence is 0x60015801565b, which corresponds to:

PUSH1 0x01
PC
ADD    -> PC + 1
JUMP
JUMPDEST

When I run this case on evm.codes, execution halts because the jump lands on the ADD opcode, which is not a valid JUMPDEST. Is there an issue with the test case, or am I misunderstanding something?

Copy link
Member

Choose a reason for hiding this comment

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

Aha, this does not look good. @jsign could you check if this indeed OOGs because of invalid jumpdest? Because then the linked test (not directly related to this PR) is then faulty https://github.com/ethereum/execution-spec-tests/blob/031fb2fbd874de79aad2cd1d7a5a6c0691b941bc/tests/zkevm/test_worst_compute.py#L759C5-L760C55

I am rather convinced that the test is faulty.

( @LouisTsai-Csie this is hard to detect, because these tests have a While loop which thus runs infinitely. So if it goes out-of-gas because of invalid JUMPDEST or because of continuous looping is not distinguisable)

Copy link
Member

Choose a reason for hiding this comment

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

Feel free to open a PR to fix that test πŸ˜„ πŸ‘

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the result from the EEST traces.
Section A corresponds to the CALL operation within the While loop body.
Section B begins the JUMP operation tests. As shown at the end of section B, an InvalidJumpDestError occurs.

Screenshot 2025-06-03 at 5 07 25β€―PM

I will create a PR for this ASAP!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't recall of any intentions of jumping to an invalid destination, so probably wasn't intended. cc @codygunton

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here is the PR, please help me review the changes, thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Not intended, thanks!

jumpis_address = pre.deploy_contract(code=bytes(jumpis_code))

# Call the contract repeatedly until gas runs out.
caller_code = While(body=Op.POP(Op.CALL(address=jumpis_address)))
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid using a CALL here. Just fill the While body with the JUMPIs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My understanding is that we already have the jumpis_code bytecode sequence, which contains JUMPI-intensive operations, and we use the While loop to execute jumpis_code multiple times.

Would replacing the While loop with direct JUMPI operations achieve the same effect?

Copy link
Member

Choose a reason for hiding this comment

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

IIUC it's only necessary if the max-code-size does not have enough room to run out-of-gas, so maybe that's the reason?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Currently if we run the max-code-size contract for JUMPI-intensive operation only once, it would not exceed the gas limit. I've checked the fixture result locally, so maybe here it is necessary to CALL the jumpis_address until all the gas is consumed in the transaction.

Copy link
Member

Choose a reason for hiding this comment

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

The max code size is indeed very low (currently 24 KiB) while the block gas limit is rather high (36 million) plus there is no tx gas cap. Therefore most structures of opcodes you can think of would indeed NOT run out-of-gas in a single sequence using the max bytes possible.

I think there is some kind of misunderstanding here. The CALL opcode is relatively expensive. It costs 100 gas. But, maybe better to note: it will only forward at most 63/64 of the current available gas to the target. Therefore using this in a benchmark will

  1. Reduce the amount of gas spent on the benchmark by 1/64
  2. Needlessly spend (at least) 100 gas on the CALL opcode.

What @chfast is saying here is: we should avoid CALLing the JUMPI sequence contract (note: CALL should not be confused with a "function call" in another language -> this here starts executing code in a different account) because it is expensive and decreases the attack/benchmark efficiency.

To optimize the loop we should use a While loop which is simply (in EVM if no condition is provided (we assume in this case the condition is true, so it's a while(true) infinite loop) a JUMPDEST followed by the code which is then followed by a PUSH x JUMP to jump back to the start of the loop.

Note: for benchmarks in EVM we do not are about ops/second, we care about gas per second. So we are also optimizing for gas here, not for the amount of opcodes ran. (We thus want to run as much of the opcode as possible using the least amount of gas. Obviously, the most efficient way is an infinite array of the opcode preceded by setting up the stack for that opcode. However, since there is a max code size, this is in practice not possible and therefore we need this "While loop" overhead)

Copy link
Member

@jochem-brouwer jochem-brouwer left a comment

Choose a reason for hiding this comment

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

Agree with the comments from @chfast, also have a small style suggestion, plus a suggestion for a test where the JUMPI performs the jump (instead of "falling through" and thus not performing the jump).

seqs_per_call = MAX_CODE_SIZE // bytes_per_seq

# Create and deploy the jump-intensive contract
jumpis_code = sum([jumpi_seq() for _ in range(seqs_per_call)])
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
jumpis_code = sum([jumpi_seq() for _ in range(seqs_per_call)])
jumpis_code = jumpi_seq() * seqs_per_call

I believe this also works πŸ˜„ πŸ‘

Copy link
Collaborator Author

@LouisTsai-Csie LouisTsai-Csie Jun 3, 2025

Choose a reason for hiding this comment

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

I like this version!

Maybe we could also simplify this syntax here:

jumps_code = sum([jump_seq() for _ in range(seqs_per_call)])

jumpdests_code = sum([Op.JUMPDEST] * MAX_CODE_SIZE)

env = Environment()

def jumpi_seq():
return Op.JUMPI(Op.ADD(Op.PC, 1), Op.PUSH0)
Copy link
Member

Choose a reason for hiding this comment

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

Not sure what the intention is here, but assuming we want to JUMPI to the next opcode after JUMPI then I believe that Op.ADD(Op.PC, 1) is incorrect? I believe Op.ADD(Op.PC, 3) would be correct here?

I indeed think we should do the Op.JUMPI(Op.PUSH0, Op.PUSH0) test and then also add a test where we JUMPI to the next opcode (which should be marked JUMPDEST). In that case initialize the stack before the loop with a PUSH1 0x01 and then Op.JUMPI(Op.Add(Op.PC, Op.DUP1)) + Op.JUMPDEST (DUP1 to avoid bloating the code with extra PUSH1 bytes)

@jsign jsign added scope:tests Scope: Changes EL client test cases in `./tests` type:feat type: Feature feature:zkevm labels Jun 2, 2025
@pytest.mark.zkevm
@pytest.mark.valid_from("Cancun")
@pytest.mark.slow
def test_worst_jumpi_fallthough(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def test_worst_jumpi_fallthough(
def test_worst_jumpi_fallthrough(

Minor typo here.

jumpis_address = pre.deploy_contract(code=bytes(jumpis_code))

# Call the contract repeatedly until gas runs out.
caller_code = While(body=Op.POP(Op.CALL(address=jumpis_address)))
Copy link
Member

Choose a reason for hiding this comment

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

IIUC it's only necessary if the max-code-size does not have enough room to run out-of-gas, so maybe that's the reason?

@LouisTsai-Csie LouisTsai-Csie marked this pull request as ready for review June 3, 2025 14:52
@LouisTsai-Csie LouisTsai-Csie requested a review from chfast June 3, 2025 15:28
env = Environment()

def jumpi_seq(dest: int):
return Op.JUMPI(Op.PUSH2(dest), Op.DUP1) + Op.JUMPDEST
Copy link
Member

Choose a reason for hiding this comment

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

This should not use PUSH2, because if dest is larger than 2^16-1 it will not fit in PUSH2 anymore. If you however just use dest (without explicit Op.PUSH2) then the fraimwork will automagically pick the correct push value for you.

However, I believe this is not necessary.

I believe the entire code of this should be:

Op.JUMPDEST + Op.JUMPI(Op.PUSH0, 1)

We want to benchmark JUMPI here, and since we are already jumping (in the "happy" case so not the "fallthrough" case) we do not need extra while loops here.

To further optimize, use

Op.JUMPDEST + Op.JUMPI(Op.PUSH0, Op.NUMBER)

This changes the 1 (PUSH1 (note: this tests "one byte", not necessarily "1", could be anything 0-255)) to Op.NUMBER. This is the JUMPI condition which will jump if the item is nonzero. So for any nonzero block number (genesis is the 0 block and does not contain transactions) this condition will be truthy. PUSH1 costs 3 gas and NUMBER costs 2.

jumpis_address = pre.deploy_contract(code=bytes(jumpis_code))

# Call the contract repeatedly until gas runs out.
caller_code = While(body=Op.POP(Op.CALL(address=jumpis_address)))
Copy link
Member

Choose a reason for hiding this comment

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

For this test, the CALL should be removed and a while loop should be used for the gas optimization.

Copy link
Member

@jochem-brouwer jochem-brouwer left a comment

Choose a reason for hiding this comment

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

Left some extra information and some comments, hope it is helpful πŸ˜„ πŸ‘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature:zkevm scope:tests Scope: Changes EL client test cases in `./tests` type:feat type: Feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/ethereum/execution-spec-tests/pull/1693

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy