erigontech / erigontech/erigon
execution/chain, p2p/forkid: six encodings of the fork ladder, and a chain's own schedule reaches none of them
- 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
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