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
146 changes: 146 additions & 0 deletions tests/live_etc/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//go:build live

// Package live_etc provides pre-fork integration tests that verify current
// ETC and Mordor chain state via JSON-RPC. These tests require a running
// core-geth node and are excluded from normal test runs.
//
// Run with: go test -tags live ./tests/live_etc/ -v
Comment on lines +3 to +7
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions fork-block validation (multiple forks) and ECIP-1017 era reward verification, but this test suite currently only checks chain ID, genesis hash, PoW/difficulty, gas limit, net_version, ECIP-1017 era (by height), and the DAO-fork block hash. Either update the PR description to match the implemented coverage, or add the missing fork/reward/state-root checks so the suite matches the stated scope.

Copilot uses AI. Check for mistakes.
//
// Environment variables:
//
// MORDOR_RPC - Mordor RPC endpoint (default: http://localhost:8545)
// ETC_RPC - ETC mainnet RPC endpoint (default: https://etc.rivet.link)
package live_etc

import (
"context"
"encoding/json"
"math/big"
"os"
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)

// Pre-olympia chain constants (NO olympia-specific values here)
const (
MordorChainID = 63
ETCMainnetChainID = 61

// ECIP-1017 era parameters
ClassicEraLength = 5_000_000
MordorEraLength = 2_000_000

// ECIP-1099 etchash fork blocks
ClassicECIP1099Block = 11_700_000
MordorECIP1099Block = 2_520_000

// Gas limits (pre-olympia)
ETCGasLimit = 8_000_000
)

// Known genesis hashes
var (
MordorGenesisHash = common.HexToHash("0xa68ebde7932f0bf2579b075499416f0a693de84c26b05cd01de86e60aad05ec0")
ETCGenesisHash = common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
)

// rpcBlock is a minimal block structure for JSON-RPC responses.
type rpcBlock struct {
Number *hexutil.Big `json:"number"`
Hash common.Hash `json:"hash"`
ParentHash common.Hash `json:"parentHash"`
GasUsed *hexutil.Big `json:"gasUsed"`
GasLimit *hexutil.Big `json:"gasLimit"`
StateRoot common.Hash `json:"stateRoot"`
Miner common.Address `json:"miner"`
Difficulty *hexutil.Big `json:"difficulty"`
Nonce string `json:"nonce"`
MixHash common.Hash `json:"mixHash"`
Timestamp *hexutil.Big `json:"timestamp"`
}

// getMordorRPC returns the Mordor RPC endpoint.
func getMordorRPC() string {
if v := os.Getenv("MORDOR_RPC"); v != "" {
return v
}
return "http://localhost:8545"
}

// getETCRPC returns the ETC mainnet RPC endpoint.
func getETCRPC() string {
if v := os.Getenv("ETC_RPC"); v != "" {
return v
}
return "https://etc.rivet.link"
}

// dialRPC connects to an RPC endpoint with a 10-second timeout.
func dialRPC(t *testing.T, endpoint string) *rpc.Client {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := rpc.DialContext(ctx, endpoint)
if err != nil {
t.Skipf("cannot connect to %s: %v", endpoint, err)
}
return client
}

// getChainID returns the chain ID from eth_chainId.
func getChainID(t *testing.T, client *rpc.Client) uint64 {
t.Helper()
var result hexutil.Big
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.CallContext(ctx, &result, "eth_chainId"); err != nil {
t.Fatalf("eth_chainId failed: %v", err)
}
return result.ToInt().Uint64()
}

// getBlockByNumber fetches a block by number.
func getBlockByNumber(t *testing.T, client *rpc.Client, num *big.Int) *rpcBlock {
t.Helper()
var block rpcBlock
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var numArg string
if num == nil {
numArg = "latest"
} else {
numArg = hexutil.EncodeBig(num)
}
if err := client.CallContext(ctx, &block, "eth_getBlockByNumber", numArg, false); err != nil {
t.Fatalf("eth_getBlockByNumber(%s) failed: %v", numArg, err)
}
return &block
}
Comment on lines +118 to +122
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getBlockByNumber decodes the JSON-RPC response into a value struct; if the node returns null for an unavailable block (common when syncing or when an ancient block is not present), the pointer fields (e.g., Number, GasLimit, Difficulty) will remain nil and callers will panic when calling ToInt(). Consider decoding into a *rpcBlock and skipping/failing with a clear message when the result is nil, or explicitly validating required fields after the call and t.Skipf/t.Fatalf before returning.

