erigontech / erigontech/erigon
Chain SDK: extension points for running a custom chain on erigon core
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
Erigon has taken on six non-Ethereum chains: Gnosis, BSC, Polygon, Arbitrum, Taiko, Optimism. One lives in core. Two were put in core and later torn back out — BSC/Parlia in 2023 (#3086 in, #7246 and #7306 out) and Polygon/Bor in 2026 (#23495/#23497 out, tracked in #23503). Three sit on branches, two of them long stale (`taiko` 2025-07, `op-erigon` 2025-03; `arbitrum` 2026-03 is ~3,800 commits behind). `bsc_support` is the exception and the subject of the section below: 172 commits behind main, actively developed. None of the six runs as an external repository.
**Goal:** a third party writes a thin repository that imports erigon core as a library, registers their chain, and runs it — with zero commits to erigon core.
**Acceptance criterion:** core files touched per chain integration, target zero. Two historical baselines, counted the same way — union of that chain's removal-PR diffs, excluding only the chain's own top-level directory: BSC **43** (excludes `consensus/parlia/`; #7246, #7306), Polygon **208** (excludes `polygon/`; #23492/#23495/#23497/#23505). Roughly one core file entangled per hundred lines of chain code — 208/20,161 and 43/3,314.
An earlier revision said 41 for BSC. It additionally excluded `core/vm/contracts_lightclient.go` and its test — BSC's precompiles, inside core's `vm` package. Those are exactly what the criterion counts.
## The required surface, measured
Rather than guessing which seams matter, this is what all six integrations actually had to touch. Cells were re-derived from each chain's own diff: the removal-PR union for Polygon and BSC, `merge-base..branch` for Arbitrum, Taiko and Optimism, and what Aura still touches on main for Gnosis.
| subsystem | Polygon | Gnosis | Arbitrum | BSC | Taiko | Optimism | count |
|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| chain config (`chain.Config` struct) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **6** |
| chainspec / genesis registration | ✓ | ✓ | ✓ | ✓ | ✓ | bypass | **6** |
| staged sync | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **6** |
| EVM fork gating | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **6** |
| node assembly | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | **6** |
| rules engine construction | ✓ | ✓ | ✓ | ✓ | ✓ | – | **5** |
| CLI flags | ✓ | ✓ | ✓ | ✓ | ✓ | – | **5** |
| txpool / txnprovider | ✓ | ✓ | ✓ | – | bypass | ✓ | **5** |
| block reader / snapshots | ✓ | ✓ | ✓ | ✓ | ✓ | – | **5** |
| tx types | ✓ | – | ✓ | – | ✓ | ✓ | **4** |
| RPC namespace registration | ✓ | ✓ | ✓ | – | ✓ | – | **4** |
| KV tables | ✓ | ✓ | ✓ | – | ✓ | – | **4** |
| internal gRPC / proto | ✓ | ✓ | – | – | ✓ | ✓ | **4** |
| precompiles | ✓ | – | ✓ | ✓ | – | ✓ | **4** |
| p2p / forkid | ✓ | ✓ | – | ✓ | – | ✓ | **4** |
| receipt / txn marshalling | ✓ | – | ✓ | – | – | ✓ | **3** |
**The 4/6 cut has stopped discriminating.** It was chosen to separate "the SDK's job" from "that chain's own problem". After re-derivation it separates 15 rows from one. Either the whole surface is the SDK's job, or the cut needs replacing with something that ranks — cost to seam, or how many chains had to edit the *same file*.
Cells that moved, each with the deciding file:
| cell | was | now | evidence |
|---|:-:|:-:|---|
| BSC precompiles | – | ✓ | `core/vm/contracts.go`, `contracts_lightclient.go` |
| BSC block reader | – | ✓ | `turbo/snapshotsync/snapcfg/util.go` |
| BSC EVM fork gating | – | ✓ | `core/state_transition.go`, `isParlia` gating `gasBailout` |
| BSC staged sync | bypass | ✓ | `eth/stagedsync/stage_execute.go` calls `ExecuteBlockEphemerallyForBSC` — a chain branch inside the shared stage |
| Arbitrum block reader | – | ✓ | `DumpTxs` swaps in `types.NewArbitrumSigner` |
| Arbitrum gRPC / proto | ? | – | no `.proto` of its own |
| Optimism p2p / forkid | – | ✓ | `params/bootnodes.go`, `V5OPBootnodes` in `BootnodeURLsOfChain` |
| Optimism rules engine | ✓ | – | `oplegacy.OpLegacy` never matched in `CreateConsensusEngine` — dead code |
| Gnosis receipt marshalling | ✓ | – | `execution/types/receipt.go` has zero Aura refs; the touch is header marshalling at `execution/types/block.go:97-99` |
| Taiko node assembly | – | ✓ | `eth/backend.go` |
| Taiko txpool | – | bypass | |
Every one of the six added a field to the chain-config struct — `erigon-lib/chain/chain_config.go` for the branches predating #16557's rename, `execution/chain/chain_config.go` after it — in four incompatible shapes: a typed pointer sub-struct (`Aura *AuRaConfig`, `Parlia *ParliaConfig`, `Optimism *OptimismConfig`), a bare non-pointer interface (`Bor BorConfig`, 15 methods), a bare non-pointer struct (`ArbitrumChainParams`), and bare scalars with no sub-struct at all (Taiko's `Taiko bool` / `OntakeBlock` / `PacayaBlock`). #22194's opaque `L2`/`L2JSON` slot is the fix; nothing forces its use, and the BSC section below is what that costs.
## BSC is the first integration since the seams landed, and it used one of them
`bsc_support` is current and under active development (#23573, phase 5 of the Chapel bring-up). It is the first chain integration to start *after* all four seams merged, which makes it the epic's only real evidence. Measured against its merge-base with main: 29 files, **19 of them core**, 10 under `bsc/`.
| seam | used |
|---|---|
| `chainspec.RegisterChainSpec` | yes — `bsc/chain/config.go` |
| #22194 opaque `L2`/`L2JSON` | no — zero references |
| #22195 fork oracle (`ResolveRules`) | no — zero references |
| #22196 `RegisterL2Engine` | no — zero references |
| #22201 tx-type registry | no — zero references |
What it did instead, and why each one is a seam gap rather than an oversight:
- **25 bare `*uint64` fork fields on core `chain.Config`** (`RamanujanBlock` … `PasteurTime`), plus `Parlia *ParliaConfig` — an empty struct — and a new core file `execution/chain/parlia_config.go`. `bsc/PROGRESS.md` gives the reason: "BSC fork fields added as plain `*uint64` so forkid's reflection gathers them and they unmarshal from the chainspec." `p2p/forkid.GatherForks` reflects over `chain.Config` looking for `*uint64` fields; it cannot see through an interface. **This is #23602 costing 25 core fields today, not a latent concern.** Until `ForkPoints()` lands, the opaque slot and a working forkid are mutually exclusive.
- **Eight `Parlia` dispatch arms across five core files** — `node/rulesconfig/config.go` (2), `node/eth/backend.go` (3), `cmd/rpcdaemon/cli/config.go`, `cmd/integration/commands/stages.go`, `execution/chain/chain_config.go` (`getEngine`). In `CreateRulesEngineBareBones` the new `case chainConfig.Parlia != nil:` sits directly above the existing `case chainConfig.L2 != nil:`. The sanctioned path was adjacent in the same switch and was not taken.
- **Core now imports a chain package.** `node/rulesconfig/config.go` imports `github.com/erigontech/erigon/bsc/parlia` for `case *chain.ParliaConfig: eng = parlia.New(...)`. The lint-rule item below said it had zero subjects until Aura migrates. It has one now.
- **`ParliaRules` added to the closed `RulesName` enum** (`execution/chain/rules.go`) — the chain-name registration item, live.
- **A new `Spec.StaticPeers` field on the core chainspec**, plus per-chain protocol defaults (`[eth/70, eth/68]`) and discovery defaults keyed on `--chain=chapel` in `cmd/utils/flags.go`.
- **A BSC branch inside the shared p2p handshake.** `UpgradeStatusMsg = 0x0b` and `StatusPacketBsc70` in `p2p/protocols/eth/protocol.go`, an extra post-Status round gated on `isBscNetwork(networkID)` inside the generic `handShake[T]`, and a new arm at `p2p/sentry/sentry_grpc_server.go`. The same switch-on-chain-identity this epic condemns in the RPC layer, one layer down, with no row naming it.
`bsc/PROGRESS.md` line 12 claims Parlia is "registered via `RegisterL2Engine("parlia", …)`". Line 89 says it was wired "as `case *chain.ParliaConfig`". The code matches line 89. Worth fixing in the branch, and worth noting here: the seam was known, considered, and not used.
The rpcdaemon fall-through defect below reproduces in the same branch, half-fixed — BSC patches `remoteRulesEngine.init`'s switch but leaves the sibling `WithDatadir` switch falling through to `ethash.NewFaker()` for Parlia.
## Two findings that should change how we build this
**The RPC seam already failed.** `rules.EngineWriter` carries `APIs(chain) []rpc.API` so an engine can declare its own namespaces. Five of the six chains' engines implement it — Aura (`execution/protocol/rules/aura/aura.go:1104`), Parlia (`bsc/parlia/parlia.go:132`), Taiko (`execution/consensus/taiko/consensus.go:386`), Bor before its removal, and Optimism's `OpLegacy` — and all five return empty or nil. Optimism's is dead code besides: `OpLegacy` is never matched in `CreateConsensusEngine`. Only Arbitrum has no chain-specific engine at all.
Each chain then got its RPC surface by editing core instead — `case "bor":` and `case "taiko":` in `rpc/jsonrpc/daemon.go`, Aura's seal fields hardcoded in `rpc/ethapi/api.go`, Arbitrum overriding the `eth` and `net` services outright (`rpc/jsonrpc/daemon.go:83,90,102,109` on `origin/arbitrum`, from #17273). Five of six implement the seam; zero of six get a namespace through it.
Five teams independently routing around the same interface is not carelessness. Offering a seam is not sufficient — the epic needs an enforcement item, which is point 1 below.
The live instance, in a chain that ships: `rpc/ethapi/api.go:416-420` carries `// For Gnosis only` and hardcodes `auraSeal`/`auraStep` into block marshalling. A chain's RPC surface reached core by editing core, and it is still there.
The historical case ran the other way — core inherited from the chain. Both exhibits were removed by #23495/#23497, so they record what happened rather than what to check today: `execution/vm/jump_table.go` built `newCancunInstructionSet()` on top of `newNapoliInstructionSet()`, so mainnet's Cancun opcodes were assembled through a Polygon hardfork; and `evmtypes/rules.go` OR'd Bor's forks into the canonical flags every opcode switch reads (`IsShanghai: c.IsShanghai(bc.Time) || c.IsAgra(bc.BlockNumber)`). #22195's fork oracle fixes the second. The first is what happens when there is no fork-tier registration and a chain edits the inheritance chain in place.
**Two chains widened the same interface incompatibly.** Taiko bolted `MarkAsAnchor()` onto the shared `Transaction` interface; Arbitrum independently added `IsTimeBoosted()` and `SetTimeboosted()` (#17243). Neither knew about the other. This is exactly the failure `execution/types/tx_registry.go` (#22201) was built to prevent, and it is the argument for closing the remaining dispatch sites rather than leaving the registry half-wired.
## Landed
- [x] #22194 — opaque config slot on `chain.Config` (`L2JSON`/`L2Config`)
- [x] #22195 — fork oracle on `Rules`; `MakeSignerFromRules`
- [x] #22196 — registry for chain rules engines in `CreateRulesEngine`
- [x] #22201 — additive transaction-type registry
- [x] #22217 — one fork tier behind `Precompiles`/`ActivePrecompiles`; resolved `Rules` on `tracing.VMContext`
- [x] #23565 — tx-type registry wired into the three receipt dispatch sites, so a registered type's receipts round-trip
None of them has a production consumer. Only tests register into them, and the one integration that started after they landed bypassed four of five. That is the epic's main risk, and it is now measured rather than predicted.
## In flight
- [ ] #22200 — tx lifecycle hooks on `rules.EngineReader`
- [ ] #23908 — chain-keyed precompile provider registry: the consumer #22217 landed the fork tier for and held back
- [ ] #23903 — stateful precompiles — own gas in both EIP-8037 dimensions, state access, own revert classification. Stacked on #23908
- [ ] **TBD** — EVM re-entry out of a precompile, carrying the EIP-8037 reservoir handoff. Cut from #23903 and held on `awskii/l2-precompile-reentry`: it charges none of the opcodes' caller-side gas phase, and unpriced account creation under-counts the block state-gas pool. Blocked on a first consumer, which is also what makes the pricing decidable
- [ ] #21976 — `ExtraGenesisAlloc`, `InProcServer`, `datadir.ForChain`, `RegisterPrecompile`
## Enforcement — the gate
Seven integrations have now improvised their own way in, the most recent of them six weeks after the seams merged. That is not seven teams each choosing badly; it is what happens when no path is proven. So this section gates the rest of the epic — a seam with no consumer is not unfinished work, it is the condition that produces the next improvisation.
**The consumer is Gnosis/Aura, not an external chain.** Earlier drafts named cocoon and nitro-erigon; neither is reachable. `cocoon-pocs` has zero erigon integration — no file in it imports `github.com/erigontech/erigon` — and last moved 2026-04-10; #22219 was itself a core commit adding `chain.CocoonRules`, closed unmerged 2026-08-10 and stacked on #22200, still CHANGES_REQUESTED, and #22217, since merged. The `arbitrum` branch is ~3,800 commits behind. Gnosis ships today and is already the shape the seams were built for: `execution/vm/evmtypes/rules.go:49` sets `IsAura: c.Aura != nil` on canonical `Rules`, three lines above the `c.L2.ResolveRules(...)` call at `:52-54` that exists to let a chain set its own flags.
Migrating Aura behind the opaque slot proves `RegisterChainSpec`, `RegisterL2Engine` and the fork oracle against a chain with real users, and it is the same work that makes the CI gate below reachable.
- [ ] **TBD — 1. Fix the rpcdaemon registry fall-through.** Prerequisite for anything being provable end-to-end; see the first item under "Rows 6/6". Closes standalone, adds no seam.
- [ ] **TBD — 2. Land `ForkPoints()` on `L2Config`.** #23602. No longer optional: it is what forces BSC's 25 fork fields into core.
- [ ] **TBD — 3. Migrate Aura behind the opaque slot.** Honest sizing: removing the named `Aura *AuRaConfig` deletes `rules.go:49`, so every direct `chainConfig.Aura != nil` producer moves to `Rules().IsAura`. The state-semantics consumers already route through `Rules`, but seven producers do not — `execution/builder/exec.go:244`, `execution/chain/chain_config.go:319,523`, `committer.go:769`, `exec3_parallel.go:3133,3386`, `blockgen/chain_makers.go:577` — and beyond them sit `p2p/forkid/forkid.go:231`, the consensus-DB open path, the genesis constructor, and `db/snaptype2`'s `RegimeFor(chainConfig)`, which returns `chainConfig.Aura == nil` to pick the segment grid (#23525). A multi-file first iteration, not a one-file one.
- [ ] **TBD** — CI job proving core builds and passes with **no chain module present**. It cannot pass before item 3: `execution/chain/spec/config.go:42-43` registers Gnosis and Chiado at package init, and AuRa is constructed directly in `node/rulesconfig/config.go:128` — in the same function whose first branch, at `:85-94`, is #22196's registry lookup with an early return. The named-field switch below it is the fallback nobody left. Also `cmd/rpcdaemon/cli/config.go:549` (gated at `:544`). (`node/eth/backend.go` does not import aura; it only selects a config off a core struct field at `:620-621`.)
- [ ] **TBD** — lint rule forbidding core packages from importing a chain package. It has one subject today: `node/rulesconfig/config.go` imports `bsc/parlia`. Item 3 adds the second.
## Rows 6/6 — the required surface
- [ ] **TBD — defect in a landed seam, not a cleanup.** `remoteRulesEngine.init` (`cmd/rpcdaemon/cli/config.go:999-1031`) and the `WithDatadir` path (`:543-557`) never consult #22196's registry and fall through to `ethash.NewFaker()` (`:1022`, `:557`). A chain that registers the sanctioned way therefore gets its engine in-node and a faker in standalone rpcdaemon — L2 calls and traces execute with L1 semantics. Recorded by yperbasis on #22200 (2026-07-29); reproduced live on `bsc_support`, which patches one switch and not the other. Collapsing the duplication is the fix, but the item is a correctness bug and belongs ahead of new seams. Polygon, Gnosis and BSC each re-implemented `case cc.X != nil` in five places: `node/rulesconfig/config.go`, `cmd/integration/commands/stages.go:1375`, `cmd/rpcdaemon/cli/config.go:544` and `:1008`, `execution/chain/chain_config.go:319`. `// TODO(yperbasis): try to unify with CreateRulesEngine` has been sitting at `cmd/rpcdaemon/cli/config.go:1005` through three chain integrations.
- [ ] **TBD** — make the opaque `L2`/`L2Config` slot the only way to add chain config, and remove the named fields. `Aura *AuRaConfig` at `execution/chain/chain_config.go:128` was the last one in core until `bsc_support` added `Parlia *ParliaConfig` beside it.
- [ ] **TBD** — pluggable block ingestion. `polygon/sync` was the precedent — zero staged-sync stages added, execution driven through an `ExecutionClient` wrapper — and #23497 deleted it. `bsc/PROGRESS.md` records the consequence directly: "`polygon/sync` is gone, so the reference is now `execution/p2p/bbd.go`". The item is to name and document the supported way before the next chain re-derives it.
- [ ] **TBD** — chain-name and `RulesName` registration. Both are closed core enums. `bsc_support` had to add `ParliaRules` to `ValidRulesNames` in `execution/chain/rules.go`; Optimism avoided this only by skipping `RegisterChainSpec` entirely and fetching genesis from `superchain-registry` at runtime (`params/op_superchain.go` on `origin/op-erigon`; zero `RegisterChainSpec` calls on that branch).
## Rows 5/6 and 4/6
- [ ] **TBD** — fold `rules.Engine.APIs()` into `jsonrpc.APIList`, currently a closed 14-case namespace switch. Cheap, and it retires a seam that has failed five times.
- [ ] **TBD** — p2p protocol and handshake registration. `bsc_support` adds `UpgradeStatusMsg`/`StatusPacketBsc70` to `p2p/protocols/eth/protocol.go`, gates an extra post-Status exchange on `isBscNetwork(networkID)` inside the generic `handShake[T]`, and adds an arm at the single dispatch site in `p2p/sentry/sentry_grpc_server.go`. A chain cannot add a wire message, a protocol version, or a handshake round without editing shared p2p code.
- [ ] **TBD** — execution-optional pipeline mode. #23573 sets `dbg.StagesOnlyBlocks` from `chainConfig.Parlia != nil` in `node/eth/backend.go` and special-cases `execution/stagedsync/stage_finish.go` to advance off `stages.Senders`, or forkchoice stalls. Distinct from pluggable ingestion: that item covers who inserts blocks, this one covers running with no execution engine at all.
- [ ] **TBD** — reconcile three colliding precompile-registration designs before any of them merges: #21976's `RegisterPrecompile(chainID, addr, p)`, #23908's `RegisterPrecompiles(chainID, f)`, and `feat/caplin-l2-integration`'s per-`Config` `Precompiles *ChainPrecompiles`. The third exists because neither global chainID-keyed registry supports L1 and L2 in one process.
- [ ] **TBD** — CL/Caplin pluggability. `cl/consensus.Engine` (#22025) and multi-instance Caplin (#22017) let an L2 based-rollup run alongside L1 in one process; `feat/caplin-l2-integration` already builds on both. The epic's item list is EL-only.
- [ ] **TBD** — continue the component extraction in `node/nodebuilder/builder.go`, whose comment at `:45` reads "Fields are added here as components graduate from `backend.go`". Downloader and Storage have graduated; Sentry has not — `node/eth/backend.go` holds `sentryProvider` as a bare field (`:171`, `:484`), and open #20257 repeats the bypass for TxPool. Fix Sentry's bypass rather than adding a fourth ad-hoc field. Registration lets an external repo add behaviour, not replace a component, and `New()` still fixes construction across 765 of `backend.go`'s 1,629 lines. reth's shape is the target — four associated types on `NodeTypes`, five swappable builders on `ComponentsBuilder` — reached incrementally.
- [ ] **TBD** — opaque "extra services" context for node-assembly constructors, replacing positional chain-typed parameters. The Bor-only params on `CreateRulesEngine`, `NewKvServer`, `NewEthBackendServer` and `StartGrpc` were removed by #23492/#23495, so core has no current exhibit; the live one is #23573 threading `dbg.StagesOnlyBlocks` off `chainConfig.Parlia != nil`. Re-anchor on that or drop the item.
- [ ] **TBD** — CLI flag registration. Five of six added flags, and `bsc_support` adds per-chain protocol-version, discovery and static-peer defaults in `cmd/utils/flags.go`.
- [ ] RPC extension points — tx/receipt/header extras providers, gas-cap adjustment, RPC-only call interception
- [ ] state-semantics seam — empty-account retention, chain-owned journal entries, per-chain state extras
- [ ] multi-dimensional gas extension
- [ ] alternate-VM dispatch at interpreter entry + guest host API + compiled-artifact store
- [ ] genesis at non-zero block number
## The reframe is not finished at the disk format
- [ ] **TBD** — the extension surface is still named for L2: `L2`, `L2JSON`, `IsL2`, `L2Version`, `RegisterL2Engine`, and the JSON key `"l2"` at `execution/chain/chain_config.go:134`. That key is **persisted** — `db/rawdb/accessors_metadata.go:74-79` round-trips it into the chain config table — so this is an on-disk contract, not just an API name. BSC sharpens it: Parlia is an L1 PoSA engine, and the slot it declined to use is called `L2`. Either accept the naming permanently and say so, or plan the rename with its datadir migration. Renaming the Go symbols alone would leave the two out of step.
## Storage registries
Split candidate — coherent unit, one owner. Instantiating a chain-owned store needs two core edits plus a core-approved number range, which blocks the salvage item below.
Scope note: a database label is not only a table layout. `TablesCfgByLabel` selects the schema, but the same label drives MDBX metrics, readahead, dirty-page budgets and logging (`db/kv/mdbx/kv_mdbx.go:97-122,229-234,283-311,347-351`), plus node paths and migration behaviour. Opening these registries reaches runtime policy and persisted formats, so price them accordingly.
- [ ] **TBD** — KV table name registry, replacing the flat consts in `db/kv/tables.go`
- [ ] **TBD** — database label registry, replacing `db/kv/dbcfg/db_constants.go` and the `TablesCfgByLabel` switch
- [ ] **TBD** — dynamic snaptype enum allocation, replacing reserved per-family ranges
- [ ] **TBD** — per-snaptype-family retirement and merge strategy. The exhibit was Bor's `FrozenBorBlocks(align bool)` reaching the engine-facing `ChainHeaderReader` and forcing stubs into seven unrelated files; both it and Bor are gone from the tree, so the item now stands on design merit and needs a live case before it is scheduled.
- [ ] **TBD** — datadir subdirectory registration. `db/datadir/dirs.go`'s `Dirs` is a fixed set of typed fields, populated in `New()`/`Open()` and enumerated again in `All()`/`VersionedDirs()`; adding one means editing that core file at four sites. #21976's `ForChain(parentDataDir, chainID)` roots a whole `Dirs` under `/chains/` for running several chains in one process — a different problem, and no way for a chain to register a named subdirectory. Bor's `heimdall/` and `polygon-bridge/` were never part of `Dirs`; they survive only as string literals in the legacy-cleanup path at `:454`.
- [ ] **TBD** — hook for "this system call should also produce a visible synthetic receipt and transaction". `Finalize` plus `rules.SystemCall` already let an engine call a system contract cleanly; nothing makes the result visible. Bor's workaround was nine files with their own `chainConfig.Bor != nil` branch, including `debug_api.go` overloading `blockNum == 0` as a sentinel — removed 2026-08-18, so this is a historical exhibit too.
## Salvage from the Polygon removal
- [ ] **TBD** — promote the generic entity store from `polygon/heimdall`: `Entity`, `EntityStore[T]`, `RangeIndex`, `EntityFetcher[T]`, `Scraper[T]`, and the MDBX/snapshot decorator where `LastFrozenEntityId()` reads the frozen/live cutoff from the newest segment index rather than tracking it separately. Read at `502f1ab94339^`.
Two limits to design around: bridge events did not fit it (they need time-window and tx-hash lookups `Entity`/`ClosedRange` cannot express), and `RangeIndex` assumes monotonic non-overlapping ranges, which broke inside heimdall itself when spans stopped being monotonic after Rio. Blocked on the storage registries.
Precedent: `polygon/p2p` was promoted into `execution/p2p` by #17111 and #17194 before the delete, and is still in core at 3,575 non-test lines serving the engine block downloader. What was extracted in time survived.
## Documentation
- [ ] **TBD** — runnable examples under `docs/examples/`: a minimal custom chain, a custom engine, a custom RPC namespace. reth's SDK book carries no trait signatures and their crates are not on crates.io; what carries their ecosystem is 29 runnable examples. `docs/examples/` holds `k8s` and `single-process.md` — zero runnable SDK examples.
## Removed from this epic
- ~~converge Bor onto the same primitives~~ — moot, Polygon removed by #23495/#23497 (tracked in #23503).
- ~~route receipt encode/decode through the tx-type registry~~ — done in #23565.
## Reference consumers
BSC is now the closest thing to one, and it is instructive precisely because it did not use the seams. nitro-erigon (Arbitrum) and cocoon (#21976, #22219) remain the named targets. All three are written by people with commit access to core, which is the bias this epic has to design against — `bsc_support` is a branch of this repository, not a separate one. The success test is a chain team opening a pull request against their own repository instead of this one.
Contributor guide
Assessment
This issue has not been assessed yet.