Skip to content

cl, sentinel: quiet data column sidecar misses - #21686

Merged
domiwei merged 2 commits into
mainfrom
codex/fix-data-column-sidecar-log-flood
Jun 17, 2026
Merged

domiwei merged 2 commits into
mainfrom
codex/fix-data-column-sidecar-log-flood

Conversation

@domiwei

@domiwei domiwei commented Jun 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes the data column sidecar miss path so expected misses no longer produce noisy download logs, while keeping the wire behavior compatible with peers that signal an empty multi-chunk response by closing without a response code.

Closes #21670

Details

  • Return structured req/resp resource unavailable responses for by-root misses and valid pre-Fulu by-root requests, while validating malformed pre-Fulu by-root requests first so they return invalid request.
  • Restore by-range empty-success responses to a zero-byte close, including zero-count, all-pre-Fulu, all-future, and missing-sidecar ranges.
  • In the httpreqresp client bridge, synthesize success code 0 with an empty body when a negotiated multi-chunk protocol returns io.EOF before any response-code byte. Single-chunk EOFs, partial reads, and stream errors still surface as HTTP 400.
  • Keep handler responseErr propagation for real storage/write errors. Across the Sentinel HTTP/gRPC boundary those transport failures can still be flattened by gRPC, while structured peer response codes are surfaced as typed PeerResponseError values.
  • Bound peer error-message decoding to a 10-byte varint prefix and a 256-byte decoded message.
  • Downgrade expected data column sidecar misses to trace while keeping other download errors at debug.

Validation

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from b6c4709 to 40a122f Compare June 10, 2026 05:02
@domiwei
domiwei marked this pull request as ready for review June 10, 2026 05:03
@domiwei
domiwei requested a review from Copilot June 10, 2026 05:07
@domiwei
domiwei requested a review from yperbasis June 10, 2026 05:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the CL sentinel + DAS interaction for data column sidecar downloads by ensuring “misses” return a structured req/resp resource unavailable response (instead of silently closing/resetting streams), and by introducing typed error values so DAS can treat expected misses differently from real failures.

Changes:

  • Add httpreqresp typed errors (HTTPError, PeerResponseError) and shared ResponseCode definitions.
  • Update data column sidecar handlers to emit ResourceUnavailablePrefix when Fulu isn’t active or when no sidecars are available, while preserving real errors if nothing was written.
  • Update DAS peer download loop to downgrade expected resource unavailable misses to trace; add unit test coverage for the new classification helper.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cl/sentinel/service/service.go Switch to typed req/resp errors and centralized ResponseCode; affects peer error handling on non-success response codes.
