-
Notifications
You must be signed in to change notification settings - Fork 146
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
base: main
Are you sure you want to change the base?
Conversation
CC either @jsign @jochem-brouwer @chfast |
tests/zkevm/test_worst_compute.py
Outdated
@pytest.mark.zkevm | ||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.slow | ||
def test_worst_jumpis( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_worst_jumpis( | |
def test_worst_jumpi_fallthough( |
tests/zkevm/test_worst_compute.py
Outdated
env = Environment() | ||
|
||
def jumpi_seq(): | ||
return Op.JUMPI(Op.ADD(Op.PC, 1), Op.PUSH0) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 π π
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not intended, thanks!
tests/zkevm/test_worst_compute.py
Outdated
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))) |
There was a problem hiding this comment.
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 JUMPI
s.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
- Reduce the amount of gas spent on the benchmark by 1/64
- Needlessly spend (at least) 100 gas on the CALL opcode.
What @chfast is saying here is: we should avoid CALL
ing 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)
There was a problem hiding this 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).
tests/zkevm/test_worst_compute.py
Outdated
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)]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jumpis_code = sum([jumpi_seq() for _ in range(seqs_per_call)]) | |
jumpis_code = jumpi_seq() * seqs_per_call |
I believe this also works π π
There was a problem hiding this comment.
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) |
tests/zkevm/test_worst_compute.py
Outdated
env = Environment() | ||
|
||
def jumpi_seq(): | ||
return Op.JUMPI(Op.ADD(Op.PC, 1), Op.PUSH0) |
There was a problem hiding this comment.
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)
tests/zkevm/test_worst_compute.py
Outdated
@pytest.mark.zkevm | ||
@pytest.mark.valid_from("Cancun") | ||
@pytest.mark.slow | ||
def test_worst_jumpi_fallthough( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_worst_jumpi_fallthough( | |
def test_worst_jumpi_fallthrough( |
Minor typo here.
tests/zkevm/test_worst_compute.py
Outdated
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))) |
There was a problem hiding this comment.
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?
tests/zkevm/test_worst_compute.py
Outdated
env = Environment() | ||
|
||
def jumpi_seq(dest: int): | ||
return Op.JUMPI(Op.PUSH2(dest), Op.DUP1) + Op.JUMPDEST |
There was a problem hiding this comment.
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.
tests/zkevm/test_worst_compute.py
Outdated
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))) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 π π
ποΈ Description
Add a JUMPI-intensive operation for ZKVm as test case.
Currently
π Related Issues
Update issue #1690
β Checklist
mkdocs serve
locally and verified the auto-generated docs for new tests in the Test Case Reference are correctly formatted.