|
6 | 6 | """
|
7 | 7 |
|
8 | 8 | import math
|
| 9 | +import random |
9 | 10 |
|
10 | 11 | import pytest
|
11 | 12 |
|
|
21 | 22 | While,
|
22 | 23 | )
|
23 | 24 | from ethereum_test_tools.vm.opcode import Opcodes as Op
|
| 25 | +from ethereum_test_vm import Opcode |
24 | 26 |
|
25 | 27 | REFERENCE_SPEC_GIT_PATH = "TODO"
|
26 | 28 | REFERENCE_SPEC_VERSION = "TODO"
|
@@ -553,3 +555,92 @@ def test_worst_unop(
|
553 | 555 | post={},
|
554 | 556 | blocks=[Block(txs=[tx])],
|
555 | 557 | )
|
| 558 | + |
| 559 | + |
| 560 | +@pytest.mark.valid_from("Cancun") |
| 561 | +@pytest.mark.parametrize("shift_right", [Op.SHR, Op.SAR]) |
| 562 | +def test_worst_shifts( |
| 563 | + blockchain_test: BlockchainTestFiller, |
| 564 | + pre: Alloc, |
| 565 | + shift_right: Op, |
| 566 | +): |
| 567 | + """ |
| 568 | + Test running a block with as many shift instructions with non-trivial arguments. |
| 569 | + This test generates left-right pairs of shifts to avoid zeroing the argument. |
| 570 | + The shift amounts are randomly pre-selected from the constant pool of 15 values on the stack. |
| 571 | + """ |
| 572 | + |
| 573 | + def to_signed(x): |
| 574 | + return x if x < 2**255 else x - 2**256 |
| 575 | + |
| 576 | + def to_unsigned(x): |
| 577 | + return x if x >= 0 else x + 2**256 |
| 578 | + |
| 579 | + def shr(x, s): |
| 580 | + return x >> s |
| 581 | + |
| 582 | + def shl(x, s): |
| 583 | + return x << s |
| 584 | + |
| 585 | + def sar(x, s): |
| 586 | + return to_unsigned(to_signed(x) >> s) |
| 587 | + |
| 588 | + match shift_right: |
| 589 | + case Op.SHR: |
| 590 | + shift_right_fn = shr |
| 591 | + case Op.SAR: |
| 592 | + shift_right_fn = sar |
| 593 | + case _: |
| 594 | + raise ValueError(f"Unexpected shift op: {shift_right}") |
| 595 | + |
| 596 | + rng = random.Random(1) # Use random with a fixed seed. |
| 597 | + initial_value = 2**256 - 1 # The initial value to be shifted; should be negative for SAR. |
| 598 | + |
| 599 | + # Create the list of shift amounts if length 15 (max reachable by DUPs instructions). |
| 600 | + # For the worst case keep the values small and omit values divisible by 8. |
| 601 | + shift_amounts = [x + (x >= 8) + (x >= 15) for x in range(1, 16)] |
| 602 | + |
| 603 | + code_prefix = sum(Op.PUSH1[sh] for sh in shift_amounts) + Op.JUMPDEST + Op.CALLDATALOAD(0) |
| 604 | + code_suffix = Op.POP + Op.JUMP(len(shift_amounts) * 2) |
| 605 | + code_body_len = MAX_CODE_SIZE - len(code_prefix) - len(code_suffix) |
| 606 | + |
| 607 | + def select_shift_amount(shift_fn, v): |
| 608 | + """Select a shift amount that will produce a non-zero result.""" |
| 609 | + while True: |
| 610 | + index = rng.randint(0, len(shift_amounts) - 1) |
| 611 | + sh = shift_amounts[index] |
| 612 | + new_v = shift_fn(v, sh) % 2**256 |
| 613 | + if new_v != 0: |
| 614 | + return new_v, index |
| 615 | + |
| 616 | + def make_dup(i): |
| 617 | + """Create a DUP instruction to get the i-th shift amount constant from the stack.""" |
| 618 | + # TODO: Create a global helper for this. |
| 619 | + return Opcode(0x80 + (len(shift_amounts) - i)) |
| 620 | + |
| 621 | + code_body = Bytecode() |
| 622 | + v = initial_value |
| 623 | + while len(code_body) <= code_body_len - 4: |
| 624 | + v, i = select_shift_amount(shl, v) |
| 625 | + code_body += make_dup(i) + Op.SHL |
| 626 | + v, i = select_shift_amount(shift_right_fn, v) |
| 627 | + code_body += make_dup(i) + shift_right |
| 628 | + |
| 629 | + code = code_prefix + code_body + code_suffix |
| 630 | + assert len(code) == MAX_CODE_SIZE - 2 |
| 631 | + |
| 632 | + env = Environment() |
| 633 | + |
| 634 | + tx = Transaction( |
| 635 | + to=pre.deploy_contract(code=code), |
| 636 | + data=initial_value.to_bytes(32, byteorder="big"), |
| 637 | + gas_limit=env.gas_limit, |
| 638 | + sender=pre.fund_eoa(), |
| 639 | + ) |
| 640 | + |
| 641 | + blockchain_test( |
| 642 | + env=env, |
| 643 | + pre=pre, |
| 644 | + post={}, |
| 645 | + blocks=[Block(txs=[tx])], |
| 646 | + ) |
0 commit comments