Skip to content

Commit f5385ce

Browse files
felix314159github-actions[bot]
authored andcommitted
feat(src): EIP-8024 tests added (#2021)
1 parent 58b4bb0 commit f5385ce

14 files changed

Lines changed: 2705 additions & 5 deletions

File tree

packages/testing/src/execution_testing/forks/forks/forks.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3349,6 +3349,36 @@ def is_deployed(cls) -> bool:
33493349
"""Return True if this fork is deployed."""
33503350
return False
33513351

3352+
@classmethod
3353+
def valid_opcodes(
3354+
cls, *, block_number: int = 0, timestamp: int = 0
3355+
) -> List[Opcodes]:
3356+
"""Return list of Opcodes that are valid to work on this fork."""
3357+
del block_number, timestamp
3358+
return [
3359+
Opcodes.SWAPN,
3360+
Opcodes.DUPN,
3361+
Opcodes.EXCHANGE,
3362+
] + super(Amsterdam, cls).valid_opcodes()
3363+
3364+
@classmethod
3365+
def opcode_gas_map(
3366+
cls, *, block_number: int = 0, timestamp: int = 0
3367+
) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]:
3368+
"""Add Amsterdam opcodes gas costs."""
3369+
gas_costs = cls.gas_costs(
3370+
block_number=block_number, timestamp=timestamp
3371+
)
3372+
base_map = super(Amsterdam, cls).opcode_gas_map(
3373+
block_number=block_number, timestamp=timestamp
3374+
)
3375+
return {
3376+
**base_map,
3377+
Opcodes.SWAPN: gas_costs.G_VERY_LOW,
3378+
Opcodes.DUPN: gas_costs.G_VERY_LOW,
3379+
Opcodes.EXCHANGE: gas_costs.G_VERY_LOW,
3380+
}
3381+
33523382
@classmethod
33533383
def engine_new_payload_version(
33543384
cls, *, block_number: int = 0, timestamp: int = 0

packages/testing/src/execution_testing/vm/opcodes.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,163 @@ def __call__(self, *args_t: OpcodeCallArg, **kwargs: Any) -> Bytecode:
511511
return pre_opcode_bytecode + self
512512

513513

514+
def _exchange_encoder(*args: int | bytes) -> bytes:
515+
"""
516+
Encoder for EXCHANGE opcode following encode_pair logic from EIP-8024.
517+
518+
Supports two modes:
519+
1. bytes input: Returns verbatim (for testing invalid immediate bytes)
520+
2. int input(s): Validates and encodes using encode_pair logic
521+
522+
Parameters
523+
----------
524+
*args : int | bytes
525+
Either bytes (returned verbatim) or 1-2 ints (encoded)
526+
527+
Returns
528+
-------
529+
bytes
530+
The immediate byte for EXCHANGE opcode
531+
532+
"""
533+
# If bytes are provided, return them verbatim (for testing invalid ranges)
534+
if len(args) == 1 and isinstance(args[0], bytes):
535+
return args[0]
536+
537+
# If one int is provided, it's the immediate byte value directly
538+
if len(args) == 1 and isinstance(args[0], int):
539+
return int.to_bytes(args[0], 1, "big")
540+
541+
# If two ints are provided, use encode_pair logic from EIP-8024
542+
if len(args) == 2:
543+
n, m = args
544+
if not isinstance(n, int) or not isinstance(m, int):
545+
raise TypeError(
546+
"EXCHANGE requires int arguments when using two parameters"
547+
)
548+
549+
# encode_pair logic from EIP-8024
550+
# n is first stack index (1-13), m is second (must be > n, up to 29)
551+
if not (1 <= n <= 13 and n < m <= 29 and n + m <= 30):
552+
raise ValueError(
553+
f"EXCHANGE indices must satisfy: 1 <= n <= 13, "
554+
f"n < m <= 29, n + m <= 30, got n={n}, m={m}"
555+
)
556+
if m <= 16:
557+
q, r = n - 1, m - 1
558+
else:
559+
q, r = 29 - m, n - 1
560+
k = 16 * q + r
561+
imm = k if k <= 79 else k + 48
562+
return int.to_bytes(imm, 1, "big")
563+
564+
raise ValueError(f"EXCHANGE requires 1 or 2 arguments, got {len(args)}")
565+
566+
567+
def _dupn_swapn_encoder(*args: int | bytes) -> bytes:
568+
"""
569+
Encoder for DUPN/SWAPN opcodes following encode_single logic from EIP-8024.
570+
571+
Supports two modes:
572+
1. bytes input: Returns verbatim (for testing invalid immediate bytes)
573+
2. int input: Validates and encodes using encode_single logic
574+
575+
Parameters
576+
----------
577+
*args : int | bytes
578+
Either bytes (returned verbatim) or a single int (encoded)
579+
580+
Returns
581+
-------
582+
bytes
583+
The immediate byte for DUPN/SWAPN opcode
584+
585+
"""
586+
if len(args) != 1:
587+
raise ValueError(
588+
f"DUPN/SWAPN requires exactly 1 argument, got {len(args)}"
589+
)
590+
591+
arg = args[0]
592+
593+
# If bytes are provided, return them verbatim (for testing invalid ranges)
594+
if isinstance(arg, bytes):
595+
return arg
596+
597+
# If int is provided, use encode_single logic from EIP-8024
598+
if isinstance(arg, int):
599+
# encode_single logic: n is stack index (17-235)
600+
if not (17 <= arg <= 235):
601+
raise ValueError(
602+
f"DUPN/SWAPN index must be in range [17, 235], got {arg}"
603+
)
604+
if arg <= 107:
605+
imm = arg - 17
606+
else:
607+
imm = arg + 20
608+
return int.to_bytes(imm, 1, "big")
609+
610+
raise TypeError(
611+
f"DUPN/SWAPN requires int or bytes argument, got {type(arg)}"
612+
)
613+
614+
615+
def _swapn_stack_properties_modifier(data: bytes) -> tuple[int, int, int, int]:
616+
n = int.from_bytes(data, "big")
617+
if n <= 90:
618+
min_stack_height = n + 17
619+
elif n >= 128:
620+
min_stack_height = n - 20
621+
else:
622+
# Undefined behavior
623+
min_stack_height = 0
624+
return (
625+
0,
626+
0,
627+
min_stack_height + 1,
628+
min_stack_height + 1,
629+
)
630+
631+
632+
def _dupn_stack_properties_modifier(data: bytes) -> tuple[int, int, int, int]:
633+
n = int.from_bytes(data, "big")
634+
if n <= 90:
635+
min_stack_height = n + 17
636+
elif n >= 128:
637+
min_stack_height = n - 20
638+
else:
639+
# Undefined behavior
640+
min_stack_height = 0
641+
return (
642+
0,
643+
1,
644+
min_stack_height,
645+
min_stack_height + 1,
646+
)
647+
648+
649+
def _exchange_stack_properties_modifier(
650+
data: bytes,
651+
) -> tuple[int, int, int, int]:
652+
n = int.from_bytes(data, "big")
653+
if n > 79 and n < 128:
654+
# Undefined behavior
655+
min_stack_height = 0
656+
else:
657+
k = n if n <= 79 else n - 48
658+
q, r = divmod(k, 16)
659+
if q < r:
660+
min_stack_height = max(q + 1, r + 1)
661+
else:
662+
min_stack_height = max(r + 1, 29 - q)
663+
return (
664+
0,
665+
0,
666+
min_stack_height + 1,
667+
min_stack_height + 1,
668+
)
669+
670+
514671
class Opcodes(Opcode, Enum):
515672
"""
516673
Enum containing all known opcodes.
@@ -5080,6 +5237,118 @@ class Opcodes(Opcode, Enum):
50805237
Source: [evm.codes/#A4](https://www.evm.codes/#A4)
50815238
"""
50825239

5240+
DUPN = Opcode(
5241+
0xE6,
5242+
pushed_stack_items=1,
5243+
data_portion_length=1,
5244+
data_portion_formatter=_dupn_swapn_encoder,
5245+
stack_properties_modifier=_dupn_stack_properties_modifier,
5246+
)
5247+
"""
5248+
DUPN()
5249+
----
5250+
5251+
Description
5252+
----
5253+
5254+
- deduct 3 gas
5255+
- read uint8 operand imm
5256+
- n = imm + 1
5257+
- n'th (1-based) stack item is duplicated at the top of the stack
5258+
- Stack validation: stack_height >= n
5259+
5260+
5261+
Inputs
5262+
----
5263+
5264+
Outputs
5265+
----
5266+
5267+
Fork
5268+
----
5269+
Amsterdam
5270+
5271+
Gas
5272+
----
5273+
3
5274+
5275+
"""
5276+
5277+
SWAPN = Opcode(
5278+
0xE7,
5279+
data_portion_length=1,
5280+
data_portion_formatter=_dupn_swapn_encoder,
5281+
stack_properties_modifier=_swapn_stack_properties_modifier,
5282+
)
5283+
"""
5284+
SWAPN()
5285+
----
5286+
5287+
Description
5288+
----
5289+
5290+
- deduct 3 gas
5291+
- read uint8 operand imm
5292+
- n = imm + 1
5293+
- n + 1th stack item is swapped with the top stack item (1-based).
5294+
- Stack validation: stack_height >= n + 1
5295+
5296+
5297+
Inputs
5298+
----
5299+
5300+
Outputs
5301+
----
5302+
5303+
Fork
5304+
----
5305+
Amsterdam
5306+
5307+
Gas
5308+
----
5309+
3
5310+
5311+
"""
5312+
5313+
EXCHANGE = Opcode(
5314+
0xE8,
5315+
data_portion_length=1,
5316+
data_portion_formatter=_exchange_encoder,
5317+
stack_properties_modifier=_exchange_stack_properties_modifier,
5318+
)
5319+
"""
5320+
EXCHANGE[x, y]
5321+
----
5322+
5323+
Description
5324+
----
5325+
Exchanges two stack positions. Two nybbles, n is high 4 bits + 1,
5326+
then m is 4 low bits + 1.
5327+
Exchanges the n+1'th item with the n + m + 1 item.
5328+
5329+
Inputs x and y when the opcode is used as `EXCHANGE[x, y]`, are equal to:
5330+
- x = n + 1
5331+
- y = n + m + 1
5332+
Which each equals to 1-based stack positions swapped.
5333+
5334+
Inputs
5335+
----
5336+
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,
5337+
5338+
Outputs
5339+
----
5340+
n + m + 1, or ((imm >> 4) + (imm &0x0F) + 3) from the raw immediate,
5341+
5342+
Fork
5343+
----
5344+
Amsterdam
5345+
5346+
Gas
5347+
----
5348+
3
5349+
5350+
"""
5351+
50835352
CREATE = Opcode(
50845353
0xF0,
50855354
popped_stack_items=3,

src/ethereum/forks/amsterdam/vm/instructions/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ class Ops(enum.Enum):
188188
SWAP15 = 0x9E
189189
SWAP16 = 0x9F
190190

191+
# EIP-8024: Stack access instructions
192+
DUPN = 0xE6
193+
SWAPN = 0xE7
194+
EXCHANGE = 0xE8
195+
191196
# Memory Operations
192197
MLOAD = 0x51
193198
MSTORE = 0x52
@@ -350,6 +355,9 @@ class Ops(enum.Enum):
350355
Ops.SWAP14: stack_instructions.swap14,
351356
Ops.SWAP15: stack_instructions.swap15,
352357
Ops.SWAP16: stack_instructions.swap16,
358+
Ops.DUPN: stack_instructions.dupn,
359+
Ops.SWAPN: stack_instructions.swapn,
360+
Ops.EXCHANGE: stack_instructions.exchange,
353361
Ops.LOG0: log_instructions.log0,
354362
Ops.LOG1: log_instructions.log1,
355363
Ops.LOG2: log_instructions.log2,

0 commit comments

Comments
 (0)