Skip to content

EIP-2124 ForkID Implementation Analysis

Overview

This document analyzes Fukuii's implementation of EIP-2124 (ForkID) and compares it with the reference implementation in Core-Geth to ensure compatibility for peer-to-peer network communications.

Date: 2025-12-04
Issue: #710 - run 010
Reference Spec: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md
Core-Geth Reference: https://github.com/etclabscore/core-geth

Executive Summary

Fukuii's ForkID implementation is CORRECT and COMPATIBLE with Core-Geth

Our implementation correctly follows the EIP-2124 specification and produces identical ForkID values as Core-Geth for ETC mainnet. The peer connection issues mentioned in the issue are NOT caused by ForkID implementation differences.

EIP-2124 Specification Summary

Purpose

EIP-2124 defines a Fork Identifier scheme to: - Precisely identify which chain a node is on (ETH vs ETC, etc.) - Reject incompatible peers before establishing expensive TCP connections - Distinguish between stale nodes vs nodes on different chains

Fork Identifier Structure

ForkID = RLP([FORK_HASH, FORK_NEXT])
  • FORK_HASH: 4-byte CRC32 checksum of genesis hash + all passed fork block numbers
  • FORK_NEXT: Block number of next upcoming fork, or 0 if no forks are known

Calculation Algorithm

1. Start with CRC32(genesis_hash)
2. For each passed fork: CRC32_update(previous_hash, fork_block_number_as_uint64_BE)
3. FORK_NEXT = first fork block > current_head, or 0 if none

Validation Rules

  1. Matching hashes: If local and remote FORK_HASH match:
  2. 1a) If remote FORK_NEXT already passed locally → REJECT (incompatible)
  3. 1b) Otherwise → ACCEPT

  4. Subset: If remote FORK_HASH is a subset of local past forks AND remote FORK_NEXT matches the next local fork → ACCEPT (remote syncing)

  5. Superset: If remote FORK_HASH is a superset of local past forks → ACCEPT (local syncing)

  6. Otherwise: REJECT (incompatible chains)

Implementation Comparison

Fukuii Implementation

Location: src/main/scala/com/chipprbots/ethereum/forkid/

ForkId.scala

def create(genesisHash: ByteString, config: BlockchainConfig)(head: BigInt): ForkId = {
    val crc = new CRC32()
    crc.update(genesisHash.asByteBuffer)
    val forks = gatherForks(config)

    val next = forks.find { fork =>
      if (fork <= head) {
        crc.update(bigIntToBytes(fork, 8))  // uint64 as 8 bytes, big endian
      }
      fork > head
    }
    new ForkId(crc.getValue(), next)
}

ForkIdValidator.scala

Implements all 4 validation rules: - checkMatchingHashes() - Rule 1 - checkSubset() - Rule 2 - checkSuperset() - Rule 3 - Default reject - Rule 4

Core-Geth Implementation

Location: core/forkid/forkid.go

func NewID(config ctypes.ChainConfigurator, genesis *types.Block, head, time uint64) ID {
    hash := crc32.ChecksumIEEE(genesis.Hash().Bytes())

    forksByBlock, forksByTime := gatherForks(config, genesis.Time())
    for _, fork := range forksByBlock {
        if fork <= head {
            hash = checksumUpdate(hash, fork)
            continue
        }
        return ID{Hash: checksumToBytes(hash), Next: fork}
    }
    // ... time-based forks handling ...
    return ID{Hash: checksumToBytes(hash), Next: 0}
}

func checksumUpdate(hash uint32, fork uint64) uint32 {
    var blob [8]byte
    binary.BigEndian.PutUint64(blob[:], fork)  // uint64 as 8 bytes, big endian
    return crc32.Update(hash, crc32.IEEETable, blob[:])
}

Fork Numbers Comparison

ETC Mainnet Forks

Fork Name Block Number Fukuii Core-Geth Match
Homestead (EIP-2/7) 1,150,000
Gas Reprice (EIP-150) 2,500,000
Diehard (EIP-155/160, ECIP-1010) 3,000,000
ECIP-1017 (Monetary Policy) 5,000,000
Defuse Difficulty Bomb 5,900,000
Atlantis (Byzantium) 8,772,000
Agharta (Constantinople) 9,573,000
Phoenix (Istanbul) 10,500,839
Thanos (ECIP-1099) 11,700,000
Magneto (Berlin) 13,189,133
Mystique (London partial) 14,525,000
Spiral (Shanghai partial) 19,250,000

Result: All 12 fork blocks match exactly ✅

Test Case Verification

Both implementations include identical test cases for ETC mainnet. Here are key test points:

Block Height Expected FORK_HASH Expected FORK_NEXT Fukuii Core-Geth
0 0xfc64ec04 1,150,000
1,150,000 0x97c2c34c 2,500,000
2,500,000 0xdb06803f 3,000,000
3,000,000 0xaff4bed4 5,000,000
5,000,000 0xf79a63c0 5,900,000
5,900,000 0x744899d6 8,772,000
8,772,000 0x518b59c6 9,573,000
9,573,000 0x7ba22882 10,500,839
10,500,839 0x9007bfcc 11,700,000
11,700,000 0xdb63a1ca 13,189,133
13,189,133 0x0f6bf187 14,525,000
14,525,000 0x7fd1bb25 19,250,000
19,250,000 0xbe46d57c 0

