Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
70c51d1
test: add specs
XinyuCRO May 22, 2026
7de33c6
feat: align eth_getStorageAt
XinyuCRO May 22, 2026
4232e85
add eth-rpc-check.md
songgaoye May 26, 2026
264341d
Merge branch 'develop' of github.com:crypto-org-chain/ethermint into …
XinyuCRO Jun 8, 2026
3bc3063
Merge branch 'develop' of github.com:crypto-org-chain/ethermint into …
XinyuCRO Jun 15, 2026
9afff75
fix: create access list without gas should success
XinyuCRO Jun 16, 2026
b89f618
test: patch test input for ethermint(simulateV1)
XinyuCRO Jun 16, 2026
c1917b5
fix: json number
XinyuCRO Jun 16, 2026
0e8376a
test: eth_getBalance testcase patch for ethermint
XinyuCRO Jun 16, 2026
b42465c
Merge branch 'develop' of github.com:crypto-org-chain/ethermint into …
XinyuCRO Jun 16, 2026
67e9a12
test: exclude past fork cases for eth_getBlockByNumber
XinyuCRO Jun 18, 2026
08f48c0
test: normalize local eth_getBlockReceipts schema fixtures
XinyuCRO Jun 18, 2026
503c1a5
test: eth_getProof, eth_getLogs
XinyuCRO Jun 18, 2026
a82508b
test: receipt differences
XinyuCRO Jun 18, 2026
0c68a42
fix: tests
XinyuCRO Jun 22, 2026
7e892a5
fix: review
XinyuCRO Jun 23, 2026
2084e0c
test:
XinyuCRO Jun 24, 2026
4d977a2
test: split file
XinyuCRO Jun 24, 2026
34bfcde
Merge branch 'develop' into feat/rpc
XinyuCRO Jun 24, 2026
fb5d150
test: fix RPC schema lint
XinyuCRO Jun 24, 2026
05edaf7
test: sync execution-apis fixtures before schema test
XinyuCRO Jun 24, 2026
6a51bcc
test: sync spec fixtures from upstream
XinyuCRO Jun 24, 2026
7bfb50f
feat: eth_baseFee
XinyuCRO Jun 24, 2026
a886a07
fix: tests
XinyuCRO Jun 25, 2026
f24589c
doc:
XinyuCRO Jun 25, 2026
d2dba6f
test: rm unused specs
XinyuCRO Jun 25, 2026
bad6b63
doc: comments
XinyuCRO Jun 25, 2026
7fc9eb9
test:
XinyuCRO Jun 25, 2026
ef4bec6
fix: lint
XinyuCRO Jun 25, 2026
0b230f5
fix: review
XinyuCRO Jun 26, 2026
99109bb
Merge branch 'develop' of github.com:crypto-org-chain/ethermint into …
XinyuCRO Jun 26, 2026
5cbbfab
fix: gomod2nix
XinyuCRO Jun 26, 2026
d5b6d86
test: rename spec into execution-api
XinyuCRO Jun 26, 2026
6d1d30c
test: rm simulate, already tested in schema tests
XinyuCRO Jun 26, 2026
2e78e91
revert: changes in sub pr
XinyuCRO Jun 29, 2026
df47610
Merge branch 'develop' into feat/rpc
XinyuCRO Jun 29, 2026
445c748
fix: review
XinyuCRO Jun 29, 2026
f38875a
Merge branch 'develop' of github.com:crypto-org-chain/ethermint into …
XinyuCRO Jun 30, 2026
e7a593e
Merge branch 'develop' into feat/rpc
XinyuCRO Jun 30, 2026
0198212
Merge branch 'develop' into feat/rpc
XinyuCRO Jul 2, 2026
a5d873f
test: whitelist decimal block number request
XinyuCRO Jul 2, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ sim_log_file
tests/**/tmp/*
yarn.lock

# Generated execution-apis RPC schema fixtures
/tests/integration_tests/execution-api/*/*.io
/tests/integration_tests/execution-api/fixtures/execution_apis/

# Vagrant
.vagrant/
*.box
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Improvements

