Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ End-to-end tests for the VeChain **INTERSTELLAR** fork, which activates at block
| `tests/eip7934` | [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934) | Max RLP-encoded block size (`MaxRLPBlockSize = 8_388_608`); packer-level split test + P2P consensus-level rejection of oversized blocks |
| `tests/eip7883` | [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883) | ModExp precompile repricing |
| `tests/eip7939` | [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) | `CLZ` opcode (0x1e) — count leading zeros |
| `tests/eip6780` | [EIP-7939](https://eips.ethereum.org/EIPS/eip-6780) | `SELFDESTRUCT` only in same transaction |
| `tests/eip2935` | [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) | Serve historical block hashes from state |

## Repository layout
Expand All @@ -31,6 +32,8 @@ interstellar-e2e/
├── eip7934/
├── eip7883/
└── eip7939/
└── eip6780/
└── eth_rpc/ # test eth rpc endpoint
└── eip2935/
```

Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ jobs:
network/go.sum
tests/go.sum

- uses: actions/setup-node@v5
with:
# Node 22 makes WebSocket available as a stable global — required by
# tests/eth_rpc/ethersjs/test/websocket.test.ts (ethers v6 WebSocketProvider
# picks up globalThis.WebSocket when given a plain URL string).
node-version: '22'
cache: 'npm'
cache-dependency-path: tests/eth_rpc/ethersjs/package-lock.json

- name: Get thor commit SHA
id: thor-sha
run: |
Expand Down
21 changes: 18 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
.PHONY: build-network test clean stop status lint
.PHONY: build-network ethersjs-deps web3js-deps viem-deps test clean stop status lint

build-network:
cd network && go build -o /tmp/interstellar-network github.com/vechain/interstellar-e2e/network && cd ..

test: build-network
ethersjs-deps:
@[ -d tests/eth_rpc/ethersjs/node_modules ] || (cd tests/eth_rpc/ethersjs && npm ci)

web3js-deps:
@[ -d tests/eth_rpc/web3js/node_modules ] || (cd tests/eth_rpc/web3js && npm ci)

viem-deps:
@[ -d tests/eth_rpc/viem/node_modules ] || (cd tests/eth_rpc/viem && npm ci)

# -p 1 is required, not a perf knob: every eth_rpc suite signs its transactions
# with the same funded account (node1Key in tests/helper/client.go, mirrored in
# each JS suite's src/fixtures.ts). Letting Go run those packages concurrently
# makes them clobber each other's pending nonce, so transactions are never mined
# and the suites fail with receipt/block timeouts. Serialising is also faster
# here, since the parallel runs spent most of their time waiting on those.
test: build-network ethersjs-deps web3js-deps viem-deps
@/tmp/interstellar-network start & \
START_PID=$$! ; \
trap '/tmp/interstellar-network stop 2>/dev/null || true; kill $$START_PID 2>/dev/null || true' EXIT ; \
Expand All @@ -16,7 +31,7 @@ test: build-network
if [ -z "$$NODE_URL" ] || [ -z "$$NODE_P2P_PORT" ]; then \
echo "ERROR: empty node connection details — refusing to run e2e tests against no network"; exit 1; \
fi ; \
cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m ./...
cd tests && NODE_URL=$$NODE_URL NODE_P2P_PORT=$$NODE_P2P_PORT go test -v -count=1 -timeout 20m -p 1 ./...

stop:
/tmp/interstellar-network stop 2>/dev/null || true
Expand Down
7 changes: 7 additions & 0 deletions network/setup/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ func BuildNetwork() *network.Network {
// Raise block gas limit above MaxTxGasLimit (1<<24) so EIP-7825 boundary
// tests can verify at-limit transactions are both accepted and includable.
gen.GasLimit = 40_000_000
// Shorten the block interval from the preset default (10s) to the Thor
// minimum (2s — customnet rejects 0 or 1). Every test that submits a real
// transaction blocks on the next packed block, so this cuts confirmation
// latency ~5x and dominates total `make test` wall-clock.
if gen.Config != nil {
gen.Config.BlockInterval = 2
}
// Activate the INTERSTELLAR fork from block 1, leaving block 0 as a
// pre-fork state that tests can simulate against via InspectClauses Revision("0").
gen.ForkConfig.AddField("INTERSTELLAR", 1) //nolint:errcheck
Expand Down
302 changes: 302 additions & 0 deletions tests/eth_rpc/eth_rpc_schema/eth_rpc_schema_extra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
// Additional schema-conformance coverage, organized by the same three-category
// scheme used in the ethersjs / web3js suites:
//
// Cat-1 Implemented on Thor → full request + JSON-Schema validation of the
// result (chain stubs, uncles, the filter family, the WS 'syncing'
// subscription).
// Cat-2 Not registered by thor's dispatcher → the call is attempted and the
// test SKIPS on a "method not found" surface (auto-activates if Thor
// ever ships it).
// Cat-3 Implemented but divergent from go-ethereum → a geth-parity assertion
// that is LEFT FAILING for manual review.
//
// Authoritative method set comes from thor's rpc/*/handler.go Mount() funcs on
// branch pedro/eth_eq_json_rpc.

package ethrpcschema

import (
"context"
"encoding/json"
"strings"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// -----------------------------------------------------------------------------
// Cat-1 — chain / node metadata stubs (implemented, PoA-constant values)
// -----------------------------------------------------------------------------

// TestEthCoinbase asserts eth_coinbase returns an address (zero on PoA).
func TestEthCoinbase(t *testing.T) {
result := rpcCallAndValidate(t, "eth_coinbase")
var addr string
require.NoError(t, json.Unmarshal(result, &addr), "unmarshal coinbase")
assert.Equal(t, common.Address{}.Hex(), common.HexToAddress(addr).Hex(),
"Thor PoA coinbase must be the zero address")
}

// TestEthMining asserts eth_mining returns a boolean (false on PoA).
func TestEthMining(t *testing.T) {
result := rpcCallAndValidate(t, "eth_mining")
var mining bool
require.NoError(t, json.Unmarshal(result, &mining), "unmarshal mining")
assert.False(t, mining, "Thor PoA must report mining=false")
}

// TestEthHashrate asserts eth_hashrate returns a QUANTITY (0x0 on PoA).
func TestEthHashrate(t *testing.T) {
result := rpcCallAndValidate(t, "eth_hashrate")
assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "Thor PoA hashrate must be 0")
}

// TestEthAccounts asserts eth_accounts returns an (empty) address array — Thor
// holds no node-side keystore.
func TestEthAccounts(t *testing.T) {
result := rpcCallAndValidate(t, "eth_accounts")
var accounts []string
require.NoError(t, json.Unmarshal(result, &accounts), "unmarshal accounts")
assert.Empty(t, accounts, "Thor exposes no unlocked node accounts")
}

// -----------------------------------------------------------------------------
// Cat-1 — uncles (VeChain has none; count is 0x0, by-index is null)
// -----------------------------------------------------------------------------

// TestEthGetUncleCountByBlockNumber asserts the uncle count at latest is 0x0.
func TestEthGetUncleCountByBlockNumber(t *testing.T) {
result := rpcCallAndValidate(t, "eth_getUncleCountByBlockNumber", "latest")
assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "VeChain has no uncles")
}

// TestEthGetUncleCountByBlockHash asserts the uncle count for the latest-block
// hash is 0x0.
func TestEthGetUncleCountByBlockHash(t *testing.T) {
hash := fetchLatestBlockHash(t)
result := rpcCallAndValidate(t, "eth_getUncleCountByBlockHash", hash)
assert.Equal(t, 0, hexQuantityToInt(t, result).Sign(), "VeChain has no uncles")
}

// TestEthGetUncleByBlockNumberAndIndex asserts the uncle at (latest, 0) is null.
func TestEthGetUncleByBlockNumberAndIndex(t *testing.T) {
result, err := rpcCall(t, "eth_getUncleByBlockNumberAndIndex", "latest", "0x0")
require.NoError(t, err, "eth_getUncleByBlockNumberAndIndex call")
assert.JSONEq(t, "null", string(result), "VeChain has no uncles")
validateResult(t, "eth_getUncleByBlockNumberAndIndex", result)
}

// TestEthGetUncleByBlockHashAndIndex asserts the uncle at (latest hash, 0) is null.
func TestEthGetUncleByBlockHashAndIndex(t *testing.T) {
hash := fetchLatestBlockHash(t)
result, err := rpcCall(t, "eth_getUncleByBlockHashAndIndex", hash, "0x0")
require.NoError(t, err, "eth_getUncleByBlockHashAndIndex call")
assert.JSONEq(t, "null", string(result), "VeChain has no uncles")
validateResult(t, "eth_getUncleByBlockHashAndIndex", result)
}

// -----------------------------------------------------------------------------
// Cat-1 — filter family (newFilter / getFilterLogs / getFilterChanges /
// newBlockFilter / newPendingTransactionFilter / uninstallFilter)
// -----------------------------------------------------------------------------

// filterLogTopic and filterLogInitCode deploy a 43-byte init blob that LOG1's a
// fixed topic and returns empty runtime code — used to seed a log the filter
// family can match. Mirrors the LogOnDeploy blob in the WS logs test, with a
// distinct topic so concurrent subscriptions don't cross-match.
const (
filterLogTopic = "0x3434343434343434343434343434343434343434343434343434343434343434"
filterLogInitCode = "7f3434343434343434343434343434343434343434343434343434343434343434" +
"60006000a160006000f3"
)

// TestEthFilterLogLifecycle drives eth_newFilter → eth_getFilterLogs →
// eth_getFilterChanges → eth_uninstallFilter against a real LOG1 emission, and
// schema-validates each result.
func TestEthFilterLogLifecycle(t *testing.T) {
// Emit a LOG1 with our topic and wait for the receipt so the log is mined
// before the filter is installed.
broadcastEthTx(t, common.Hex2Bytes(filterLogInitCode), nil, 200_000)

filterIDRaw := rpcCallAndValidate(t, "eth_newFilter", map[string]any{
"fromBlock": "0x0",
"toBlock": "latest",
"topics": []any{filterLogTopic},
})
var filterID string
require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id")

defer func() {
removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID)
require.NoError(t, err, "eth_uninstallFilter call")
validateResult(t, "eth_uninstallFilter", removedRaw)
var removed bool
require.NoError(t, json.Unmarshal(removedRaw, &removed), "unmarshal uninstall result")
assert.True(t, removed, "eth_uninstallFilter must return true for an active filter")
}()

// eth_getFilterLogs returns ALL matching logs regardless of poll cursor.
var logs []map[string]any
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
raw := rpcCallAndValidate(t, "eth_getFilterLogs", filterID)
require.NoError(t, json.Unmarshal(raw, &logs), "unmarshal filter logs")
if len(logs) > 0 {
break
}
time.Sleep(500 * time.Millisecond)
}
require.NotEmpty(t, logs, "eth_getFilterLogs must return the emitted LOG1")
topics, ok := logs[0]["topics"].([]any)
require.True(t, ok, "log.topics must be an array")
require.NotEmpty(t, topics, "log must carry a topic")
assert.Equal(t, filterLogTopic, topics[0], "log.topics[0] must match the emitted topic")

// eth_getFilterChanges shape-validates (may be empty depending on the poll
// cursor relative to the emission — we only assert the schema here).
rpcCallAndValidate(t, "eth_getFilterChanges", filterID)
}

// TestEthNewBlockFilter installs a block filter, schema-validates the id and a
// subsequent getFilterChanges poll (an array of block hashes), then uninstalls.
func TestEthNewBlockFilter(t *testing.T) {
filterIDRaw := rpcCallAndValidate(t, "eth_newBlockFilter")
var filterID string
require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id")

// Poll until at least one new block hash arrives (validates the non-empty
// hash-array shape), bounded so a stalled chain can't hang the test.
var hashes []string
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
raw := rpcCallAndValidate(t, "eth_getFilterChanges", filterID)
require.NoError(t, json.Unmarshal(raw, &hashes), "unmarshal block-hash changes")
if len(hashes) > 0 {
break
}
time.Sleep(500 * time.Millisecond)
}
require.NotEmpty(t, hashes, "eth_getFilterChanges on a block filter must yield block hashes")
require.Regexp(t, "^0x[0-9a-fA-F]{64}$", hashes[0], "block-filter change must be a 32-byte hash")

removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID)
require.NoError(t, err, "eth_uninstallFilter call")
validateResult(t, "eth_uninstallFilter", removedRaw)
}

// TestEthNewPendingTransactionFilter installs a pending-tx filter, schema-
// validates the id, then uninstalls.
func TestEthNewPendingTransactionFilter(t *testing.T) {
filterIDRaw := rpcCallAndValidate(t, "eth_newPendingTransactionFilter")
var filterID string
require.NoError(t, json.Unmarshal(filterIDRaw, &filterID), "unmarshal filter id")

removedRaw, err := rpcCall(t, "eth_uninstallFilter", filterID)
require.NoError(t, err, "eth_uninstallFilter call")
validateResult(t, "eth_uninstallFilter", removedRaw)
var removed bool
require.NoError(t, json.Unmarshal(removedRaw, &removed), "unmarshal uninstall result")
assert.True(t, removed, "eth_uninstallFilter must return true for an active filter")
}

// -----------------------------------------------------------------------------
// Cat-1 (WS) — eth_subscribe('syncing')
// -----------------------------------------------------------------------------

// TestWsSubscribeSyncing validates the 'syncing' subscription Thor's
// rpc/ws/conn.go now implements (the old TestWsSubscribeSyncingRejected was
// removed once syncing shipped). On an already-synced local network runSyncing
// emits a single boolean `false` immediately; while syncing it would emit a
// {syncing,status} object — the schema accepts both.
func TestWsSubscribeSyncing(t *testing.T) {
wc := wsDial(t)

subIDRaw := wsCallAndValidate(t, wc, 1, "eth_subscribe", "eth_subscribe", "syncing")
var subID string
require.NoError(t, json.Unmarshal(subIDRaw, &subID), "unmarshal subID")

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
notif, err := wsReadNotification(t, wc, ctx, subID)
require.NoError(t, err, "wait for syncing notification")
validateResult(t, "eth_subscription_syncing", notif)

unsubRaw, err := wsCall(t, wc, 2, "eth_unsubscribe", subID)
require.NoError(t, err, "eth_unsubscribe")
validateResult(t, "eth_unsubscribe", unsubRaw)
}

// -----------------------------------------------------------------------------
// Cat-2 — standard eth_* methods thor does NOT register (skipped until shipped)
// -----------------------------------------------------------------------------

// TestUnimplementedMethods attempts each standard Ethereum method thor's
// dispatcher does not register and skips on a "method not found" surface. If
// Thor ever registers one, the success path (no error, non-null result) keeps
// the test honest; a non-"not found" error fails loudly.
func TestUnimplementedMethods(t *testing.T) {
const senderAddr = "0x61fF580B63D3845934610222245C116E013717ec"
const node2Addr = "0x327931085B4cCbCE0baABb5a5E1C678707C51d90"
zeroHash := "0x" + strings.Repeat("00", 32)

cases := []struct {
method string
params []any
}{
{"eth_getProof", []any{senderAddr, []any{}, "latest"}},
{"eth_createAccessList", []any{map[string]any{"from": senderAddr, "to": node2Addr}, "latest"}},
{"eth_protocolVersion", []any{}},
{"eth_pendingTransactions", []any{}},
{"eth_sign", []any{senderAddr, "0x68656c6c6f"}},
{"eth_signTransaction", []any{map[string]any{"from": senderAddr, "to": node2Addr, "value": "0x1"}}},
{"eth_getRawTransactionByHash", []any{zeroHash}},
{"debug_traceTransaction", []any{zeroHash}},
}

for _, tc := range cases {
t.Run(tc.method, func(t *testing.T) {
result, err := rpcCall(t, tc.method, tc.params...)
if isMethodNotFound(err) {
t.Skipf("%s not implemented by Thor: %v", tc.method, err)
}
require.NoError(t, err, "%s errored for a non-\"not found\" reason", tc.method)
require.NotNil(t, result, "%s returned a nil result without an error", tc.method)
})
}
}

// -----------------------------------------------------------------------------
// eth_feeHistory — rewardPercentiles form (geth parity)
// -----------------------------------------------------------------------------

// TestEthFeeHistory_RewardPercentiles probes the rewardPercentiles form of
// eth_feeHistory.
//
// geth returns a per-block × per-percentile `reward` matrix when called with
// rewardPercentiles. Thor now implements this form (rpc/fees/handler.go), so the
// call succeeds and returns a reward matrix with one entry per requested
// percentile per block. TestEthFeeHistory (no percentiles) covers the base path.
func TestEthFeeHistory_RewardPercentiles(t *testing.T) {
result, err := rpcCall(t, "eth_feeHistory", "0x4", "latest", []float64{25, 50, 75})
require.NoError(t, err, "eth_feeHistory with rewardPercentiles")
// Validate the whole result — including the `reward` matrix — against the
// eth_feeHistory JSON schema. This is the only path that exercises the
// schema's `reward` array-of-array-of-quantity block, since the base
// TestEthFeeHistory sends empty percentiles and gets no reward field.
validateResult(t, "eth_feeHistory", result)
var fh struct {
Reward [][]string `json:"reward"`
}
require.NoError(t, json.Unmarshal(result, &fh), "unmarshal feeHistory")
require.NotEmpty(t, fh.Reward,
"feeHistory must include a per-block reward matrix when rewardPercentiles is requested")
for _, row := range fh.Reward {
require.Len(t, row, 3, "each reward row must have one value per requested percentile")
for _, r := range row {
require.Regexp(t, "^0x[0-9a-fA-F]+$", r, "reward value must be a QUANTITY")
}
}
}
Loading
Loading