cmd, db/integrity: drop Polygon wiring from the standalone tools - #23492
Conversation
…ocs and QA workflow
|
Reviewed Crash: nil bor reader meets live
|
|
All five sections verified against Crash: nil bor reader meets live
|
…methods (erigontech#23494) The Bor snapshot type, its retire/merge path and the two Bor-named methods on the generic block-reader interfaces existed only to carry Heimdall data. With nothing constructing them any more, this removes them, which takes `db/` off the list of packages importing `polygon/`. Fifth in the Polygon removal series, branched off erigontech#23492 — review the last commit only. ## Changes - delete `db/snapshotsync/freezeblocks/bor_snapshots.go` - `BlockRetire` loses its Heimdall and bridge stores, `BorStore()`, `borSnapshots()`, the Bor prune metric and the Bor-data-not-ready backoff; `NewBlockRetire` drops two parameters - `BlockReader` loses its `borSn` field, `BorSnapshots()` and `FrozenBorBlocks()`; `NewBlockReader` takes one snapshot set instead of two - remove `FrozenBorBlocks` and `BorSnapshots` from `dbservices.FullBlockReader`, `FrozenBorBlocks` from the engine-facing `rules.ChainHeaderReader`, and `BorSnapshots` from `snapshotsync`'s reader interface, along with all seven implementations - `remotedbserver.NewKvServer` drops its Bor snapshot parameter - `db/rawdb`: `ReadChainConfig` now rejects a stored `bor` section instead of decoding it - `execution/stagedsync`: drop the Bor branches from the snapshots stage, including the synchronous-indexing exception - `db/datadir`: keep removing the legacy `heimdall` and `polygon-bridge` directories, by literal name now that the DB labels are on their way out ## Notes `ReadChainConfig` used to rehydrate `Config.Bor` from the stored `bor` JSON. Dropping that silently would let an existing Polygon chaindata load with `Bor == nil` and execute under the wrong consensus rules, so it now fails with a pointer to `0xPolygon/erigon`. This is the second of the three rehydration sites; `txnprovider/txpool` is the last and goes with `chain.Config.Bor`. The `heimdall` and `polygon-bridge` datadir cleanup is deliberately kept: anyone upgrading from a Polygon datadir still wants those directories removed. The 15 `kv.Bor*` tables, the `HeimdallDB` / `PolygonBridgeDB` labels and `snaptype.MinBorEnum` are **not** touched here. `polygon/heimdall` and `polygon/bridge` are their only readers and `polygon/heimdall` pins the snapshot enum range, so they can only go in the same PR that deletes the tree. Removing them earlier would just mean editing code that is about to disappear. The `WitnessProcessing` stage stays for the same reason: it reads `kv.BorWitnesses`. --------- Co-authored-by: Alexey Sharov <askalexsharov@gmail.com>
Resolves the fallout of the Polygon removal series (#23492..#23497) on this branch: the Bor-specific gating added in earlier review rounds goes away with the code it gated. borReceiptForBlock, borStateSyncLogs and txnLookupWithBorFallback are gone with the bridge, GetTransactionReceipt is back on the plain txnLookup, the Bor clamp in Capabilities is dropped with chain.Config.Bor, and the Bor gating tests go with their production paths.
) Polygon has not been officially supported since 3.1, but the 3.6 docs still offer it as a choice. This scopes it without removing the reference material, because the bor code does still ship in this branch — the `bor-mainnet` and `amoy` tags are selectable, the `bor` namespace is servable, and all seven `--bor.*` / `--polygon.*` flags are registered in `erigon --help`. **Presented as a choice, now not:** - Polygon sat in the Mainnets table beside Ethereum and Gnosis, with its caveat a footnote 25 lines below. It moves to its own "Polygon (not supported)" section, which absorbs the Amoy tag. - Three cards advertised a one-command Polygon easy-node setup. That guide was removed from this branch and `/get-started/easy-nodes/how-to-run-a-polygon-node` is now a redirect, so the cards promised something that no longer exists. - The FAQ answered "supports networks such as Gnosis and Polygon". That string is in `faqSchema`, so it ships as JSON-LD. **Kept, and scoped instead:** the `bor_` method reference, the Polygon gRPC services, and the seven flags — all accurate for 3.6. The flags move under a "Legacy Polygon flags" heading so the CLI reference stays complete against `erigon --help`, and the bor namespace entry and the gRPC section point at Supported Networks. **Two fixes along the way:** the help center cited `--bor.heimdall.url`, which is not a flag (the real one is `--bor.heimdall`); that example now uses `--externalcl`. The Layer 2 page description advertised "Polygon PoS, Bor" on a page whose body is entirely about running an op-node. `main` already dropped all of this along with the code (#23492, #23497), so nothing here forward-ports. `llms.txt` / `llms-full.txt` regenerated; `--check` and `npm run build` pass. --------- Co-authored-by: Bloxster <gianni.morselli@erigon.tech>
…23322) Defects in the same family — how receipt- and log-serving endpoints decide availability. > **Note:** while this PR was under review, main removed Polygon support entirely (#23492..#23497). The Bor-specific fixes made in earlier review rounds are therefore no longer part of this PR — the history gate inside `borReceiptForBlock` and `borStateSyncLogs`, the `txnLookupWithBorFallback` resolution in `eth_getTransactionReceipt`, the Bor clamp on the receipts capability, and the four Bor gating tests were retired by the merge (34f460e) together with the code they gated. Non-Bor behaviour is untouched: those helpers were wrappers, and their callers now use the generic functions they wrapped (`getReceipts`, `txnLookup`). **1. The receipts gate consulted the wrong setting.** It read `kvcfg.PersistReceipts`, a boolean saying the receipt cache exists on disk, and concluded receipts were available for every block. Retention is a separate setting: `RCacheDomain` is retired on its own `--prune.receipts.distance` window when one is set, and alongside state history otherwise, which is the default. It now consults `prune.Mode.ReceiptsAmount()`, distinguishing the same three shapes `historyRetireCutoffs` already distinguishes. Where the cache stops covering a block, availability falls back to state history, because a missing cache entry is not fatal: `ReadReceiptCacheV2` reports it as absent and `GetReceipt` re-executes the block from `ReceiptDomain` plus state history, retired at the history cutoff rather than the RCacheDomain one. So a `--prune.receipts.distance` narrower than the history window costs the caller nothing. **2. Four endpoints were gated on receipts alone, though matching logs can need more.** `eth_getLogs` and `erigon_getLogs` search `LogAddrIdx` and `LogTopicIdx` when the query filters by address or topic; those are standalone inverted indices retired at the history cutoff whatever the receipt retention is, so gated on receipts they answered with an empty log array instead of `PrunedError`. They now take the history gate on top, but only when the filter actually reaches the indices: `usesLogIndex` mirrors `applyFiltersV3`, which skips topic positions that are empty because those match any topic — so `"topics": [null]` searches nothing and stays on the receipts path, exactly like the unfiltered form. `erigon_getLatestLogs` re-executes through a `TraceWorker` and `overlay_getLogs` re-executes with overridden code, so both take the history gate unconditionally. **3. Ten endpoints serving the receipts of one block were gated on history alone**, which both rejects blocks whose body and receipts are present and never checks the body at all. New `checkBlockReceiptsAvailable` composes the two boundaries: reading a stored receipt needs the block body anyway, since the receipt carries no `TxHash` and it is derived from the transaction. The two GraphQL block-detail entry points had no gate; theirs is placed before the body read, because after it a pruned body reads back as nil and the endpoint answers "not found" before any gate could fire. The GraphQL and Otterscan block-detail paths also select one overlay view per request and thread it through resolution, gate, block and receipt reads, so a background commit cannot gate one generation while another answers. **4. The log path needs the block body for the same reason.** `getLogsV3` reads the transaction through `TxnByIdxInBlock` for every txNum it does not find in the in-memory cache, because `GetReceipt` derives the receipt from it. With bodies pruned and receipts kept — `minimal` with `--prune.receipts.distance=keep-all` — the lookup came back empty and the loop skipped the block silently, answering with an empty array. New `checkLogsAvailable` composes the three boundaries and replaces the duplicated gate block in the two `getLogs`, so the rationale is stated once. **5. A block with no transactions could not have its receipts read at all.** `GetReceipts` decided whether the cache had answered by testing `len(receiptsFromDB) > 0`, which cannot tell "the cache holds nothing for this block" from "this block has no transactions", so an empty block fell through to `PrepareEnv` and its state-history read. Where history is pruned, `eth_getBlockReceipts`, `debug_getRawReceipts`, `ots_getBlockDetails` and `ots_getBlockTransactions` answered with a leaked `ReceiptsGen: PrepareEnv: ... old data not available` instead of an empty list. The fix belongs in the generator rather than the gate: the gate decides availability from retention, and a block it lets through must not leak an execution error. Availability itself is a separate question, and the gate makes no exception for these blocks — a transaction-free block below the cutoff is refused like any other, and the caller gets `PrunedError` rather than an empty list that cannot be told apart from "no receipts". So this fix covers the blocks the gate serves: receipts retained, archive, or above the cutoff. The two boundaries are the whole gate, so `oldestBlock` in `eth_capabilities` is the exact boundary the endpoints honour rather than a floor with holes below it. **6. The blocks gate read the chain-history-expiry sentinel as "nothing is pruned".** `KeepPostMergeBlocksPruneMode` is a policy, not a window: on a chain declaring a merge point, pre-merge transaction segments are never downloaded, so `checkPruneBlocks` now refuses below `MergeHeight`, and `eth_capabilities` resolves its `blocks` boundary through the same function, so the two cannot diverge. A legacy archive datadir persists the same blocks sentinel, so the datadir decides rather than the stored retention — and neither a pre-merge body nor the oldest available block can: expiry keeps pre-merge headers and bodies, and the transaction segment spanning the merge point reaches below it. The gate reads a pre-merge transaction, sampled by halving the range so the candidates stay clear of the transaction segment spanning the merge point; where the chain carries no pre-merge transaction at all, the cumulative txnum position of the last pre-merge body says so and nothing is left to be missing. The verdict is availability rather than policy, so it is cached for a short TTL in both directions instead of being settled once, and availability widening while segments arrive reopens the gate within that window. The remote rpcdaemon's block reader answers `FrozenBlocks` through a new ethbackend RPC instead of panicking, so the receipt gates, `eth_capabilities` and the receipt generator keep consulting the real value everywhere. **7. `erigon_getLogsByHash` consulted its receipt cache before any gate.** A receipt set cached while its block was inside the retention window stayed servable after the boundary moved past it — holes below the advertised `oldestBlock`, the defect class this PR removes. The gate now runs before the cache, so a hit is gated like a miss, at the cost of one read-only tx on cache hits. This covers the audit asked for in point 2 of #22260 for `erigon_receipts.go` and `otterscan_block_details.go`: migrated to the correct boundary rather than documented as genuinely needing history. ## Coverage `prune_gating_test.go` pins **34 endpoints across the nine prune mode shapes** on an old and a recent block: 612 cells. `check_prune_gates_test.go` covers the gates directly — the boundary block itself, the boundary named in each error, the archive short circuit, each receipt retention shape including a window wider and a window narrower than history, each leg of both composed gates, and `usesLogIndex` over the criteria shapes. `checkTxFee` gains the unit test it never had, so every `check*` in the package now has one. It also pins the datadir shapes behind the blocks sentinel — aligned segments, bodies without transactions, a chain with no pre-merge user transaction, a sampled block without transactions, blocks arriving later, and a stored history retention that does not change the verdict. The remote block reader's frozen-block refresh is covered directly: a failed fetch is retried instead of being served as fresh, and a slow one delays only the goroutine that fetches. ## Verified on a live node Sepolia at tip with `--prune.mode=minimal --prune.include-receipts` — receipts on disk, block bodies pruned. Head 11500055, both boundaries at 11400054, old block probed 5750027. **35 checks, all passing.** | Family | Endpoints | Old block | Why | |---|---|---|---| | headers | `erigon_getHeaderByNumber`, `debug_getRawHeader` | served | headers are never pruned | | block data | `eth_getBlockByNumber`, `eth_getBlockTransactionCountByNumber`, `eth_getUncleCountByBlockNumber`, `debug_getRawBlock` | refused | bodies are pruned in `minimal` | | receipts of one block | `eth_getBlockReceipts`, `debug_getRawReceipts`, `erigon_getBlockReceiptsByBlockHash`, `erigon_getLogsByHash`, `ots_getBlockDetails`, `ots_getBlockTransactions` | refused, naming `blocks are available` | the blocks leg this PR adds — receipts are present, the body is not | | logs by filter | `eth_getLogs`, `erigon_getLogs` | refused, naming `history is available` | log indices follow history, not receipts | | state / trace | `eth_getBalance`, `trace_block`, `debug_traceBlockByNumber`, `ots_hasCode` | refused | history is pruned | This table was measured before the log gate took the blocks leg described in point 4. On the current revision its two log rows refuse on the blocks boundary rather than the history one, because `minimal` prunes bodies and that leg is checked first; the rows above and below are unaffected. The blocks leg is covered end to end by the unit tests, not by a live probe. Every recent-block probe is served. The two middle rows name **different boundaries** for the same block, which is what this configuration shows: before the change both said `history is available`. Note that in `minimal` the receipt endpoints refuse either way, so only the named boundary changes. The behavioural flip — refused becoming served — happens where bodies are kept and receipts outlive history (`blocks` with `--prune.receipts.distance=keep-all`), which is the largest group of the 26 red cells above. ## Second live node: `blocks` with receipts kept Sepolia at tip with `--prune.mode=blocks --prune.include-receipts --prune.receipts.distance=keep-all` — every body and every receipt on disk, state history pruned below block 11238830. Old block probed: 5750484, some 5.5M blocks below the history boundary. | Family | Endpoints | Old block | Why | |---|---|---|---| | headers | `erigon_getHeaderByNumber`, `debug_getRawHeader` | served | never pruned | | block data | `eth_getBlockByNumber`, `eth_getBlockTransactionCountByNumber`, `eth_getUncleCountByBlockNumber`, `eth_getTransactionByHash` | served | bodies are kept | | **receipts of one block** | `eth_getBlockReceipts`, `debug_getRawReceipts`, `eth_getTransactionReceipt`, `erigon_getBlockReceiptsByBlockHash`, `erigon_getLogsByHash`, `ots_getBlockDetails`, `ots_getBlockTransactions` | **served** | body and receipts are both there — on `main` all seven refuse | | **logs, unfiltered** | `eth_getLogs`, `erigon_getLogs` | **served**, 125 logs | read straight from receipts; no index is consulted | | **logs, filtered by address** | `eth_getLogs`, `erigon_getLogs` | **refused**, naming `history is available` | the index search needs history — on `main` both return an empty array | | state / trace | `eth_getBalance`, `trace_block`, `debug_traceBlockByNumber`, `ots_hasCode` | refused | history is pruned | | block data, follow-up | `debug_getRawBlock`, `debug_getRawTransaction` | refused | still gated on history; see Follow-up | This is the configuration where the change is behavioural rather than a difference in wording. `eth_getBlockReceipts` returns 101 receipts for a block whose state history is long gone, `ots_getBlockDetails` computes its `totalFees`, and `erigon_getLogsByHash` returns all 101 log positions — none of which `main` will answer. In the other direction a filtered `eth_getLogs` now refuses instead of quietly returning `[]`, while the unfiltered form still answers with its 125 logs. 36 of 38 checks pass; the two failures are `debug_getRawBlock` and `debug_getRawTransaction`, whose fix is in the follow-up PR. ### Topic shapes on the same node The empty-topic-position case, probed on block 5750932 of the same datadir with an rpcdaemon opened read-only: | filter | before | after | |---|---|---| | no `topics` | 1117 logs | 1117 logs | | `"topics": []` | 1117 logs | 1117 logs | | `"topics": [null]` | `PrunedError` | 1117 logs | | `"topics": [[]]` | `PrunedError` | 1117 logs | | `"topics": [null, null]` | `PrunedError` | 1113 logs | The last row keeps 1113 rather than 1117 because that shape requires two topic positions to match, so it drops the four logs carrying a single topic; the filter is working, the gate is not involved. A query filtered by address still refuses naming `history is available`, and the single-block receipt endpoints stay served, so the blocks leg added in point 4 introduces no false refusal where bodies are kept. ## Gate boundaries measured against the data Each gate names a boundary; a second `rpcdaemon` built with `checkPruneField` short-circuited to `nil` says where the data actually starts. Both were opened read-only on the same Sepolia `blocks` datadir — head 11501094, declared boundary 11238950 — and the first block that answers correctly was found by bisection, with a block's own receipts as the ground truth for what its log endpoints must reproduce. | gate | endpoints | declared | first block that answers | verdict | |---|---|---|---|---| | `checkPruneHistory` | `eth_getBalance`, `eth_getCode` | 11238950 | 11163680 | conservative by 75270 | | `checkPruneHistory` | `trace_block`, `debug_traceBlockByNumber` | 11238950 | 11163681 | conservative by 75269 | | index leg of `checkLogsAvailable` | `eth_getLogs`, `erigon_getLogs` filtered | 11238950 | 11163681 | conservative by 75269 | | `checkBlockReceiptsAvailable` | `eth_getBlockReceipts`, `debug_getRawReceipts`, `ots_getBlockDetails`, `ots_getBlockTransactions` | never refuses | complete at every block with transactions from 800000 to the tip | exact | | receipts leg of `checkLogsAvailable` | `eth_getLogs`, `erigon_getLogs` unfiltered | never refuses | correct at every block with logs | exact | The 75k-block gap is not a defect: file retirement is floored to the file step, so data survives below the declared boundary until the next prune pass removes it. The gate promises the boundary the configuration guarantees rather than the physical one that moves at every retire, which is the pre-existing behaviour of `checkPruneField`. The index leg is where the change earns its place. Filtering the same block by an address taken from its own logs: | block | gate removed | gate in place | |---|---|---| | 5750932 | 0 of 1 expected log | refused | | 11163680 | 0 of 1 expected log | refused | | 11163679 | 0 of 1 expected log | refused | | 11238949 | 35 logs, correct | refused | | 11238950 | 116 logs, correct | 116 logs | Below the physical floor the filtered query answers **silently zero**, which is what the history leg turns into `PrunedError`. The physical floor of the log index, 11163681, coincides with that of state history, 11163680 — the premise this PR rests on, measured rather than assumed. The unfiltered form is correct at every block probed: 1117, 1016, 1360, 525, 443, 1237 and 216 logs, each matching its receipts, identical with and without the gate. Point 5 was found the same way: blocks 1000, 1000000, 1510087 and 2000000 go from the `PrepareEnv` error to an empty list, while blocks carrying transactions are unchanged — 800000 keeps 1 receipt, 1511431 keeps 4, 5750932 keeps 99, 11501058 keeps 94. The blocks leg is the one this datadir cannot exercise, since it keeps every body; it stays covered by the unit tests. ## Follow-up The endpoints that need the block body but not the receipts are left to a second PR: `debug_getRawBlock`, `debug_getRawTransaction` and `erigon_getBlockByTimestamp` still gate on history, so under `--prune.mode=blocks` they refuse bodies that were never pruned — the defect class #21965 reported. That PR also gates `eth_feeHistory`, which has none today, and completes the table with the header, trace and Otterscan search endpoints.
The standalone binaries were still building Bor readers, a read-only Bor engine and Bor snapshot stores that nothing consumes now that the
borRPC namespace and the node-side services are gone. This removes that, which leavescmd/andnode/free of anypolygon/import. Fourth in the Polygon removal series, branched off #23491 — review the last commit only.Changes
cmd/rpcdaemon/cli: drop theBridgeReaderandHeimdallReaderinterfaces, the Bor snapshot and bridge/Heimdall store setup, thebor.NewRoarms in both the local and remote engine paths, and the two Bor version-compatibility checks;RemoteServicesreturns 9 values instead of 11cmd/utils/app: drop the Bor snapshot open/close insnapshotsandseg, the Bor stores inopenSnaps, theFrozenBorBlocksretire clamp andCheckBorChaincmd/integration: drop the Bor snapshot and store singletons, the--bor.heimdallflag, the Heimdall/polygon-bridge DB migration paths and their migratable labels, and the bor line inprint_stagesdb/integrity: remove theBorEvents,BorSpansandBorCheckpointschecks_ "polygon/heimdall"blank imports fromcmd/downloader,cmd/integrationandcmd/rpcdaemonerigon initnow rejects a genesis carrying aborconfig instead of decoding itNotes
erigon initused to rehydrateGenesis.Config.Borfrom theborJSON key. Simply dropping that would have let a Polygon genesis initialise withBor == niland run under the wrong consensus rules, so it fails with a pointer to0xPolygon/erigoninstead. The two remaining rehydration sites, indb/rawdbandtxnprovider/txpool, get the same treatment whenchain.Config.Boritself is removed.cmd/rpcdaemon/rpcservicesstill implementsBorSnapshots()andFrozenBorBlocks()because they are still on theFullBlockReaderinterface. That interface, the Bor snapshot machinery indb/snapshotsyncand the 15kv.Bor*tables are the next PR.