Copilot uses AI. Check for mistakes.

// getNetVersion returns the network version from net_version.
func getNetVersion(t *testing.T, client *rpc.Client) string {
t.Helper()
var result string
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.CallContext(ctx, &result, "net_version"); err != nil {
t.Fatalf("net_version failed: %v", err)
}
return result
}

// getSyncing returns whether the node is syncing.
func getSyncing(t *testing.T, client *rpc.Client) json.RawMessage {
t.Helper()
var result json.RawMessage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.CallContext(ctx, &result, "eth_syncing"); err != nil {
t.Fatalf("eth_syncing failed: %v", err)
}
return result
}
108 changes: 108 additions & 0 deletions tests/live_etc/mainnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//go:build live

package live_etc

import (
"math/big"
"testing"
)

// TestETCMainnetChainID verifies the ETC mainnet reports chain ID 61.
func TestETCMainnetChainID(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

chainID := getChainID(t, client)
if chainID != ETCMainnetChainID {
t.Errorf("ETC mainnet chain ID = %d, want %d", chainID, ETCMainnetChainID)
}
}

// TestETCMainnetGenesisHash verifies the ETC mainnet genesis block hash
// (same as original Ethereum genesis — the chain split happened at block 1,920,000).
func TestETCMainnetGenesisHash(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

genesis := getBlockByNumber(t, client, big.NewInt(0))
if genesis.Hash != ETCGenesisHash {
t.Errorf("ETC genesis hash = %s, want %s", genesis.Hash.Hex(), ETCGenesisHash.Hex())
}
}

// TestETCMainnetPoW verifies that ETC mainnet blocks have valid PoW fields.
func TestETCMainnetPoW(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

block := getBlockByNumber(t, client, nil) // latest
if block.Difficulty == nil || block.Difficulty.ToInt().Sign() <= 0 {
t.Error("latest block has zero or nil difficulty — not PoW")
}
}

// TestETCMainnetECIP1017Era verifies the current ECIP-1017 era based on
// the latest block number. As of 2026, ETC is in era 4 (blocks 20M-25M).
func TestETCMainnetECIP1017Era(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

block := getBlockByNumber(t, client, nil)
blockNum := block.Number.ToInt().Int64()

if blockNum < ClassicEraLength {
t.Skipf("block %d is before era 1 start", blockNum)
}

era := (blockNum - 1) / ClassicEraLength
t.Logf("ETC mainnet block %d is in era %d", blockNum, era)

// As of early 2026, should be in era 4 (20M+) heading toward era 5 (25M+)
if era < 3 {
t.Errorf("expected era >= 3 for current ETC mainnet, got era %d (block %d)", era, blockNum)
Comment on lines +54 to +62
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The era calculation is off-by-one relative to ECIP-1017’s era numbering: ECIP era 1 starts at block 5,000,000, but (blockNum-1)/ClassicEraLength yields 0 at block 5,000,000. Either compute a 1-based era (e.g., subtract the fork block and add 1) or rename the variable/logging to make it explicit that this is a 0-based era index; otherwise the comments and logs ("era 4 (20M-25M)") are misleading.

Suggested change
t.Skipf("block %d is before era 1 start", blockNum)
}
era := (blockNum - 1) / ClassicEraLength
t.Logf("ETC mainnet block %d is in era %d", blockNum, era)
// As of early 2026, should be in era 4 (20M+) heading toward era 5 (25M+)
if era < 3 {
t.Errorf("expected era >= 3 for current ETC mainnet, got era %d (block %d)", era, blockNum)
t.Skipf("block %d is before ECIP-1017 era 1 start", blockNum)
}
// ECIP-1017 era numbering is 1-based and starts at block ClassicEraLength.
era := (blockNum-ClassicEraLength)/ClassicEraLength + 1
t.Logf("ETC mainnet block %d is in ECIP-1017 era %d", blockNum, era)
// As of early 2026, should be in era 4 (20M+) heading toward era 5 (25M+).
if era < 4 {
t.Errorf("expected ECIP-1017 era >= 4 for current ETC mainnet, got era %d (block %d)", era, blockNum)

Copilot uses AI. Check for mistakes.
}
}

