feat(l1): switch EIP-8070 blob sampling on at Amsterdam - #7299
MegaRedHand wants to merge 7 commits into
Conversation
eth/72 was gated behind `--blob-sampling`, off by default, so a node never advertised the capability unless an operator opted in. That gate exists for a real reason: eth/72 always elides blob payloads from PooledTransactions, so blobs arrive only via GetCells, which only the sampler/provider state machine issues. Advertising the capability without running that loop would mean accepting blob txs the node can never reconstruct. Tie the gate to the fork the EIP rides on instead of to the flag alone. Sampling now turns itself on once the chain head reaches Amsterdam, which is the point at which the rest of the network is expected to speak eth/72 as well. `--blob-sampling` keeps working as an early-activation switch for devnets and for chains that schedule Amsterdam later than they want the state machine running; `--blob-eager-provider` still implies it. The latch is one-way, for the same reason the eager-provider latch is: a reorg back below the fork boundary would otherwise flap the advertised capability set and churn peer connections over a boundary the chain is about to cross again. It is resolved lazily on the p2p paths that ask the question (per connection, per announcement) rather than on block import, so the import path pays nothing and the steady-state cost after the fork is one atomic load. L2 stays out: it rejects blob txs outright, so its fork schedule must not drag the blobpool state machine in.
|
🤖 Kimi Code ReviewI'll review this PR which changes EIP-8070 blob sampling from a static flag to a fork-gated mechanism that activates at the Amsterdam hard fork. Let me analyze each area. Overall AssessmentThis is a well-structured PR that correctly transitions blob sampling from a manual operator flag to an automatic fork-gated mechanism. The core logic is sound, but I have several concerns around edge cases, atomic ordering, and potential race conditions. Detailed Findings1. Race Condition in
|
| Item | Severity | File | Line | Description |
|---|---|---|---|---|
| 1 | Medium | blockchain.rs |
4203-4215 | Race condition allows duplicate log; return value from enable_blob_sampling ignored |
| 2 | Low | mempool.rs |
731, 741, 766 | Atomic ordering mix is correct but could be cleaner; verify SeqCst not needed |
| 3 | Low | blockchain.rs |
4206-4208 | L2 check is defensive; verify no L2 has Amsterdam in config |
| 4 | Medium | store.rs |
1646-1649 | Verify latest_block_header.get() thread-safety |
| 5 | Low | blockchain.rs |
4183-4215 | Fork check on hot path; acceptable but document trade-off |
| 6 | Low | cli.rs |
527 | Help text slightly misleading about "before Amsterdam" semantics |
| 7 | — | — | — | Correct: blob_eager_provider implies force_blob_sampling |
| 8 | — | payload.rs |
444 | Correct change to use latch_eager_provider wrapper |
| 9 | Medium | — | — | Missing tests for fork transition behavior |
Recommended Changes
Priority 1 (must fix):
- Item 1: Change
enable_blob_samplingto returnbool, use it to avoid duplicate logging
Priority 2 (should fix):
- Item 4: Verify and document
latest_block_header.get()thread-safety - Item 9: Add tests for fork transition behavior
Priority 3 (nice to have):
- Item 6: Clarify CLI help text
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code Review
Other than that, the refactor looks mechanically sound. I did not run tests; this is a static review. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Lines of code reportTotal lines added: Detailed view |
🤖 Claude Code ReviewReview: PR 7299 — switch EIP-8070 blob sampling on at AmsterdamOverall design is sound. Gating eth/72 on the fork rather than solely on an operator flag closes a real gap (a default node could otherwise never speak eth/72). The one-way latch to avoid capability flapping on reorg, the lazy resolution on p2p paths so block import pays nothing, and routing all four p2p call sites through High-severity bug
The L2 exclusion is: if !matches!(self.options.r#type, BlockchainType::L1) {
return false;
}But This directly contradicts the PR's own stated invariant ("L2 is excluded explicitly... its fork schedule must not drag the blobpool state machine in") for exactly the L2 variant that shares Suggested fix: the exclusion needs a signal independent of Minor / styleCommand-query separation: Things that look correct
Automated review by Claude (Anthropic) · sonnet · custom prompt |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical activation and native-rollup L2 exclusion issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 4
Open (4)
What changed in this PR
This PR changes EIP-8070 blob sampling from CLI-only activation to lazy, latched Amsterdam-fork activation, while centralizing the decision in Blockchain.
Changes:
- Adds fork-aware activation and latest-head timestamp access.
- Routes P2P and payload checks through blockchain-level state.
- Renames the startup override to
force_blob_samplingand updates wiring. - Preserves explicit L2 exclusion.
Final findings include four critical comments covering native-rollup L2 handling, later-fork schedules, and forced sampling bypassing L2 exclusion, plus one nit requesting focused tests.
| File | Summary |
|---|---|
crates/storage/store.rs |
Adds latest-head timestamp access. |
crates/networking/p2p/rlpx/connection/server.rs |
Uses centralized blockchain sampling state. |
crates/blockchain/payload.rs |
Routes eager-provider behavior through Blockchain. |
crates/blockchain/mempool.rs |
Adds atomic sampling state and latching. |
crates/blockchain/blockchain.rs |
Implements fork-based activation and centralized gating. |
cmd/ethrex/l2/initializers.rs |
Updates L2 option wiring. |
cmd/ethrex/initializers.rs |
Wires the renamed sampling option. |
cmd/ethrex/cli.rs |
Clarifies blob-sampling help text. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| perf_logs_enabled: true, | ||
| max_blobs_per_block: None, | ||
| blob_sampling_enabled: false, // L2 rejects blob txs; no eth/72 sampling | ||
| force_blob_sampling: false, // L2 rejects blob txs; no eth/72 sampling |
| /// run per connection or per announcement, and pay one atomic load each once | ||
| /// the fork is behind us. | ||
| pub fn blob_sampling_enabled(&self) -> bool { | ||
| if self.mempool.blob_sampling_enabled() { |
| } | ||
| // L2 rejects blob txs outright, so its fork schedule must not drag the | ||
| // blobpool state machine in. | ||
| if !matches!(self.options.r#type, BlockchainType::L1) { |
| return false; | ||
| } | ||
| let head = self.storage.latest_block_timestamp(); | ||
| if !self.storage.get_chain_config().is_amsterdam_activated(head) { |
`NewPooledTransactionHashes72` encoded `cell_mask: None` as RLP nil, on the reading that the mask "MUST be nil when no type-3 tx is announced". That is not what the wire format says. devp2p `caps/eth.md` types the field `cells: B_16`, a fixed 16-byte element, and says only that it "can be ignored when no blob transactions are announced" — its content is irrelevant there, its width is not. geth decodes the field into `types.CustodyBitmap [16]byte`, which rejects a shorter string with "input string too short", so every announcement we sent without a blob tx was undecodable by the rest of the network. The hive devp2p `Transaction`, `InvalidTxs`, `NewPooledTxs` and `LargeTxRequest` cases all fail on exactly that error. Encode an all-zero bitmap for the unset case. `cell_mask` stays `Option` in memory, where `None` still means "nothing to advertise", and the decoder stays lenient about a nil mask from a peer that reads the spec the way we did. `cell_mask_none_encodes_to_rlp_nil` asserted the old behavior and is retargeted to the spec's.
The custody-generation sweep walked blob_txs_missing_cells, which spans the whole pool, and pushed a GetCells for every entry at whichever peer triggered the sweep. devp2p caps/eth.md fetches cells from peers that announced overlapping availability, so a peer was being asked for transactions it had never announced. The devp2p GetCells test catches this as a request carrying hashes left over from an earlier test rather than the one just announced. Filter the sweep by the announcement record the mempool already keeps.
…unsolicited pooled txs Two eth/72 fixes surfaced by the devp2p suite once sampling switches itself on. Native rollups run as BlockchainType::L1 on purpose, because their blocks must re-execute under an unmodified L1 environment, so the L2 type check could not keep them out of the blobpool state machine. Their `force_blob_sampling: false` used to mean "no eth/72 sampling"; with activation driven by the fork it no longer did, and a native rollup scheduling Amsterdam would have started advertising eth/72 on a chain that rejects blob transactions outright. Carry the intent in an explicit `blob_txs_supported` flag that both rollup entry points clear, and keep the type check as a backstop. The eth/72 PooledTransactions handler also dropped any peer whose response carried a transaction outside the request. The stored announcement is trimmed to the hashes a given request asked for, so a peer answering the whole announcement it originally sent — which devp2p permits, and which go-ethereum does — was disconnected for it. devp2p's BlobTxAvailabilityFailure test fails on exactly that. Tolerate the extra entries instead, and drop them before they reach the pool rather than admitting transactions nobody asked for; announced type and size are still enforced for everything the request did cover.
A type-3 transaction carrying no blob versioned hashes is invalid on its own terms, since EIP-4844 requires at least one, and an empty sidecar agrees with it. The eth/72 handler treated every BlobsBundleError as a protocol violation and disconnected, so such a transaction cost the connection rather than just being rejected. devp2p's BlobTxAvailabilityFailure announces ten blob txs from makeBlobTxs(10, 4, ...), whose blob budget only covers the first two; the remaining eight are exactly this shape, and the suite expects the node to stay connected. A sidecar that disagrees with its transaction is still a violation and still disconnects, which is what BlobTxWithoutSidecar and BlobTxWithMismatchedSidecar check.

Motivation
eth/72 (EIP-8070) is gated behind
--blob-sampling, which defaults to off, so a default node never advertises the capability.The gate is load-bearing rather than cosmetic: eth/72 always elides blob payloads from
PooledTransactions, so blobs reach the node only throughGetCells, and only the sampler/provider state machine issues those. A node that advertised eth/72 without running the loop would accept blob transactions it could never reconstruct.What the gate got wrong is what it keys on. A CLI flag has no relationship to when the network starts speaking eth/72; the fork does.
Changes
Blob sampling now switches itself on once the chain head reaches Amsterdam, the fork EIP-8070 rides on.
Blockchain::blob_sampling_enabled()replaces the publicMempool::blob_sampling_enabledfield as the single answer to "is the state machine active", and resolves the fork check.BlockchainOptions::blob_sampling_enabledbecomesforce_blob_sampling, reflecting its new job: bring activation forward, for devnets and for chains that schedule Amsterdam later than they want the state machine running.--blob-samplingand--blob-eager-providerare unchanged for operators.Blockchaininstead of reaching into the mempool.Store::latest_block_timestamp()is added so the fork check places the head against the schedule without cloning a header.Rollups are excluded explicitly. They reject blob transactions outright, so their fork schedule must not drag the blobpool state machine in.
BlockchainTypealone cannot carry this: native rollups run asBlockchainType::L1on purpose, because their blocks must re-execute under an unmodified L1 environment, so the type check would have let a native rollup that schedules Amsterdam start advertising eth/72. An explicitblob_txs_supportedflag, cleared by both rollup entry points, carries the intent; the type check stays as a backstop.Latching
The flag is one-way, for the same reason the eager-provider latch is: a reorg back below the fork boundary would otherwise flap the advertised capability set and churn peer connections over a boundary the chain is about to cross again.
It resolves lazily on the p2p paths that ask the question (per connection, per announcement) rather than on block import, so the import path pays nothing and the steady state after the fork is one atomic load.
The
Hive - Devp2p testsjobThat job has failed on every
mainrun since 2026-09-07. The cause is upstream, in geth's test helper:eth/71 is missing from the list, while
negotiateEthProtocolpicks the highest remote version<= ourHighestProtoVersionwithout consulting that list at all. Against a node topping out at eth/71 the simulator therefore speaks eth/71 while we negotiate eth/70, ourvalidate_statusrejects the version mismatch, and we disconnect.Statusstill passes because the simulator reads our Status before sending its own, so the wholeethandsnapsuites fail on the next message.GetBlockAccessListsis the one eth test that passes, because it usesdialEth71(), which overrides the caps list.Advertising eth/72 puts both sides on the version the helper does advertise. This is a real fix for us rather than a workaround, but the upstream helper is still wrong for any client that tops out at eth/71, so it is worth reporting separately.
It does not go green, and it un-skips three tests that did not pass
Measured against
mainatc00743efc(23 failures: 18eth, 5snap):snapfailures, plus 16ethBlobViolationsBlobTxAvailabilityFailure,GetCells,BlobTxWithInvalidCellsCounts here are real tests. Each suite also reports a
client launchcase that fails whenever anything under it does, somain's 23 is 21 real failures plus two wrappers.Advertising eth/72 un-skips the three tests that previously reported
skipping: node negotiated eth/71, eth/72 required, and they did not pass on first contact. Turning the capability on by default while they fail would ship a default-on wire protocol that fails conformance, so the causes are fixed here rather than deferred. Reproduced locally against hive43ea47bewith the simulator's own go-ethereum: 4 failures before, 2 after.GetCells— cells requested from a peer that never announced the transaction. The custody-generation sweep walkedblob_txs_missing_cells, which spans the whole pool, and queued aGetCellsfor every entry at whichever peer happened to trigger the sweep. devp2pcaps/eth.mdfetches cells "from peers that announced overlapping availability". The test caught it as a request carrying two hashes left over from an earlier test instead of the one just announced — the same two hashes, deterministically, in CI and locally. Now filtered by the announcement record the mempool already keeps.BlobTxAvailabilityFailure— two unnecessary disconnects. The handler dropped any peer whose response carried a transaction outside the request; the stored announcement is trimmed to the hashes a given request asked for, so a peer answering the whole announcement it originally sent — which devp2p permits, and which geth does — was disconnected for it. Those entries are now tolerated and then dropped before they reach the pool, so nothing unasked-for is admitted. Separately, a type-3 transaction declaring no blob versioned hashes is invalid on its own terms and its empty sidecar agrees with it; that is a bad transaction, not a lying peer, and no longer costs the connection.makeBlobTxs(10, 4, ...)has a blob budget covering only its first two transactions, so eight of the ten it announces are exactly that shape. A sidecar that disagrees with its transaction still disconnects, which is whatBlobTxWithoutSidecarandBlobTxWithMismatchedSidecarcheck, and both still pass.BlobTxWithInvalidCellspasses; it failed in one CI run and in neither local run, so it looks order- or timing-sensitive rather than broken.What still fails
BlobViolationsannounces a blob tx with a deliberately wrong size (Size() + 10) and expects a disconnect.validate_requestedskips the size check for blob txs on purpose: the announced size covers the full wrapper while eth/72 delivers the elided form, so the received encoding cannot be compared against it directly. Catching this needs the full size reconstructed from the blob count, which is exact but fiddly RLP framing arithmetic, and getting it wrong disconnects honest peers. Left as is deliberately, and better done as its own change. Its second case, the wrong announced tx type, is caught today.client launchis not a separate failure. It is thehivesim.ClientTestSpecwrapper insimulators/devp2p/main.gothat starts the client and runs geth'sdevp2p rlpx eth-test --tapbinary, reporting each TAP line as a sub-test;runTAPends inreturn cmd.Wait()andrunEthTestcallst.Fatalon a non-zero exit, and the tool exits non-zero whenever any sub-test failed. So it double-counts, and it clears itself onceBlobViolationsdoes. Thesnapsuite's ownclient launchalready passes here, because nothing under it fails.Also here: the announcement cell bitmap
cell_mask: Noneused to go on the wire as RLP nil. devp2pcaps/eth.mdtypes the elementcells: B_16— always 16 bytes, whose content "can be ignored when no blob transactions are announced" — while EIP-8070 says the field "MUST be set tonil" in that case. The two specs contradict each other. geth decodes the element intotypes.CustodyBitmap [16]byteand rejects anything shorter, so a nil mask makes every blob-free announcement undecodable to the rest of the network; this follows devp2p. Worth raising against EIP-8070 separately.The round trip is no longer the identity:
Noneencodes to an all-zeroB_16and decodes back asSome(0). That is harmless because receivers only consult the mask when the announcement actually carries a type-3 transaction, but it is a behaviour change on the wire and not only in memory.Caveat
EIP-8070 is Draft and a Glamsterdam candidate, not scheduled for inclusion. Tying activation to Amsterdam presumes it lands there. If it slips to a later fork, the constant in
Blockchain::blob_sampling_enabledmoves with it.Testing
cargo clippy --lib --bins -F debug,sync-test -- -D warningsclean. Fullethrex_testsbinary: 1232 passed, 0 failed, includingsampling_tests,mempool_cells_testsandeth72_engine_tests.devp2p was run end to end rather than reasoned about, on hive
43ea47bewith the simulator building its own go-ethereum, against images built from this branch:discv4,discv5,snapandsnap2pass in full. Of the two remainingethfailures one is theclient launchwrapper, soBlobViolationsis the only real test still failing.