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

// ECBP-1100 (MESS) activation windows
MordorECBP1100Activate = 2_380_000
MordorECBP1100Deactivate = 10_400_000

ClassicECBP1100Activate = 11_380_000
ClassicECBP1100Deactivate = 19_250_000

// Spiral fork blocks
MordorSpiralBlock = 9_957_000
ClassicSpiralBlock = 19_250_000

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

// Etchash epoch lengths
EpochLengthDefault = 30_000
EpochLengthECIP1099 = 60_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 +121 to +137
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 unmarshals into a non-pointer struct. If the RPC returns JSON null for an unavailable block (common on pruned/partial endpoints), rpc.CallContext will succeed and leave block zero-valued, causing downstream nil dereferences (e.g., block.Number.ToInt()). Consider unmarshaling into var block *rpcBlock and failing the test when block == nil (mirroring ethclient.HeaderByNumber).

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
}
166 changes: 166 additions & 0 deletions tests/live_etc/mainnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//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 +44 to +63
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.

TestETCMainnetECIP1017Era’s comment says “era 4 (blocks 20M-25M)”, but the computed era := (blockNum - 1) / ClassicEraLength is 0-indexed and yields 3 for block 20,000,000. Either adjust the era calculation to match the stated era numbering, or fix the comment/assertions to avoid an off-by-one interpretation.

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).
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 +90 to +107
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 “classic” state root at block 1,920,000, but the implementation checks the block hash instead. Either update the comment to match what’s asserted, or also assert the expected state root if that’s the intended verification.

Copilot uses AI. Check for mistakes.
}

// TestETCMainnetECBP1100Deactivated verifies ECBP-1100 (MESS) is deactivated
// on ETC mainnet. MESS was active 11,380,000→19,250,000 (deactivated at Spiral).
func TestETCMainnetECBP1100Deactivated(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

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

if blockNum < ClassicECBP1100Deactivate {
t.Skipf("ETC mainnet %d has not reached ECBP-1100 deactivation (%d)",
blockNum, ClassicECBP1100Deactivate)
}

t.Logf("ETC mainnet block %d is past ECBP-1100 deactivation at %d (Spiral)",
blockNum, ClassicECBP1100Deactivate)
}

// TestETCMainnetECIP1099Epoch verifies ECIP-1099 epoch calculation on live chain.
// After block 11,700,000, epochs are 60,000 blocks long.
func TestETCMainnetECIP1099Epoch(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

latest := getBlockByNumber(t, client, nil)
blockNum := latest.Number.ToInt().Uint64()

if blockNum < ClassicECIP1099Block {
t.Skipf("ETC mainnet block %d is before ECIP-1099 activation", blockNum)
}

epoch := blockNum / EpochLengthECIP1099
t.Logf("ETC mainnet block %d is in etchash epoch %d (60K-block epochs)", blockNum, epoch)

if epoch == 0 {
t.Errorf("epoch should not be 0 at block %d", blockNum)
}
Comment on lines +141 to +146
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.

TestETCMainnetECIP1099Epoch is meant to verify the 30K→60K epoch-length change, but it only computes blockNum / 60000 for the latest block and asserts epoch != 0. Consider asserting epoch behavior across the fork boundary (pre-activation uses 30K, post-activation uses 60K) so the test actually detects an incorrect epoch length.

Suggested change
epoch := blockNum / EpochLengthECIP1099
t.Logf("ETC mainnet block %d is in etchash epoch %d (60K-block epochs)", blockNum, epoch)
if epoch == 0 {
t.Errorf("epoch should not be 0 at block %d", blockNum)
}
// Pre-ECIP-1099 epochs are 30,000 blocks long; post-ECIP-1099 epochs are 60,000 blocks long.
const epochLengthPreECIP1099 uint64 = 30000
preBlock := uint64(ClassicECIP1099Block - 1)
postBlock := uint64(ClassicECIP1099Block)
preEpoch := preBlock / epochLengthPreECIP1099
postEpoch := postBlock / EpochLengthECIP1099
t.Logf("ETC mainnet ECIP-1099 boundary: pre-block %d in epoch %d (30K-block epochs), post-block %d in epoch %d (60K-block epochs)",
preBlock, preEpoch, postBlock, postEpoch)
if preEpoch == 0 {
t.Errorf("pre-ECIP-1099 epoch should not be 0 at block %d", preBlock)
}
if postEpoch == 0 {
t.Errorf("post-ECIP-1099 epoch should not be 0 at block %d", postBlock)
}
if preEpoch == postEpoch {
t.Errorf("expected different epochs across ECIP-1099 boundary, got pre=%d (block %d), post=%d (block %d)",
preEpoch, preBlock, postEpoch, postBlock)
}
// Also log the epoch for the latest block using the post-ECIP-1099 epoch length.
latestEpoch := blockNum / EpochLengthECIP1099
t.Logf("ETC mainnet latest block %d is in etchash epoch %d (60K-block epochs)", blockNum, latestEpoch)

Copilot uses AI. Check for mistakes.
}

// TestETCMainnetSpiralForkBlock verifies the Spiral fork block exists on mainnet.
func TestETCMainnetSpiralForkBlock(t *testing.T) {
client := dialRPC(t, getETCRPC())
defer client.Close()

latest := getBlockByNumber(t, client, nil)
if latest.Number.ToInt().Int64() < ClassicSpiralBlock {
t.Skipf("ETC mainnet %d has not reached Spiral (%d)",
latest.Number.ToInt().Int64(), ClassicSpiralBlock)
}

spiral := getBlockByNumber(t, client, big.NewInt(ClassicSpiralBlock))
if spiral.Difficulty == nil || spiral.Difficulty.ToInt().Sign() <= 0 {
t.Error("Spiral fork block has zero difficulty")
}
t.Logf("ETC mainnet Spiral block %d: difficulty=%s",
ClassicSpiralBlock, spiral.Difficulty.ToInt().String())
}
Comment on lines +149 to +166
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.

PR description mentions (1) fork verification by cross-referencing chain config for all 14 ECIP-1066 forks and (2) Spiral fork verification of EIP-3855/EIP-3860/EIP-6049. In this change set, the Spiral tests only check that the fork block exists and has non-zero difficulty/gasLimit, and there are no ECIP-1066 fork cross-reference tests. Please either add the missing assertions/tests or update the PR description to match what’s implemented.

Copilot uses AI. Check for mistakes.
Loading
Loading