// TestETCMainnetGasLimit verifies the ETC mainnet gas limit is around 8M.
func TestETCMainnetGasLimit(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

block := getBlockByNumber(t, client, nil)
gasLimit := block.GasLimit.ToInt().Uint64()

if gasLimit < 7_000_000 || gasLimit > 9_000_000 {
t.Errorf("ETC mainnet gas limit = %d, expected ~8M", gasLimit)
}
}

// TestETCMainnetNetVersion verifies net_version returns "1" (ETC network ID).
func TestETCMainnetNetVersion(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

version := getNetVersion(t, client)
if version != "1" {
t.Errorf("ETC mainnet net_version = %q, want %q", version, "1")
}
}

// TestETCMainnetDAOForkBlock verifies that ETC did NOT execute the DAO fork.
// Block 1,920,000 should have the "classic" state root (no irregular state change).
Comment on lines +90 to +91
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says this test verifies the DAO-fork block’s “classic” state root, but the implementation only checks the block hash. Either update the comment to match what’s asserted, or add a StateRoot check (the rpcBlock struct already includes it) so the test actually validates the stated behavior.

Suggested change
// TestETCMainnetDAOForkBlock verifies that ETC did NOT execute the DAO fork.
// Block 1,920,000 should have the "classic" state root (no irregular state change).
// TestETCMainnetDAOForkBlock verifies that ETC did NOT execute the DAO fork by checking
// the canonical ("classic") block hash for block 1,920,000 (no irregular state change).

Copilot uses AI. Check for mistakes.
func TestETCMainnetDAOForkBlock(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

// The DAO fork block
block := getBlockByNumber(t, client, big.NewInt(1920000))

if block.Number.ToInt().Int64() != 1920000 {
t.Fatalf("expected block 1920000, got %d", block.Number.ToInt().Int64())
}

// ETC's block 1920000 hash — confirms this chain rejected the DAO fork
expectedHash := "0x94365e3a8c0b35089c1d1195081fe7489b528a84b22199c916180db8b28ade7f"
if block.Hash.Hex() != expectedHash {
t.Errorf("DAO fork block hash = %s, want %s (ETC classic chain)", block.Hash.Hex(), expectedHash)
Comment on lines +103 to +106
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected DAO-fork block hash is duplicated here as a string literal, but the same value already exists in params.ClassicChainConfig.RequireBlockHashes[1920000] (see params/config_classic.go). Using the canonical value from chain config would avoid drift if the config is ever updated and removes the need to hardcode consensus constants in the test.

Copilot uses AI. Check for mistakes.
}
}
99 changes: 99 additions & 0 deletions tests/live_etc/mordor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//go:build live

package live_etc

import (
"math/big"
"testing"
)

// TestMordorChainID verifies the Mordor testnet reports chain ID 63.
func TestMordorChainID(t *testing.T) {
client := dialRPC(t, getMordorRPC())
defer client.Close()

chainID := getChainID(t, client)
if chainID != MordorChainID {
t.Errorf("Mordor chain ID = %d, want %d", chainID, MordorChainID)
}
}

// TestMordorGenesisHash verifies the Mordor genesis block hash.
func TestMordorGenesisHash(t *testing.T) {
client := dialRPC(t, getMordorRPC())
defer client.Close()

genesis := getBlockByNumber(t, client, big.NewInt(0))
if genesis.Hash != MordorGenesisHash {
t.Errorf("Mordor genesis hash = %s, want %s", genesis.Hash.Hex(), MordorGenesisHash.Hex())
}
}

