feat: move blobs from consensus txs to p2p so they can be pruned properly - #3149
feat: move blobs from consensus txs to p2p so they can be pruned properly#3149fridrik01 wants to merge 1 commit into
Conversation
5d493e7 to
eb0acfc
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3149 +/- ##
==========================================
+ Coverage 63.11% 64.26% +1.15%
==========================================
Files 372 385 +13
Lines 15549 16965 +1416
==========================================
+ Hits 9814 10903 +1089
- Misses 4775 4993 +218
- Partials 960 1069 +109
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR moves blob sidecar distribution out of CometBFT blocks and onto a dedicated P2P reactor channel (with EL reconstruction + peer fetch fallback), so blob data lives only in the availability store and can be pruned correctly after the DA window. The change is fork-gated via a new blob-consensus.enable-height chain-spec parameter (enabled on devnet only), and it updates sync reporting + APIs so “synced” continues to imply “has all in-window blobs”.
Changes:
- Introduces a CometBFT Reactor-based blob sidecar gossip/request protocol (
da/blobreactor) and wires it into Comet service startup and proposal flow. - Adds tiered sidecar retrieval (push cache → local EL blob pool via
engine_getBlobsV2→ peer by-root fetch) plus a crash-safe background fetcher/queue for sync/catch-up. - Updates availability storage semantics (slot replacement + commitment||index keys) and sync/API behavior to reflect pending in-window blob fetches.
Reviewed changes
Copilot reviewed 78 out of 78 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| testing/simulated/utils.go | Makes simulated chain advancement tolerant to 1-tx vs 2-tx proposal layouts across the enable-height boundary. |
| testing/simulated/simcomet.go | Injects blob reactor into simulated Comet service and exposes pending-blob-fetch status. |
| testing/simulated/components.go | Adds DI providers for blob reactor/reconstructor/fetcher and a blob-consensus-enabled sim chain spec. |
| testing/simulated/blob_fetcher_integration_test.go | New integration test for background blob fetch lifecycle over real P2P switches. |
| testing/simulated/blob_consensus_test.go | New simulated suite validating tx layout transition across enable height through ABCI flow. |
| testing/networks/80094/spec.toml | Adds [blob-consensus] section (disabled) for mainnet spec file. |
| testing/networks/80069/spec.toml | Adds [blob-consensus] section (disabled) for testnet spec file. |
| testing/files/spec.toml | Adds [blob-consensus] section (enabled at 2) for local testing spec. |
| storage/filedb/range_db.go | Rejects writes below pruning lower bound to prevent resurrecting/clobbering pruned indices. |
| storage/filedb/range_db_test.go | Adds coverage for rejecting stale writes below the pruning lower bound. |
| node-core/types/node.go | Extends ConsensusService interface with HasPendingBlobFetches. |
| node-core/types/mocks/consensus_service.mock.go | Regenerates consensus service mock with HasPendingBlobFetches. |
| node-core/components/cometbft_service.go | Wires blob reactor dependency into CometBFT service provider. |
| node-core/components/chain_service.go | Wires blob reactor/reconstructor/fetcher into blockchain service provider. |
| node-core/components/blob_reactor.go | New DI providers for blob reactor, EL reconstructor, and background blob fetcher. |
| node-api/handlers/node/node.go | Marks node as syncing if blob fetches are pending (in addition to sync distance). |
| node-api/handlers/node/node_test.go | Updates node syncing handler tests for HasPendingBlobFetches. |
| node-api/handlers/node/mocks/backend.mock.go | Regenerates node handler backend mock with HasPendingBlobFetches. |
| node-api/handlers/node/backend.go | Extends node handler backend interface with HasPendingBlobFetches. |
| node-api/handlers/beacon/blobs.go | DA-window validation relative to head slot and “blobs pending” not-found behavior. |
| node-api/backend/getters.go | Delegates HasPendingBlobFetches from API backend to node backend. |
| kurtosis/src/services/spamoor/launcher.star | Adds startup delay before blob spam command to allow RPC readiness. |
| go.mod | Adds direct dependencies needed by new code paths (gogoproto, x/time). |
| execution/client/metrics.go | Adds metric timing for EL blob retrieval calls. |
| execution/client/ethclient/engine.go | Implements engine_getBlobsV2 JSON-RPC call wrapper. |
| execution/client/ethclient/constants.go | Advertises engine_getBlobsV2 capability constant. |
| execution/client/engine.go | Exposes EngineClient.GetBlobsV2 with capability gating and metrics/timeout handling. |
| engine-primitives/errors/errors.go | Adds ErrGetBlobsUnsupported for missing EL blob API support. |
| da/store/store.go | Updates blob store keying to include blob index and makes Persist slot-idempotent by clearing slot first. |
| da/store/store_test.go | Adds tests for pruning/write race rejection, duplicate commitments, and Persist idempotency. |
| da/kzg/proof.go | Adds BlobProofProver interface to compute canonical blob proofs for sidecar reconstruction. |
| da/kzg/noop/noop.go | Implements ComputeBlobProof for the noop verifier. |
| da/kzg/gokzg/gokzg.go | Implements ComputeBlobProof using go-kzg context. |
| da/blobreactor/serve.go | New request serving logic for by-root and by-range sidecar requests. |
| da/blobreactor/requests.go | New request/response tracking, peer selection/scoring hooks, and fetch APIs. |
| da/blobreactor/push.go | New push-cache + gossip logic (push/have) with bounded caching and verification. |
| da/blobreactor/metrics.go | New metrics helpers and error-status classification for reactor operations. |
| da/blobreactor/messages.go | New SSZ message types and envelope for Comet reactor channel transport. |
| da/blobreactor/messages_test.go | Tests SSZ round-trips and validation for reactor message types. |
| da/blobreactor/interfaces.go | Defines minimal interfaces for blob store, verifier, and telemetry sink integration. |
| da/blobreactor/export_test.go | Exposes internal helpers/hooks to facilitate deterministic reactor tests. |
| da/blobreactor/errors.go | Defines reactor-level errors (no peers, all peers failed, slot not requested). |
| da/blobreactor/config.go | Adds reactor config for per-peer request timeout and overall fetch deadline. |
| da/blob/reconstructor.go | New EL-based sidecar reconstructor (engine_getBlobsV2 + local proof recomputation). |
| da/blob/reconstructor_test.go | Tests canonical sidecar reconstruction and rejects malformed EL responses. |
| consensus/cometbft/service/service.go | Registers blob reactor via Comet custom reactors and wires chainSpec into service struct. |
| consensus/cometbft/service/process_proposal.go | Caches verified sidecars alongside cached state for FinalizeBlock path. |
| consensus/cometbft/service/prepare_proposal.go | Switches proposal tx layout at enable height and broadcasts sidecars via reactor. |
| consensus/cometbft/service/node_api_support.go | Implements HasPendingBlobFetches via blockchain pending queue depth. |
| consensus/cometbft/service/interfaces.go | Adds BlobReactorI interface used by Comet service. |
| consensus/cometbft/service/finalize_block.go | Refactors cached-finalization path and sources sidecars from cache post-transition. |
| consensus/cometbft/service/encoding/interfaces.go | Extends ABCIRequest interface with GetHeight() for height-aware decoding. |
| consensus/cometbft/service/cache/cache.go | Extends cached-state element to include verified sidecars. |
| consensus/cometbft/service/blobconsensus/config.go | New chain-spec config surface for blob consensus activation gating. |
| config/template/template.go | Adds [beacon-kit.blob-reactor] config template entries. |
| config/spec/testnet.go | Ensures blob-consensus defaults are explicitly set for Bepolia spec data. |
| config/spec/mainnet.go | Ensures blob-consensus defaults are explicitly set for mainnet spec data. |
| config/spec/devnet.go | Enables blob-consensus transition early on devnet spec data. |
| config/spec/creator_test.go | Extends spec-creator validation tests to require blob-consensus.enable-height. |
| config/config.go | Adds blob reactor config to node config defaults and schema. |
| cmd/beacond/defaults.go | Includes blob reactor/reconstructor/fetcher providers in default component set. |
| chain/spec.go | Adds blob-consensus getters and IsBlobConsensusEnabled to chain spec interface/impl. |
| chain/data.go | Adds blob-consensus section to chain SpecData (mapstructure key blob-consensus). |
| beacon/blockchain/service.go | Adds blob distribution dependencies, starts/stops fetcher, and adjusts orphan pruning behavior. |
| beacon/blockchain/process_proposal.go | Returns verified sidecars from ProcessProposal and fetches them when consensus tx no longer carries them. |
| beacon/blockchain/payload_test.go | Updates tests to satisfy new blockchain service constructor dependencies. |
| beacon/blockchain/interfaces.go | Extends interfaces for new blob requester/reconstructor/fetcher and updated ProcessProposal/FinalizeSidecars signatures. |
| beacon/blockchain/finalize_sidecars_test.go | New unit tests for FinalizeSidecars decision tree and pruning behavior across transition. |
| beacon/blockchain/finalize_block.go | Updates FinalizeBlock to pass signed block into sidecar finalization and wires head slot into fetcher. |
| beacon/blockchain/common.go | Height-aware decoding: legacy 2-tx vs blob-consensus 1-tx proposal layouts. |
| beacon/blockchain/common_test.go | New tests validating ParseBeaconBlock behavior across enable height. |
| beacon/blockchain/blob_retrieval.go | New tiered tip-of-chain sidecar retrieval and binding verification helpers. |
| beacon/blockchain/blob_queue.go | New crash-safe on-disk retry queue for background blob fetch requests. |
| beacon/blockchain/blob_queue_test.go | Tests for queue correctness, durability, expiry, corruption handling, and metachar paths. |
| beacon/blockchain/blob_fetcher.go | New background fetcher draining queue via by-range requests with strict verification and no retry cap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
eb0acfc to
62deecd
Compare
62deecd to
be843f8
Compare
issue: #2665
replaces: #2938
Context
Blobs are carried today as a CometBFT transaction, so every block permanently stores its blob sidecars in CometBFT's block store even though sidecars only need to live for the Data Availability (DA) window. The result is unprunable disk growth, up to ~768 KiB of blob data per block (6 blobs), which at a 2-second block time is roughly 30 GiB per day at full blob usage, plus every syncing node re-downloading historical sidecars it will immediately discard.
An earlier attempt (#2938) solved this by teaching CometBFT itself about blobs, but that required maintaining a large fork of CometBFT's consensus core that we did not write, is fairly complex and that is now well behind upstream.
What this PR does
Blobs never enter CometBFT. A proposal carries a single transaction, the signed beacon block, and blob sidecars travel on their own p2p channel, handled by a new blob reactor registered through CometBFT's public reactor API.
This is Ethereum's own model. Blocks on one channel, blob sidecars on another, each pruned independently. It works safely because the block body already commits to its blobs (KZG commitments) and sidecars carry inclusion proofs against the block header, so blobs are self-verifying no matter which peer delivers them and peers never need to be trusted.
With the reactor as the delivery path, BeaconKit's availability store becomes the only copy of blob data, and the existing DA-window pruning genuinely reclaims disk.
How blobs reach nodes
At the tip. The proposer pushes sidecars to its peers at the same moment it hands the block to CometBFT. Nodes that verify them forward them once, and cheap "have" announcements keep duplicate traffic low, so the sidecars flood the network alongside the proposal. A validator that got the proposal but not the blobs falls back, in order, to its own execution client (
engine_getBlobsV2, usually a local hit since blob transactions gossip through the EL mempool with their blobs attached) and then to a targeted request to peers. Everything retrieved is fully verified against the block before the node votes. No blobs in time means rejecting the proposal, the same posture as any invalid proposal, so finality still implies that 2/3+ of voting power held and verified the data.While syncing. Blocks arriving through block sync no longer contain blobs. A background fetcher requests missing in-window sidecars from peers in bulk, verifies them, and persists them, backed by a crash-safe on-disk retry queue. Two rules are strict by design. A response with fewer sidecars than the block commits to is a failure, never an "empty success", and requests are retried until the slot leaves the DA window, never dropped after a retry cap. A node does not report itself as synced while in-window fetches are pending, so "synced within the DA window" keeps implying "holds the blobs".
Every node answers blob requests from its availability store, bounded by per-peer rate limits, response size caps, and peer scoring.
Activation
The transition is gated by a new
blob-consensus.enable-heightchain-spec parameter. Below the height blobs ride as the second consensus transaction exactly as today. At and above it, proposals carry one transaction and the reactor is authoritative. Decoding honors each layout at its historical heights, so replay from genesis works across the boundary.This PR enables the feature on devnet only. Mainnet and Bepolia specs keep it disabled (height 0), so behavior on public networks is unchanged until an activation height is scheduled.
Trade-off vs #2938
CometBFT no longer enforces blob delivery before prevote, so there is a small liveness risk if a validator cannot fetch a block's blobs within a round. In practice blocks without blobs are unaffected, and a validator that misses the push almost always finds the blobs in its own execution client. In exchange we drop the unmaintained consensus fork entirely, and we fix two sync-path issues the original design had, a fixed retry cap that could silently drop a fetch and leave a permanent DA gap, and empty peer responses being counted as success.
Testing
testing/blobs/blob-sync-local-test.sh) that runs a local multi-node devnet, sends blob transactions, and repeatedly restarts random nodes, asserting each catches up and serves the same sidecars as a healthy reference node.Still left to deploy this to
devnet-core/devnet-cli.Operator notes
[blob-consensus]section withenable-height(0disables the feature), consistent with all other required spec fields.