* (rpc) [#1010](https://github.com/crypto-org-chain/ethermint/pull/1010) test(rpc): validate JSON-RPC schema against ethereum/execution-apis fixtures.
* (rpc) [#992](https://github.com/crypto-org-chain/ethermint/pull/992) test(rpc): improve test coverage for GetTransactionByBlock* methods.
* (rpc) [#996](https://github.com/crypto-org-chain/ethermint/pull/996) refactor(rpc): fix type consistency in `buildReceiptDirect` and `GetTransactionByBlockHashAndIndex`.

Expand Down
26 changes: 26 additions & 0 deletions tests/integration_tests/configs/execution-api-schema.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
local config = import 'simulate.jsonnet';

config {
'ethermint_9000-1'+: {
'app-config'+: {
'json-rpc'+: {
api: 'eth,net,web3,debug,txpool',
'feehistory-cap': 100,
'block-range-cap': 10000,
'logs-cap': 10000,
'return-data-limit': 300000,
},
},
genesis+: {
app_state+: {
feemarket+: {
params+: {
no_base_fee: false,
base_fee: '100000000000',
min_gas_multiplier: '0',
},
},
},
},
},
}
21 changes: 21 additions & 0 deletions tests/integration_tests/execution-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Schema-level test for Ethermint JSON-RPC against ethereum/execution-apis fixtures.

The pytest fixture syncs `.io` fixtures from ethereum/execution-apis before the
test run. The copied fixture directories and upstream geth chain files are
generated under this `execution-api/` directory and ignored by git.
By default the sync downloads the latest `main` archive from
`ethereum/execution-apis`.

Running the test

```sh
nix-shell ./tests/integration_tests/shell.nix --run \
"pytest -vv --basetemp=/tmp/eth -s -k test_rpc_spec_schema \
--session-timeout=6000 --timeout=6000"
```

Reading the report after running the test

```sh
/tmp/eth/ethermintcurrent/rpc_schema_report.md
```
276 changes: 276 additions & 0 deletions tests/integration_tests/execution-api/_execution_apis_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# Downloads the ethereum/execution-apis test suite from GitHub, extracts the
# .io fixture files into this directory, and regenerates the Ethermint genesis
# overlay (ethermint_genesis_overlay.json) that imports the geth head state so
# the local chain starts from the same account/storage snapshot as the fixtures.

import json
import os
import shutil
import tempfile
import urllib.request
import zipfile
from pathlib import Path

import bech32

SPEC_ROOT = Path(__file__).parent
EXECUTION_APIS_FIXTURE_DIR = SPEC_ROOT / "fixtures" / "execution_apis"

DEFAULT_EXECUTION_APIS_REF = "main"
EXECUTION_APIS_ARCHIVE_URL = (
"https://github.com/ethereum/execution-apis/archive/{ref}.zip"
)

TEST_FIXTURE_FILES = [
"chain.rlp",
"forkenv.json",
"genesis.json",
"headfcu.json",
]
TOOL_CHAIN_FIXTURE_FILES = [
"accounts.json",
"headstate.json",
"txinfo.json",
]

DENOM = "aphoton"
PREFIX = "ethm"
EMPTY_CODE_HASH = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
INIT_VALIDATOR_COUNT = 2
INIT_VALIDATOR_APHOTON_BALANCE = 10_000_000_000_000_000_000_000
INIT_VALIDATOR_STAKE_BALANCE = 1_000_000_000_000_000_000


def _env_disabled(value):
return str(value).lower() in {"0", "false", "no", "off"}


def _download_archive(ref, archive_path):
archive_url = os.environ.get("EXECUTION_APIS_ARCHIVE_URL")
if archive_url is None:
archive_url = EXECUTION_APIS_ARCHIVE_URL.format(ref=ref)

request = urllib.request.Request(
archive_url,
headers={"User-Agent": "ethermint-rpc-schema-tests"},
)
with urllib.request.urlopen(request, timeout=120) as response:
archive_path.write_bytes(response.read())
return archive_url


def _extract_archive(archive_path, extract_dir):
with zipfile.ZipFile(archive_path) as archive:
# Reject absolute paths and .. traversals before extracting.
for member in archive.infolist():
path = Path(member.filename)
if path.is_absolute() or ".." in path.parts:
raise ValueError(f"unsafe archive path: {member.filename}")
archive.extractall(extract_dir)

roots = [path for path in extract_dir.iterdir() if path.is_dir()]
if len(roots) != 1:
raise ValueError(f"expected one execution-apis archive root, got {roots}")
return roots[0]


def _remove_copied_io_fixtures():
for child in SPEC_ROOT.iterdir():
if child.is_dir() and any(child.glob("*.io")):
shutil.rmtree(child)


def _copy_io_fixtures(source_tests_dir):
_remove_copied_io_fixtures()
copied = 0
for source in sorted(source_tests_dir.glob("*/*.io")):
target = SPEC_ROOT / source.relative_to(source_tests_dir)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
copied += 1
if copied == 0:
raise ValueError(f"no execution-apis .io fixtures found in {source_tests_dir}")
return copied


def _copy_required_files(source_dir, target_dir, names):
for name in names:
source = source_dir / name
if not source.exists():
raise FileNotFoundError(f"execution-apis fixture missing: {source}")
target_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target_dir / name)


def _eth_to_bech32(addr):
addr = addr.removeprefix("0x")
# bech32 encodes 5-bit groups; convertbits re-packs the raw 8-bit address bytes.
data = bech32.convertbits(bytes.fromhex(addr), 8, 5)
return bech32.bech32_encode(PREFIX, data)


def _quantity_to_int(value):
if value is None:
return 0
if isinstance(value, int):
return value
if isinstance(value, str) and value.startswith("0x"):
return int(value, 16)
return int(value)


def _storage_items(storage):
return [
{
"key": key,
"value": value if value.startswith("0x") else f"0x{value}",
}
for key, value in sorted((storage or {}).items())
]


def _chain_config(geth_genesis):
config = geth_genesis["config"]
return {
"homestead_block": str(config["homesteadBlock"]),
"dao_fork_block": "0",
"dao_fork_support": True,
"eip150_block": str(config["eip150Block"]),
"eip150_hash": config.get(
"eip150Hash",
"0x0000000000000000000000000000000000000000000000000000000000000000",
),
"eip155_block": str(config["eip155Block"]),
"eip158_block": str(config["eip158Block"]),
"byzantium_block": str(config["byzantiumBlock"]),
"constantinople_block": str(config["constantinopleBlock"]),
"petersburg_block": str(config["petersburgBlock"]),
"istanbul_block": str(config["istanbulBlock"]),
"muir_glacier_block": str(config["muirGlacierBlock"]),
"berlin_block": str(config["berlinBlock"]),
"london_block": str(config["londonBlock"]),
"arrow_glacier_block": str(config["arrowGlacierBlock"]),
"gray_glacier_block": str(config["grayGlacierBlock"]),
"merge_netsplit_block": str(config["mergeNetsplitBlock"]),
"shanghai_time": str(config["shanghaiTime"]),
"cancun_time": str(config["cancunTime"]),
"prague_time": str(config["pragueTime"]),
}


def _regenerate_ethermint_genesis_overlay():
# Translates the upstream geth headstate into Cosmos SDK auth/bank/evm
# account structures so Ethermint can import the same state at genesis.
geth_genesis = json_load(EXECUTION_APIS_FIXTURE_DIR / "genesis.json")
headstate = json_load(EXECUTION_APIS_FIXTURE_DIR / "headstate.json")["accounts"]

auth_accounts = []
bank_balances = []
evm_accounts = []
imported_aphoton_supply = 0

for account_number, (raw_addr, account) in enumerate(sorted(headstate.items())):
eth_addr = f"0x{raw_addr.removeprefix('0x').lower()}"
balance = _quantity_to_int(account.get("balance"))
nonce = _quantity_to_int(account.get("nonce"))
code = account.get("code", "").removeprefix("0x")
code_hash = account.get("codeHash") or EMPTY_CODE_HASH

auth_accounts.append(
{
"@type": "/ethermint.types.v1.EthAccount",
"base_account": {
"address": _eth_to_bech32(eth_addr),
"pub_key": None,
"account_number": str(account_number),
"sequence": str(nonce),
},
"code_hash": code_hash,
}
)

if balance > 0:
imported_aphoton_supply += balance
bank_balances.append(
{
"address": _eth_to_bech32(eth_addr),
"coins": [{"denom": DENOM, "amount": str(balance)}],
}
)

evm_accounts.append(
{
"address": eth_addr,
"code": code,
"storage": _storage_items(account.get("storage")),
}
)

output = {
"source": {
"genesis": "genesis.json",
"headstate": "headstate.json",
"chain": "chain.rlp",
"note": (
"Ethermint can import this head EVM state at genesis, but it "
"cannot replay geth chain.rlp blocks without a dedicated importer."
),
},
"auth_accounts": auth_accounts,
"bank_balances": bank_balances,
"bank_supply": [
{
"denom": DENOM,
"amount": str(
INIT_VALIDATOR_COUNT * INIT_VALIDATOR_APHOTON_BALANCE
+ imported_aphoton_supply
),
},
{
"denom": "stake",
"amount": str(INIT_VALIDATOR_COUNT * INIT_VALIDATOR_STAKE_BALANCE),
},
],
"evm_accounts": evm_accounts,
"chain_config": _chain_config(geth_genesis),
}
(EXECUTION_APIS_FIXTURE_DIR / "ethermint_genesis_overlay.json").write_text(
json_dumps(output)
)


def json_load(path):
return json.loads(path.read_text())


def json_dumps(value):
return json.dumps(value, indent=2, sort_keys=True) + "\n"


def sync_execution_apis():
if _env_disabled(os.environ.get("EXECUTION_APIS_SYNC", "1")):
return "execution-apis sync disabled by EXECUTION_APIS_SYNC"

ref = os.environ.get("EXECUTION_APIS_REF", DEFAULT_EXECUTION_APIS_REF)
with tempfile.TemporaryDirectory(prefix="execution-apis-") as tmp:
tmp_path = Path(tmp)
archive_path = tmp_path / "execution-apis.zip"
archive_url = _download_archive(ref, archive_path)
upstream_root = _extract_archive(archive_path, tmp_path / "src")

source_tests_dir = upstream_root / "tests"
source_tools_chain_dir = upstream_root / "tools" / "chain"
copied = _copy_io_fixtures(source_tests_dir)
_copy_required_files(
source_tests_dir,
EXECUTION_APIS_FIXTURE_DIR,
TEST_FIXTURE_FILES,
)
_copy_required_files(
source_tools_chain_dir,
EXECUTION_APIS_FIXTURE_DIR,
TOOL_CHAIN_FIXTURE_FILES,
)
_regenerate_ethermint_genesis_overlay()

return f"synced {copied} execution-apis fixtures from {archive_url}"
Loading
Loading