// TestMordorPoWFields verifies that Mordor blocks have valid PoW fields
// (non-zero difficulty, non-empty nonce and mixHash).
func TestMordorPoWFields(t *testing.T) {
Comment on lines +32 to +34
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this test verifies a non-empty mixHash, but the implementation never checks for an all-zero mixHash. Add an explicit assertion that block.MixHash is not the zero hash (and/or that it differs from the parent where appropriate) so the test actually enforces the stated PoW field requirements.

Copilot uses AI. Check for mistakes.
client := dialRPC(t, getMordorRPC())
defer client.Close()

block := getBlockByNumber(t, client, nil) // latest
if block.Difficulty == nil || block.Difficulty.ToInt().Sign() <= 0 {
t.Error("latest block has zero or nil difficulty — not PoW")
}
if block.Nonce == "" || block.Nonce == "0x0000000000000000" {
t.Error("latest block has empty nonce — not PoW")
}
if block.MixHash == (MordorGenesisHash) {
// MixHash should be a unique hash from the PoW computation
t.Log("warning: mixHash equals genesis hash — unusual but not necessarily wrong")
Comment on lines +45 to +47
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block.MixHash == MordorGenesisHash is comparing a header field to the genesis block hash, which is unrelated and will almost never catch a bad mixHash. If the goal is to ensure a valid PoW mixHash, compare against the zero hash (or check for a correctly-sized non-zero value) and fail the test rather than logging a warning.

Suggested change
if block.MixHash == (MordorGenesisHash) {
// MixHash should be a unique hash from the PoW computation
t.Log("warning: mixHash equals genesis hash — unusual but not necessarily wrong")
if block.MixHash.Hex() == "0x0000000000000000000000000000000000000000000000000000000000000000" {
t.Error("latest block has zero mixHash — not PoW")

Copilot uses AI. Check for mistakes.
}
}

// TestMordorGasLimit verifies that Mordor gas limit is around 8M (pre-olympia).
func TestMordorGasLimit(t *testing.T) {
client := dialRPC(t, getMordorRPC())
defer client.Close()

block := getBlockByNumber(t, client, nil) // latest
gasLimit := block.GasLimit.ToInt().Uint64()

// Gas limit should be near 8M (within adjustment bounds)
if gasLimit < 7_000_000 || gasLimit > 9_000_000 {
t.Errorf("Mordor gas limit = %d, expected ~8M (pre-olympia)", gasLimit)
}
}

// TestMordorDifficultyProgression verifies that difficulty is non-trivial
// and blocks have incrementing numbers.
func TestMordorDifficultyProgression(t *testing.T) {
client := dialRPC(t, getMordorRPC())
defer client.Close()

latest := getBlockByNumber(t, client, nil)
latestNum := latest.Number.ToInt().Int64()

if latestNum < 10_000_000 {
t.Skipf("Mordor chain height %d too low for meaningful difficulty check", latestNum)
}

// Check a block from around 1M blocks ago
oldNum := latestNum - 1_000_000
oldBlock := getBlockByNumber(t, client, big.NewInt(oldNum))

if oldBlock.Difficulty == nil || latest.Difficulty == nil {
t.Fatal("difficulty is nil")
}

t.Logf("Block %d difficulty: %s", oldNum, oldBlock.Difficulty.ToInt().String())
t.Logf("Block %d difficulty: %s", latestNum, latest.Difficulty.ToInt().String())
Comment on lines +86 to +87
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestMordorDifficultyProgression currently only logs difficulties and doesn't assert anything about progression or even that the fetched historical block number matches oldNum. Add concrete assertions (e.g., oldBlock.Number == oldNum, latest.Number == oldNum+1/monotonicity, and that difficulties are > 0) so the test can actually fail when the invariants are violated.

Suggested change
t.Logf("Block %d difficulty: %s", oldNum, oldBlock.Difficulty.ToInt().String())
t.Logf("Block %d difficulty: %s", latestNum, latest.Difficulty.ToInt().String())
// Verify that the fetched historical block number matches the requested number.
oldBlockNum := oldBlock.Number.ToInt().Int64()
if oldBlockNum != oldNum {
t.Fatalf("fetched block number %d does not match requested number %d", oldBlockNum, oldNum)
}
// Verify block number monotonicity: latest block must be newer than the old block.
if latestNum <= oldBlockNum {
t.Fatalf("latest block number %d is not greater than old block number %d", latestNum, oldBlockNum)
}
// Verify that both difficulties are strictly positive.
oldDiff := oldBlock.Difficulty.ToInt()
latestDiff := latest.Difficulty.ToInt()
if oldDiff.Sign() <= 0 || latestDiff.Sign() <= 0 {
t.Fatalf("non-positive difficulty detected: old=%s, latest=%s", oldDiff.String(), latestDiff.String())
}
t.Logf("Block %d difficulty: %s", oldNum, oldDiff.String())
t.Logf("Block %d difficulty: %s", latestNum, latestDiff.String())

Copilot uses AI. Check for mistakes.
}

// TestMordorNetVersion verifies net_version returns "7" (Mordor network ID).
func TestMordorNetVersion(t *testing.T) {
client := dialRPC(t, getMordorRPC())
defer client.Close()

version := getNetVersion(t, client)
if version != "7" {
t.Errorf("Mordor net_version = %q, want %q", version, "7")
}
}
Loading