erigontech / erigontech/erigon

execution/chain, p2p/forkid: six encodings of the fork ladder, and a chain's own schedule reaches none of them

Open
#23,602 2 comments 0 reactions 0 assignees View on GitHub
Networking
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

A chain that puts its fork schedule behind the config slot from #22194 contributes zero fork points to its own forkid. Crossing that fork changes neither `FORK_HASH` nor `FORK_NEXT`, so an upgraded node and a stale one keep accepting each other. Nothing errors.

The ladder is written down six times, no two sharing a source:

| where | shape | covers |
|---|---|---|
| `execution/chain/chain_config.go:608` `forkBlockNumbers()` | ordered slice, `optional`/`outOfOrder` annotated | block forks, ends at `mergeNetsplitBlock` |
| `p2p/forkid/forkid.go:200` `GatherForks` | reflection over `chain.Config` fields | any `*uint64` named `*Block`/`*Time` |
| `execution/vm/interpreter.go:308` `jumpTable` | 14-arm switch → instruction set | Frontier → Amsterdam |
| `execution/vm/contracts.go:96` `forkTierFor` | 7-arm switch → precompile set | Homestead → Osaka |
| `execution/vm/evmtypes/rules.go:26` `Rules()` | one assignment per `IsX` | all |
| `txnprovider/txpool/pool.go:147` | a `*uint64` field and an atomic per fork | Shanghai → Osaka |

Two are defects.

`GatherForks` keeps fields named `*Block`/`*Time` of type `*uint64`. `Config.L2` (`chain_config.go:133`) is an interface and `L2JSON` is `json.RawMessage`; neither matches, so a schedule held there is invisible. Chains that did not fit the struct got a hardcoded arm instead — Bor's until #23495, and `forkid.go:231` still carries `config.Aura != nil` for `PosdaoTransition`. An out-of-tree chain cannot get one.

`CheckConfigForkOrder` is the only ordering check and consumes `forkBlockNumbers()`, which ends at `mergeNetsplitBlock`. No time-based fork is order-checked: Shanghai, Cancun, Prague, Osaka, Amsterdam.

Not latent. `bsc_support` keeps its Parlia forks as plain `*uint64` on the core config and says why at `execution/chain/chain_config.go:102-105` — *"Kept as plain `*uint64` on the core config so that forkid gathers them by reflection"* — while the same comment puts engine state behind the L2 config. A seventh integration adopts the slot everywhere except where `GatherForks` forces the bypass, with the reason written down.

Fix: a `ForkPoints() (heights, times []uint64)` accessor on `L2Config`, consumed by `GatherForks` and the pool, and one ordered schedule covering time forks for the order check. The two `execution/vm` switches differ in granularity on purpose and are out of scope.

Found while reviewing #22217. Part of #22193.

Contributor guide

Open the contributing guide

Research direction

Start with execution/chain/chain_config.go, especially L2Config and forkBlockNumbers(), then trace p2p/forkid/forkid.go:200 GatherForks and txnprovider/txpool/pool.go:147. Review CheckConfigForkOrder and the existing fork schedule fields before defining the accessor. Done means schedules behind L2 are included in fork IDs and the pool, while block- and time-based forks are covered by ordering checks.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
distributed-systems, networking
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.