Source: - Fukuii: src/test/scala/com/chipprbots/ethereum/forkid/ForkIdSpec.scala - Core-Geth: core/forkid/forkid_test.go

Mordor Testnet Forks

Block Height Expected FORK_HASH Expected FORK_NEXT Fukuii Core-Geth
0 0x175782aa 301,243
301,243 0x604f6ee1 999,983
999,983 0xf42f5539 2,520,000
2,520,000 0x66b5c286 3,985,893
3,985,893 0x92b323e0 5,520,000
5,520,000 0x8c9b1797 9,957,000
9,957,000 0x3a6b00d7 0

Result: All Mordor fork IDs match exactly ✅

RLP Encoding Verification

Both implementations use the same RLP encoding format:

ForkID = RLP([FORK_HASH, FORK_NEXT])

Where: - FORK_HASH: 4 bytes (big-endian uint32) - FORK_NEXT: 8 bytes (big-endian uint64), or empty if 0

Test Cases

ForkID Expected RLP Hex Fukuii Core-Geth
ForkId(0, None) c6840000000080
ForkId(0xdeadbeef, Some(0xbaddcafe)) ca84deadbeef84baddcafe
ForkId(0xffffffff, Some(0xffffffffffffffff)) ce84ffffffff88ffffffffffffffff

Key Differences (None Affecting Compatibility)

1. Language

  • Fukuii: Scala 3
  • Core-Geth: Go

2. Code Organization

  • Fukuii: Separates ForkId creation and validation into two files
  • Core-Geth: Combines both in one file

3. Time-Based Forks

  • Core-Geth: Supports both block-based and timestamp-based forks (for Ethereum Merge)
  • Fukuii: Currently only supports block-based forks (appropriate for ETC which is PoW-only)

Note: ETC does not use timestamp-based forks, so this difference does not affect ETC compatibility.

Validation Logic Comparison

Both implementations follow the exact same 4-step validation algorithm from EIP-2124:

Rule 1: Matching Hashes

// Fukuii
case ForkId(hash, _) if checksum != hash            => None
case ForkId(_, Some(next)) if currentHeight >= next => Some(ErrLocalIncompatibleOrStale)
case _                                              => Some(Connect)
// Core-Geth
if sums[i] == id.Hash {
    if id.Next > 0 && (head >= id.Next || ...) {
        return ErrLocalIncompatibleOrStale
    }
    return nil
}

Rule 2: Subset Check

Both check if remote hash appears in past local checksums and remote.Next matches the corresponding local fork.

Rule 3: Superset Check

Both check if remote hash appears in future local checksums.

Rule 4: Default Reject

Both reject if none of the above rules match.

Conclusion

Implementation Status: ✅ CORRECT

  1. Fork Numbers: Fukuii uses identical fork block numbers as Core-Geth for ETC mainnet
  2. Hash Calculation: Identical CRC32 algorithm with big-endian uint64 encoding
  3. RLP Encoding: Identical RLP encoding format
  4. Validation Logic: Identical 4-step validation algorithm
  5. Test Coverage: Comprehensive test suite matching Core-Geth test cases

Peer Connection Issues

Since the ForkID implementation is correct and compatible, the peer connection issues mentioned in the original issue are NOT caused by ForkID incompatibility. Potential other causes to investigate:

  1. Network Layer: RLPx handshake issues unrelated to ForkID
  2. Capability Negotiation: Issues with eth/6x protocol version negotiation
  3. Message Encoding: Problems with Snappy compression or message framing
  4. Peer Discovery: Issues finding and connecting to peers
  5. Configuration: Incorrect network ID or genesis hash settings

Recommendations

  1. ✅ Continue using current ForkID implementation - it is correct
  2. 🔍 Investigate RLPx handshake and message encoding for peer connection issues
  3. 📝 Document that Fukuii's ForkID is verified compatible with Core-Geth
  4. ✅ Keep test suite aligned with Core-Geth for future fork updates

References

  • EIP-2124: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2124.md
  • Core-Geth Implementation: https://github.com/etclabscore/core-geth/tree/master/core/forkid
  • Fukuii Implementation: src/main/scala/com/chipprbots/ethereum/forkid/
  • Test Suites:
  • Fukuii: src/test/scala/com/chipprbots/ethereum/forkid/ForkIdSpec.scala
  • Core-Geth: core/forkid/forkid_test.go

Appendix: Fork List Source Code

Fukuii Fork Gathering

def gatherForks(config: BlockchainConfig): List[BigInt] = {
  val maybeDaoBlock: Option[BigInt] = config.daoForkConfig.flatMap { daoConf =>
    if (daoConf.includeOnForkIdList) Some(daoConf.forkBlockNumber)
    else None
  }

  (maybeDaoBlock.toList ++ config.forkBlockNumbers.all)
    .filterNot(v => v == 0 || v == noFork)
    .distinct
    .sorted
}

Core-Geth Fork Gathering

Core-Geth uses a reflective approach via confp.BlockForks() which extracts all non-zero, non-compatibility fork blocks from the chain configuration and sorts them.

Both approaches produce identical fork lists for ETC mainnet.