cl/sentinel/httpreqresp/errors.go New shared typed error + response code utilities, including snappy error-message decoding.
cl/sentinel/handlers/data_cloumn_sidecar.go Ensure sidecar miss paths return resource unavailable responses and preserve underlying errors when nothing was written.
cl/sentinel/handlers/data_column_sidecar_test.go New tests validating resource unavailable prefix for missing/before-fork cases.
cl/das/peer_das.go Treat resource unavailable peer responses as expected misses (trace) instead of debug-level failures.
cl/das/peer_das_test.go New unit test for isExpectedColumnDownloadMiss.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cl/sentinel/service/service.go
Comment on lines +16 to +19
func (c *ConsensusHandlers) dataColumnSidecarsByRangeHandler(s network.Stream) error {
curEpoch := c.ethClock.GetCurrentEpoch()
if curEpoch < c.beaconConfig.FuluForkEpoch {
return nil
return ssz_snappy.EncodeAndWrite(s, &emptyString{}, ResourceUnavailablePrefix)
@yperbasis yperbasis added the Caplin Caplin: Consensus Layer, Beacon API label Jun 10, 2026

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few changes requested:

  1. Don't drop peers on code-3 misses. In requestPeer (service.go), any non-success response code still triggers RemovePeer/ClosePeer when at max peer count, so the misses this PR teaches the client to recognize as benign still churn healthy peers (Copilot flagged this too). Please skip removal for ResponseCodeResourceUnavailable — otherwise we log the miss at Trace but disconnect the peer anyway.

  2. By-range: ResourceUnavailable for legitimately-empty ranges. Per the Fulu spec, code 3 is for ranges the node is unable to serve within data_column_serve_range; a range whose blocks carry no blobs, an all-future range, or an all-pre-Fulu range should produce an empty success response. Other clients request columns by range blindly alongside blocks during range sync, so erroring on blobless stretches risks failed-batch handling / downscoring of erigon peers. The all-future and all-pre-Fulu cases are cheaply distinguishable (no storage lookup) and should return empty success; for the remaining blobless-vs-pruned ambiguity, please sanity-check behavior against lighthouse/prysm on a mixed-client devnet before merge.

  3. Missing success-path test. Nothing pins the key invariant of the new count/responseErr tail logic: a found sidecar must produce a success chunk with no trailing code-3 chunk. Please add that test. Also consider a wrapped-error case (fmt.Errorf("...: %w", peerErr)) in TestIsExpectedColumnDownloadMiss so future wrapping in the call chain can't silently break classification.

  4. Dead code: the four ResponseCode* const aliases in service.go are unused (only the type aliases are referenced) — drop them.

  5. Document the limitation. Peers that reply with spec-canonical empty responses (non-erigon clients, pre-upgrade erigons) still surface as HTTPError 400 "Read Code: EOF" at Debug, so the flood is only quieted for upgraded erigon peers. Worth a note in the description and a tracking issue for the client-side fix (treating a zero-chunk response as valid empty in httpreqresp).

Nits: errors.go is missing the license header its package-mate server.go has, and the moved ErrorMessage lost the one-line comment explaining the varint-skip-before-snappy decoding — worth keeping.

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from bc1d1fb to 50868ea Compare June 11, 2026 04:15

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Addressed the review feedback in 50868ea255:

  • Skipped peer removal/close for structured ResponseCodeResourceUnavailable misses, so expected code-3 misses no longer churn healthy peers.
  • Kept by-range legitimately-empty responses as empty success. I also added an all-pre-Fulu boundary check because the previous unsigned endSlot-startSlot math could turn that case into request range is too large.
  • Added the by-root success-path test that verifies one successful sidecar is not followed by a trailing code-3 chunk.
  • Added wrapped-error coverage for isExpectedColumnDownloadMiss.
  • Dropped the unused ResponseCode* const aliases from service.go.
  • Added the license header and restored the varint-before-snappy comment in httpreqresp/errors.go.
  • Updated the PR description to clarify that HTTPError 400 "Read Code: EOF" remains treated as a real transport/protocol error; this PR only downgrades structured peer resource unavailable responses.

Validation:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from 50868ea to 73f7f3f Compare June 11, 2026 04:29

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Follow-up after another adversarial pass: fixed two by-root boundary cases in 73f7f3fce1:

  • Invalid by-root column indexes are now rejected before they can fall through to the count == 0 resource-unavailable path, matching by-range validation.
  • Empty by-root requests / identifiers with empty column lists now return empty success instead of resource unavailable.

Added tests for both empty by-root cases and for the invalid-column path not being reported as resource unavailable.

Validation remains:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from 73f7f3f to 52c9360 Compare June 11, 2026 04:46

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Second follow-up after another adversarial pass, now in 52c9360a9d:

  • Invalid data-column requests now emit InvalidRequestPrefix instead of resetting/no-response:
    • by-range overflow
    • by-range invalid column index
    • by-range too-large range
    • by-root oversized request
    • by-root invalid column index
  • Added tests for the by-range boundary matrix:
    • zero-count empty success
    • all-future empty success
    • all-pre-Fulu empty success
    • invalid column -> invalid request
    • overflow -> invalid request
    • too-large range -> invalid request
  • Updated the by-root invalid-column test to expect invalid request instead of merely “not resource unavailable”.

Revalidated:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from 52c9360 to d5f535b Compare June 11, 2026 05:23

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Third follow-up after another split adversarial review, now in d5f535b91a:

  • Fixed empty-success data-column sidecar responses to write the success response-code byte (SuccessfulResponsePrefix) before closing the stream.
  • This covers by-range zero-count, all-pre-Fulu, all-future, no-hit/blobless ranges, and by-root empty request / empty column-list cases.
  • Updated the empty-success tests to require the 0x00 response-code byte followed by no payload, rather than accepting a no-byte EOF.

Revalidated:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from d5f535b to 05e3ca9 Compare June 11, 2026 05:47

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Fourth follow-up after another split subagent review, now in 05e3ca9e6a:

  • Code-3 error body decoding now propagates read/decode errors instead of swallowing them. If a peer sends resource unavailable with an oversized/malformed body, requestPeer treats that as a real failure instead of an expected miss.
  • Handler storage/write errors after a previous successful chunk now return the error so the stream resets instead of gracefully closing a malformed partial success response.
  • Nested column lists longer than NumberOfColumns now return InvalidRequestPrefix for both by-root and by-range, even if all values are otherwise in range.
  • Decode-time invalid data-column requests now return InvalidRequestPrefix in the data-column handlers.
  • Added focused tests for over-limit nested column lists and for ResponseCode.ErrorMessage surfacing ErrResponseTooLarge.

Revalidated:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from 05e3ca9 to 5cdf92b Compare June 11, 2026 06:33

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Fifth follow-up after an AGENTS.md-style adversarial review, now in 5cdf92b5d6:

  • Added the missing by-range empty-columns fast path: nonzero-count requests with zero requested columns now return empty success immediately instead of scanning slots / reading canonical roots with only the wrapper admission token charged.
  • Added TestDataColumnSidecarsByRangeEmptyColumnsReturnsEmptySuccess.

Risk matrix checked in this pass:

  • zero / empty / all-pre-Fulu / all-future / overflow / too-large / invalid-column behavior
  • by-root vs by-range symmetry
  • untrusted input causing avoidable I/O
  • partial write error handling
  • code-3 body decode / oversize behavior
  • direct-client typed error classification
  • retry/log-flood residual behavior

Revalidated:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

domiwei commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Consolidated update for the review feedback: the latest pushed commit is 5cdf92b5d6.

Addressed items:

  • Code-3 ResourceUnavailable no longer removes/closes otherwise healthy peers; malformed/oversized code-3 bodies still propagate as real failures and are not treated as expected misses.
  • By-range legitimately-empty responses now return a valid empty success (0x00 response-code byte, no payload), including zero-count, empty columns, all-pre-Fulu, all-future, and blobless/no-hit ranges.
  • By-root still returns structured ResourceUnavailable for miss/unavailable cases, while empty by-root requests / empty column lists return empty success.
  • Invalid requests now return InvalidRequestPrefix for overflow, too-large ranges, invalid columns, nested column lists over NumberOfColumns, and decode-time malformed data-column requests.
  • Success paths are pinned so a found sidecar does not get a trailing code-3/empty response; storage/write errors after partial output now return an error so the stream resets instead of graceful-closing a malformed response.
  • DAS only downgrades typed/wrapped PeerResponseError{Code: ResourceUnavailable}; HTTP EOF, invalid request, server error, and legacy string errors remain non-benign.
  • Removed the unused response-code const aliases from service.go; added the license header and preserved the error-body varint/snappy decoding note.

Adversarial review residuals I intentionally did not broaden in this PR:

  • The existing DAS retry loop can still retry every 100ms until context cancellation or data availability; this PR only demotes expected code-3 misses to trace.
  • A generated external gRPC Sentinel client would not preserve the concrete PeerResponseError type, but the runtime path here is the direct in-process Sentinel client.
  • By-root ResourceUnavailable still intentionally conflates missing/pruned/unavailable causes.

Validation:

  • go test ./cl/sentinel/httpreqresp ./cl/sentinel/service ./cl/sentinel/handlers ./cl/das -count=1
  • make lint

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-1 items are addressed. Remaining:

  1. Bare 0x00 empty-success is a truncated chunk to other clients. Per the req/resp spec an empty response is zero chunks (zero bytes on the wire); a result byte followed by EOF is a truncated chunk. Our blobs/blocks handlers close with zero bytes for empty results. Lighthouse's outbound codec buffers a lone byte (if src.len() <= 1 { return Ok(None) } in rpc/codec.rs) and doesn't override decode_eof, so tokio-util's default turns the leftover byte at stream close into Err("bytes remaining on stream") → RPCError::IoError → PeerAction::HighToleranceError plus a failed request. Every blobless/pruned/backfilling by-range we serve to a Lighthouse peer would fail with a penalty instead of parsing as a clean empty; Prysm's chunk reader errors on the same pattern. Please revert the writeDataColumnSidecarsEmptySuccess paths to plain return nil (zero-byte close). The by-root code-3 responses are complete chunks and should stay — verified penalty-free in Lighthouse's handle_rpc_error for DataColumnsByRoot.

  2. The #21670 flood is mostly out of scope as written — fix the client side. The flood is Read Code: EOF, readBytes=0 on by-root: peers (mostly non-erigon) sending spec-canonical zero-byte empty responses that our client turns into HTTP 400. Code-3 quieting only helps against upgraded erigon peers. In httpreqresp.NewRequestHandler, when io.ReadFull(stream, code) returns io.EOF with 0 bytes read and communication.IsMultiChunkProtocol(topic), synthesize success (set REQRESP-RESPONSE-CODE: 0, hand back the empty body). parseResponseData already treats an empty body as zero chunks and the DAS loop already skips zero sidecars. Keep erroring for single-chunk protocols and for stream resets (network.ErrReset ≠ io.EOF). This is coupled with point 1: reverting the 0x00 alone would reintroduce erigon↔erigon by-range EOF noise. With both, this PR can say Closes #21670 — please add the reference either way.

  3. Description wording. "Preserve real server/storage/write errors when no response chunk was written" — the code returns responseErr regardless of whether chunks were written (the wrapper then resets, discarding the partial response). Behavior is fine; fix the wording. Also worth one line that over real gRPC (separate sentinel process) *PeerResponseError is flattened, so misses stay at Debug there.

Nits:

  • License headers missing on data_column_sidecar_test.go and peer_das_test.go.
  • Pre-Fulu by-root: malformed requests get code-3 instead of code-1 (fork gate precedes validation).
  • ErrorMessage: consider bounding the varint loop (≤10 bytes) and the decoded message (spec caps ErrorMessage at 256 bytes) rather than relying on the per-topic cap.
  • Validation section lists ./cl/sentinel/service, which has no test files.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment on lines 63 to 66
// Consume additional rate-limit tokens: slots × columns per slot, capped at config max.
if cost := min(int(req.Count)*req.Columns.Length(), int(c.beaconConfig.MaxRequestDataColumnSidecars)) - 1; !c.consumeRateLimit(s, cost) {
return nil
}
@domiwei
domiwei force-pushed the codex/fix-data-column-sidecar-log-flood branch from 5cdf92b to 01b086c Compare June 16, 2026 09:38
@yperbasis
yperbasis requested a review from Copilot June 16, 2026 10:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment on lines +85 to +89
for i := 0; i < 10; i++ {
b, err := rawReader.ReadByte()
if err != nil {
return "", err
}
Comment on lines +18 to +20
func writeDataColumnSidecarsEmptySuccess(s network.Stream) error {
return nil
}
"github.com/libp2p/go-libp2p/core/network"
)

var errInvalidDataColumnIndex = errors.New("invalid column index")

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — prior review items are addressed; targeted tests and golangci-lint pass locally. A few non-blocking nits:

  • Before merge: by-root returns code-3 (ResourceUnavailable) on a miss — verified penalty-free in Lighthouse, but please sanity-check Prysm on a mixed-client devnet, since the downside is peer downscoring during range sync.
  • Filename typo: the handler is still data_cloumn_sidecar.go while the new test is data_column_sidecar_test.go; a git mv to fix the spelling would make them match and grep cleanly.
  • Service-layer coverage: the new "keep peer on code-3" branch in service.go isn't unit-tested, and the Validation command still lists ./cl/sentinel/service, which has no test files — drop it from the list or add the coverage.
  • httpreqresp/server.go: prefer errors.Is(err, io.EOF) over err == io.EOF for the synthesized-empty-success check (robust to any reader wrapping).
  • Cosmetic: the in-loop GetCurrentStateVersion(epoch) < FuluVersion skip in the by-range handler is now unreachable (startSlot >= fuluStartSlot); ResponseCode.ErrorMessage returns "" for code-1 without reading the body (preserved behavior, just noting).

@domiwei
domiwei added this pull request to the merge queue Jun 17, 2026
Merged via the queue into main with commit 04098aa Jun 17, 2026
93 checks passed
@domiwei
domiwei deleted the codex/fix-data-column-sidecar-log-flood branch June 17, 2026 08:23
mh0lt added a commit that referenced this pull request Jun 23, 2026
A "Merge origin/main" resolution on this branch accidentally reverted #21686
("quiet data column sidecar misses"), deleting cl/sentinel/httpreqresp/errors.go,
the data_column_sidecar / peer_das / server tests, isExpectedColumnDownloadMiss,
etc. — ~908 lines that are on main. It compiled clean so CI didn't flag it.
Restore all eight cl/das + cl/sentinel files to origin/main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Caplin Caplin: Consensus Layer, Beacon API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

debug logs flood - failed to download columns from peer

4 participants