Skip to content
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

Precompile modexp test cases #364

Merged
merged 28 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
55b5935
add modexp test case
jochem-brouwer Dec 19, 2023
40b2192
fix storage len
jochem-brouwer Dec 19, 2023
d7d3dbe
fix code
jochem-brouwer Dec 19, 2023
1e13d99
add cases
jochem-brouwer Dec 20, 2023
dccaec3
make tox happy
jochem-brouwer Dec 20, 2023
c44ea58
refactor: use pytest parametrize and TestAddress
danceratopz Dec 20, 2023
aa3c60f
update changelog
jochem-brouwer Dec 21, 2023
5033153
Merge pull request #1 from danceratopz/modexp-parametrize-test-function
jochem-brouwer Dec 21, 2023
21454cf
Merge branch 'precompile-modexp-test-cases' of github.com:jochem-brou…
jochem-brouwer Dec 21, 2023
77a6828
add evm version
jochem-brouwer Dec 21, 2023
40a7586
add remaining EIP changes
jochem-brouwer Dec 23, 2023
fadd5ae
apply suggestions
jochem-brouwer Jan 10, 2024
e8c008c
Merge branch 'main' into precompile-modexp-test-cases
jochem-brouwer Jan 10, 2024
8ba82f9
Update tests/byzantium/precompiles/test_05_modexp.py
jochem-brouwer Jan 10, 2024
35f1eef
apply suggestions
jochem-brouwer Jan 10, 2024
460bf35
apply test names
jochem-brouwer Jan 10, 2024
fe4c0a8
rename test
jochem-brouwer Jan 10, 2024
470cd84
make test more readable
jochem-brouwer Jan 10, 2024
43fc9f3
add reference files
jochem-brouwer Jan 10, 2024
8b05a47
test rename back
jochem-brouwer Jan 10, 2024
ca852ce
rename back
jochem-brouwer Jan 10, 2024
da26d1d
rename init.py byzantium tests
jochem-brouwer Jan 10, 2024
b2da0d7
refactor(tests): express test parameters as dataclasses
danceratopz Jan 11, 2024
8ac3052
feat(fw): add a base dataclass to auto-generate test ids
danceratopz Jan 16, 2024
6af9ecb
feat(tests): use TestParameterGroup to simplify test parameter classes
danceratopz Jan 16, 2024
fb80c48
docs: update changelog
danceratopz Jan 16, 2024
e0749dd
Merge pull request #2 from danceratopz/refactor/more-readable-params
jochem-brouwer Jan 16, 2024
98b29a3
Update tests/byzantium/eip198_modexp_precompile/test_modexp.py
jochem-brouwer Jan 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Test fixtures for use by clients are available for each release on the [Github r

### πŸ§ͺ Test Cases

- ✨ Add tests for the MODEXP precompile ([#364](https://github.com/ethereum/execution-spec-tests/pull/364))
- πŸ”€ Add reentrancy suicide revert test ([#372](https://github.com/ethereum/execution-spec-tests/pull/372)).
- πŸ”€ BlockchainTests converted to StateTest (also automatically generate BlockchainTest) ([#368](https://github.com/ethereum/execution-spec-tests/pull/368), [#370](https://github.com/ethereum/execution-spec-tests/pull/370)):
- tests/cancun/eip4844_blobs/test_blob_txs.py::test_invalid_normal_gas
Expand Down Expand Up @@ -43,6 +44,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- πŸ’₯ Removed `--enable-hive` parameter, now all test types are generated by default ([#358](https://github.com/ethereum/execution-spec-tests/pull/358))
- πŸ’₯ `StateTest`, spec format used to write tests, is now limited to a single transaction per test ([#361](https://github.com/ethereum/execution-spec-tests/pull/361))
- ✨ `StateTestOnly`, spec format is now available and its only difference with `StateTest` is that it does not produce a `BlockchainTest` ([#368](https://github.com/ethereum/execution-spec-tests/pull/368))
- ✨ Add a helper class `ethereum_test_tools.TestParameterGroup` to define test parameters as dataclasses and auto-generate test IDs ([#364](https://github.com/ethereum/execution-spec-tests/pull/364)).

### πŸ”§ EVM Tools

Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Storage,
TestAddress,
TestAddress2,
TestParameterGroup,
TestPrivateKey,
TestPrivateKey2,
Transaction,
Expand Down Expand Up @@ -95,6 +96,7 @@
"TestAddress",
"TestAddress2",
"TestInfo",
"TestParameterGroup",
"TestPrivateKey",
"TestPrivateKey2",
"Transaction",
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_tools/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
TestPrivateKey2,
)
from .helpers import (
TestParameterGroup,
add_kzg_version,
ceiling_division,
compute_create2_address,
Expand Down Expand Up @@ -72,6 +73,7 @@
"Storage",
"TestAddress",
"TestAddress2",
"TestParameterGroup",
"TestPrivateKey",
"TestPrivateKey2",
"Transaction",
Expand Down
28 changes: 28 additions & 0 deletions src/ethereum_test_tools/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Helper functions/classes used to generate Ethereum tests.
"""

from dataclasses import MISSING, dataclass, fields
from typing import List, SupportsBytes

from ethereum.crypto.hash import keccak256
Expand Down Expand Up @@ -124,3 +125,30 @@ def add_kzg_version(
else:
raise TypeError("Blob hash must be either an integer, string or bytes")
return kzg_versioned_hashes


@dataclass(kw_only=True, frozen=True, repr=False)
class TestParameterGroup:
"""
Base class for grouping test parameters in a dataclass. Provides a generic
__repr__ method to generate clean test ids, including only non-default
optional fields.
"""

__test__ = False # explicitly prevent pytest collecting this class

def __repr__(self):
"""
Generates a repr string, intended to be used as a test id, based on the class
name and the values of the non-default optional fields.
"""
class_name = self.__class__.__name__
field_strings = []

for field in fields(self):
value = getattr(self, field.name)
# Include the field only if it is not optional or not set to its default value
if field.default is MISSING or field.default != value:
field_strings.append(f"{field.name}_{value}")

return f"{class_name}_{'-'.join(field_strings)}"
3 changes: 3 additions & 0 deletions tests/byzantium/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Test cases for EVM functionality introduced in Byzantium.
"""
3 changes: 3 additions & 0 deletions tests/byzantium/eip198_modexp_precompile/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Test for precompiles introduced in Byzantium.
"""
273 changes: 273 additions & 0 deletions tests/byzantium/eip198_modexp_precompile/test_modexp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
"""
abstract: Test [EIP-198: MODEXP Precompile](https://eips.ethereum.org/EIPS/eip-198)

Tests the MODEXP precompile, located at address 0x0000..0005. Test cases from the EIP are
labelled with `EIP-198-caseX` in the test id.
"""
from dataclasses import dataclass

import pytest

from ethereum_test_tools import (
Account,
Environment,
StateTestFiller,
TestAddress,
TestParameterGroup,
Transaction,
compute_create_address,
to_address,
)
from ethereum_test_tools.vm.opcode import Opcodes as Op

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-198.md"
REFERENCE_SPEC_VERSION = "9e393a79d9937f579acbdcb234a67869259d5a96"


@dataclass(kw_only=True, frozen=True, repr=False)
class ModExpInput(TestParameterGroup):
"""
Helper class that defines the MODEXP precompile inputs and creates the
call data from them.

Attributes:
base (str): The base value for the MODEXP precompile.
exponent (str): The exponent value for the MODEXP precompile.
modulus (str): The modulus value for the MODEXP precompile.
extra_data (str): Defines extra padded data to be added at the end of the calldata
to the precompile. Defaults to an empty string.
"""

base: str
exponent: str
modulus: str
extra_data: str = ""

def create_modexp_tx_data(self):
"""
Generates input for the MODEXP precompile.
"""
return (
"0x"
+ f"{int(len(self.base)/2):x}".zfill(64)
+ f"{int(len(self.exponent)/2):x}".zfill(64)
+ f"{int(len(self.modulus)/2):x}".zfill(64)
+ self.base
+ self.exponent
+ self.modulus
+ self.extra_data
)


@dataclass(kw_only=True, frozen=True, repr=False)
class ModExpRawInput(TestParameterGroup):
"""
Helper class to directly define a raw input to the MODEXP precompile.
"""

raw_input: str

def create_modexp_tx_data(self):
"""
The raw input is already the MODEXP precompile input.
"""
return self.raw_input


@dataclass(kw_only=True, frozen=True, repr=False)
class ExpectedOutput(TestParameterGroup):
"""
Expected test result.

Attributes:
call_return_code (str): The return_code from CALL, 0 indicates unsuccessful call
(out-of-gas), 1 indicates call succeeded.
returned_data (str): The output returnData is the expected output of the call
"""

call_return_code: str
returned_data: str


@pytest.mark.valid_from("Byzantium")
@pytest.mark.parametrize(
["input", "output"],
[
(
ModExpInput(base="", exponent="", modulus="02"),
ExpectedOutput(call_return_code="0x01", returned_data="0x01"),
),
(
ModExpInput(base="", exponent="", modulus="0002"),
ExpectedOutput(call_return_code="0x01", returned_data="0x0001"),
),
(
ModExpInput(base="00", exponent="00", modulus="02"),
ExpectedOutput(call_return_code="0x01", returned_data="0x01"),
),
(
ModExpInput(base="", exponent="01", modulus="02"),
ExpectedOutput(call_return_code="0x01", returned_data="0x00"),
),
(
ModExpInput(base="01", exponent="01", modulus="02"),
ExpectedOutput(call_return_code="0x01", returned_data="0x01"),
),
(
ModExpInput(base="02", exponent="01", modulus="03"),
ExpectedOutput(call_return_code="0x01", returned_data="0x02"),
),
(
ModExpInput(base="02", exponent="02", modulus="05"),
ExpectedOutput(call_return_code="0x01", returned_data="0x04"),
),
(
ModExpInput(base="", exponent="", modulus=""),
ExpectedOutput(call_return_code="0x01", returned_data="0x"),
),
(
ModExpInput(base="", exponent="", modulus="00"),
ExpectedOutput(call_return_code="0x01", returned_data="0x00"),
),
(
ModExpInput(base="", exponent="", modulus="01"),
ExpectedOutput(call_return_code="0x01", returned_data="0x00"),
),
(
ModExpInput(base="", exponent="", modulus="0001"),
ExpectedOutput(call_return_code="0x01", returned_data="0x0000"),
),
# Test cases from EIP 198 (Note: the cases where the call goes out-of-gas and the
# final test case are not yet tested)
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
pytest.param(
ModExpInput(
base="03",
exponent="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
modulus="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
),
ExpectedOutput(
call_return_code="0x01",
returned_data="0000000000000000000000000000000000000000000000000000000000000001",
),
id="EIP-198-case1",
),
pytest.param(
ModExpInput(
base="",
exponent="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e",
modulus="fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f",
),
ExpectedOutput(
call_return_code="0x01",
returned_data="0000000000000000000000000000000000000000000000000000000000000000",
),
id="EIP-198-case2",
),
pytest.param( # Note: This is the only test case which goes out-of-gas.
ModExpRawInput(
raw_input="0000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000020"
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd"
),
ExpectedOutput(
call_return_code="0x00",
returned_data="0000000000000000000000000000000000000000000000000000000000000000",
),
id="EIP-198-case3-raw-input-out-of-gas",
),
pytest.param(
ModExpInput(
base="03",
exponent="ffff",
modulus="8000000000000000000000000000000000000000000000000000000000000000",
extra_data="07",
),
ExpectedOutput(
call_return_code="0x01",
returned_data="0x3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab",
),
id="EIP-198-case4-extra-data_07",
),
pytest.param(
ModExpRawInput(
raw_input="0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000002"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"ffff"
"80"
),
ExpectedOutput(
call_return_code="0x01",
returned_data="0x3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab",
),
id="EIP-198-case5-raw-input",
),
],
ids=lambda param: param.__repr__(), # only required to remove parameter names (input/output)
)
def test_modexp(state_test: StateTestFiller, input: ModExpInput, output: ExpectedOutput):
"""
Test the MODEXP precompile
"""
env = Environment()
pre = {TestAddress: Account(balance=1000000000000000000000)}

account = to_address(0x100)

pre[account] = Account(
code=(
# Store all CALLDATA into memory (offset 0)
Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE())
# Store the returned CALL status (success = 1, fail = 0) into slot 0:
+ Op.SSTORE(
0,
# Setup stack to CALL into ModExp with the CALLDATA and CALL into it (+ pop value)
Op.CALL(Op.GAS(), 0x05, 0, 0, Op.CALLDATASIZE(), 0, 0),
)
# Store contract deployment code to deploy the returned data from ModExp as
# contract code (16 bytes)
+ Op.MSTORE(
0,
(
(
# Need to `ljust` this PUSH32 in order to ensure the code starts
# in memory at offset 0 (memory right-aligns stack items which are not
# 32 bytes)
Op.PUSH32(
(
Op.CODECOPY(0, 16, Op.SUB(Op.CODESIZE(), 16))
+ Op.RETURN(0, Op.SUB(Op.CODESIZE, 16))
).ljust(32, bytes(1))
)
)
),
)
# RETURNDATACOPY the returned data from ModExp into memory (offset 16 bytes)
+ Op.RETURNDATACOPY(16, 0, Op.RETURNDATASIZE())
# CREATE contract with the deployment code + the returned data from ModExp
+ Op.CREATE(0, 0, Op.ADD(16, Op.RETURNDATASIZE()))
# STOP (handy for tracing)
+ Op.STOP()
)
)

tx = Transaction(
ty=0x0,
nonce=0,
to=account,
data=input.create_modexp_tx_data(),
gas_limit=500000,
gas_price=10,
protected=True,
)

post = {}
if output.call_return_code != "0x00":
contract_address = compute_create_address(account, tx.nonce)
post[contract_address] = Account(code=output.returned_data)
post[account] = Account(storage={0: output.call_return_code})

state_test(env=env, pre=pre, post=post, tx=tx)
2 changes: 2 additions & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ customizations
Customizations
danceratopz
dao
dataclasses
datastructures
delitem
dev
Expand Down Expand Up @@ -510,6 +511,7 @@ precompile
precompiles
deployer
0x
modexp
0x00
0x10

Expand Down