Skip to content

cl: complete errcheck rollout across cl package and fix violations - #23577

Merged
AskAlexSharov merged 21 commits into
erigontech:mainfrom
Sahil-4555:linter/enable-errcheck-cl
Aug 26, 2026
Merged

cl: complete errcheck rollout across cl package and fix violations#23577
AskAlexSharov merged 21 commits into
erigontech:mainfrom
Sahil-4555:linter/enable-errcheck-cl

Conversation

@Sahil-4555

@Sahil-4555 Sahil-4555 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Completes the errcheck rollout for #22538 across the entire cl/* tree. Started as cl/beacon, cl/cltypes, cl/phase1, cl/validator; this PR adds every remaining cl/* package: cl/aggregation, cl/antiquary, cl/das, cl/merkle_tree, cl/p2p, cl/persistence/blob_storage, cl/persistence/state(+historical_states_reader), cl/rpc, cl/sentinel(+communication/ssz_snappy,handlers,httpreqresp,service), cl/spectest/consensus_tests, cl/transition/impl/eth2(+statechange), cl/utils/bls. Only non-cl/* packages remain excluded for future PRs.

Notable fixes beyond lint-satisfying:

  • SetSlot/ResetEpochParticipation left the state half-mutated on an antiquary-hook failure (slot changed but leaf not marked dirty; previousEpochParticipation reassigned before the hook ran) - both now roll back cleanly on error.
  • fork_graph.NewForkGraphDisk panicked on a disk write failure during startup; now returns the error through the existing startup error path.
  • ProcessPendingConsolidations and two wp.Execute() worker-pool results in epoch processing were silently discarding real errors; now propagated.
  • Added a "Review also caught and fixed" section covering Alex's direct commits: the errors.Is/bare-io.EOF distinction on network streams vs bytes.Reader, the dropPeer/closePeer dedup (9x repeated block), two error returns that could never fire (runDownload, the one-worker ParallellForLoop), and the AddAttestation test now only tolerating ErrIsSuperset specifically instead of masking anything.

Everything else is peer/cleanup operations logging on failure instead of discarding silently, or straightforward test fixes.

Sahil-4555 and others added 16 commits August 21, 2026 17:05
…nsition, fix review findings

Closes the gap the interface change opened: cl/transition could still
silently discard the persistence-hook errors now returned by
abstract.BeaconState setters. Also fixes partial-mutation bugs in
SetSlot/ResetEpochParticipation on hook failure, and replaces a panic
on disk-write failure in fork_graph.NewForkGraphDisk with a returned
error routed through the existing startup error path.
- solid: bound-check memberIndexInCommittee in ToAttestation; the dev
  validator feeds it straight from a beacon-API duties response, so an
  out-of-range value panicked one line below the new committee-index check.
- raw/setters: mark ValidatorsLeafIndex after the event hook, not before.
  The new early return on hook error left the leaf dirty for a write that
  never landed, forcing a full validators-subtree re-hash.
- historical_states_reader: propagate SetSlot/SetCurrentSyncCommittee/
  SetNextSyncCommittee errors instead of discarding them.
- forkchoice: propagate the versionedHashes RangeErr; a truncated list made
  the EL blob check pass and skipped data availability.
- state: panic in New if InitBeaconState fails instead of ignoring it.
- raw: drop init's unused error return; the two `_ =` swallows go with it.
- synced_data, handler: no require from a goroutine holding the manager
  mutex, and bound the ViewHeadState retry loop.
- state: hoist require.NoError out of b.Loop in the root and shuffling
  benchmarks.
- upgrade_test: key the balance hook on the QueueExcessActiveBalance write
  so the test cannot pass via the earlier zeroing loop.
- services, devvalidator: raise the pending-envelope failure to Warn and
  log the aggregate-build inputs.
- ssz_snappy.EncodeAndWrite discarded both flush errors, so a send that
  never reached the peer still returned nil; the buffered writes it now
  checks almost never fail on their own.
- ProcessPendingConsolidations resolved the target balance only while
  crediting it, so a bad target index drained the source and credited
  nothing. Resolve it before touching the source.
- sentinel_requests_test compares the read error with errors.Is, like the
  other handlers' tests.
… block

- runDownload never returned anything but nil, so both new callers' error
  branches were dead. Drop the return value instead of pretending.
- getFlagsTotalBalances' worker panicked on a bad validator index, so the
  error return added for it could never fire. Return the error.
- httpreqresp logs a failed SetDeadline like cl/sentinel/handlers does,
  instead of a 400 that costs the peer its connection over a stream reset.
- handlers.go keeps only SetDeadline; it already covers both directions.
- Extract dropPeer/closePeer: the RemovePeer/RemovePeer/ClosePeer block was
  copied nine times across service.go and discovery.go.
- GetUnslashedIndiciesSet ran a one-worker ParallellForLoop whose error was
  discarded; a plain loop is the same thing without the ignored error.
- Bench loops use b.Fatal, not testify, so the recorded ns/op stay comparable.

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

Completes the errcheck rollout across the cl/* tree by converting previously ignored errors into explicit handling/propagation, and updates .golangci.yml to stop excluding cl/* from errcheck. This touches consensus state transition logic, CL networking, persistence, and a broad set of tests/benchmarks to keep the package tree lint-clean.

Changes:

  • Removed errcheck exclusions for cl/* and updated CL code paths to return, log, or assert on errors rather than discarding them.
  • Propagated worker-pool execution errors and made some state transition helpers return errors (e.g., pending consolidations).
  • Updated many tests/benchmarks to check new/previously ignored error returns.

Reviewed changes

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

Show a summary per file
File Description
cl/utils/bls/cache_test.go Benchmarks/tests now check cache-load and aggregate-verify errors.
cl/transition/impl/eth2/statechange/process_sync_committee_update_test.go Test updated to assert state mutation calls succeed.
cl/transition/impl/eth2/statechange/process_rewards_and_penalties.go Worker-pool execution errors are now propagated to callers.
cl/transition/impl/eth2/statechange/process_pending_consolidations.go Function now returns error and checks balance mutations.
cl/transition/impl/eth2/statechange/process_epoch.go Epoch processing now handles consolidation errors and avoids unchecked parallel loop return.
cl/transition/impl/eth2/statechange/process_epoch_test.go Test harness simplified to pass error-returning functions directly.
cl/transition/impl/eth2/statechange/finalization_and_justification.go Worker-pool execution errors are now returned instead of dropped.
cl/transition/impl/eth2/operations_bls_change_test.go Test now asserts validator addition succeeds.
cl/spectest/consensus_tests/epoch_processing.go Spectest wiring updated to use error-returning functions directly.
cl/sentinel/service/service.go ClosePeer errors are now logged instead of ignored.
cl/sentinel/sentinel_requests_test.go Tests now handle ReadByte errors (including EOF) explicitly.
cl/sentinel/httpreqresp/server.go Deadline-set errors are now checked and surfaced via HTTP error responses.
cl/sentinel/handlers/rate_limiter_integration_test.go Integration tests now assert stream deadlines are set successfully.
cl/sentinel/handlers/light_client_test.go Tests now check stream reads and tolerate EOF explicitly.
cl/sentinel/handlers/heartbeats.go Explicitly discards (documents) a best-effort send error.
cl/sentinel/handlers/handlers.go Deadline-set errors on streams are now checked and logged.
cl/sentinel/handlers/blocks_by_root_test.go Test now checks tx.Commit and stream read errors.
cl/sentinel/handlers/blocks_by_range_test.go Test now checks stream read errors.
cl/sentinel/handlers/blobs_test.go Test now checks stream read errors.
cl/sentinel/discovery.go ClosePeer errors are now logged instead of ignored during pruning/connection handling.
cl/sentinel/communication/ssz_snappy/encoding.go EncodeAndWrite now checks buffered writes and flush errors to avoid silent short writes.
cl/rpc/rpc.go BanPeer RPC call errors are now logged instead of ignored.
cl/persistence/state/validator_events_test.go Test now asserts ReplayEvents returns nil.
cl/persistence/state/historical_states_reader/historical_states_reader_test.go Test now asserts OnHeadState succeeds.
cl/persistence/state/historical_states_reader/gloas_roundtrip_test.go Tests now assert BitVector SetBitAt errors are handled.
cl/persistence/blob_storage/bucket_store.go Temp-file cleanup now checks/remediates remove errors after write failures.
cl/p2p/p2p_discovery.go Peer connect/close failures are now logged instead of ignored.
cl/merkle_tree/merkle_tree_test.go Test helpers now assert MerkleRoot computations succeed.
cl/das/peer_das.go Cleanup/scheduling paths now log/handle errors instead of dropping them.
cl/antiquary/state_prune_test.go Test now asserts OnHeadState succeeds.
cl/antiquary/state_prune_reader_test.go Test now asserts OnHeadState succeeds.
cl/antiquary/state_antiquary_test.go Tests now assert OnHeadState succeeds.
cl/antiquary/antiquary_test.go Test now asserts etl.Collector Load succeeds.
cl/aggregation/pool_test.go Tests now check bitvector set errors and (mostly) AddAttestation errors.
.golangci.yml Removes cl/* from the errcheck exclusion bootstrap list.
Suppressed comments (1)

cl/utils/bls/cache_test.go:91

  • Using testify/require inside the benchmark hot loop adds non-trivial overhead and can skew the measured performance. Prefer a simple branch with b.Fatal on error.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +51 to +53
if applyErr != nil {
return applyErr
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed. IncreaseBalance failing now restores the source balance to its pre-debit value, so a failed move cannot destroy balance. (SetValidatorBalance runs its antiquary hook before mutating, so the failed credit itself leaves nothing behind.) The wider point — earlier consolidations in the same Range are already applied when a later one fails — is inherent to the epoch transition, which is not atomic; the caller discards the state on error.

Comment thread cl/aggregation/pool_test.go Outdated
Comment on lines +320 to +323
// Some testcases intentionally add a subset attestation
// (e.g. "skip att1_1"), which AddAttestation rejects with
// ErrIsSuperset by design; only the final merged state matters here.
_ = pool.AddAttestation(tc.atts[i])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed. The loop now allows only ErrIsSuperset; any other AddAttestation error fails the test.

Comment thread cl/utils/bls/cache_test.go Outdated
@AskAlexSharov

Copy link
Copy Markdown
Collaborator

Reviewed and pushed the fixes directly to the branch (two commits). Summary of what the review turned up.

Fixed: the error checks that could never fire

The errcheck pass added if err := ... in several places where the callee has no failing path, which reads as error handling without being any:

  • cl/das/peer_das.gorunDownload had return nil on every path, so both new log.Warn("failed to run download") branches were dead and DownloadOnlyCustodyColumns / DownloadColumnsAndRecoverBlobs still reported success after a cancelled or entirely-failed download. Dropped the error from its signature rather than keep the appearance of handling.
  • getFlagsTotalBalances — the new ([]uint64, error) return was unreachable: the one real failure, s.ValidatorEffectiveBalance, panicked inside a worker goroutine. Now returns the error, so the plumbing added at the call site actually carries something.
  • GetUnslashedIndiciesSet_ = threading.ParallellForLoop(1, ...) with a comment asserting the loop cannot error. With numWorkers = 1 that executor wraps a plain sequential loop, so it is now a plain loop: no ignored error, no asserted invariant.

computePreviousAndCurrentTargetBalancePostAltair's wp.Execute() check is in the same category (every worker returns nil), but errcheck requires it and it costs nothing, so I left it.

Fixed: errors that were still being swallowed

  • ssz_snappy.EncodeAndWrite — the two wr.Write calls the PR started checking write into a bufio.Writer sized 10+len(enc); they cannot fail. Both Flush() calls — the ones that actually reach the libp2p stream — were still discarded through defer, so a response that never left the process returned nil. errcheck misses this because the legacy exclusion preset skips .*Flush. Now flushes explicitly and returns those errors.
  • ProcessPendingConsolidationsc.TargetIndex was never validated, so IncreaseBalance could fail after DecreaseBalance had already debited the source: balance destroyed, consolidation still queued. The target is now resolved before the source is touched.

Fixed: behaviour and duplication

  • cl/sentinel/httpreqresp/server.go — three SetDeadline failures were promoted from ignored to http.StatusBadRequest. SentinelServer.requestPeer treats any non-2xx/3xx as peer failure and, past MaxPeerCount, does RemovePeer + Peerstore.RemovePeer + ClosePeer — so a stream reset between NewStream and SetWriteDeadline now cost the peer its connection, while the identical calls in handlers.go only log at Trace. Both files now log at Trace; the subsequent read/write surfaces the real transport error anyway.
  • handlers.go — kept only SetDeadline; per network.Stream it covers both directions, so the diff's three checked calls were one.
  • service.go / discovery.go — the RemovePeer + Peerstore.RemovePeer + ClosePeer triple appeared nine times, and the errcheck fix grew each from three lines to five. Extracted dropPeer/closePeer; net −85 lines across the two files.

Test-side

b.Fatal instead of require.NoError inside b.Loop() in cl/utils/bls/cache_test.go — testify's Helper() takes a mutex and a map insert per iteration, and this file records its own baseline ns/op in comments. Also errors.Is(err, io.EOF) in sentinel_requests_test.go to match the four sibling handler tests changed in the same diff, dropped a duplicated SetBitAt(10, true) in pool_test.go, and trimmed two comments to the repo's policy.

Not changed — worth a decision

  1. ProcessPendingConsolidations is now inconsistent about failure. A DecreaseBalance error aborts the epoch, but ValidatorForValidatorIndex and ValidatorBalance failures still log a Warn, advance nextConsolidationIndex, and let the consolidation be Cut from the queue as if processed. That is main's behaviour and turning it into an error is a consensus-visible change, so I left it — but it should be all four or none, and the current split is hard to defend.

  2. The description's "notable fixes" list is stale. SetSlot/ResetEpochParticipation rollback and the fork_graph.NewForkGraphDisk panic are not in this diff; only the ProcessPendingConsolidations and wp.Execute() items are.

Verified after the changes: go build ./..., the full ./cl/... suite, and golangci-lint --enable-only errcheck ./cl/... as a full run (not --new-from-rev) — which matters here, since removing packages from the exclusion list exposes untouched lines too.

Comment thread cl/sentinel/sentinel_requests_test.go Outdated

responsePacket = append(responsePacket, responseChunk)
if _, err := r.ReadByte(); err != nil && err != io.EOF {
if _, err := r.ReadByte(); err != nil && !errors.Is(err, io.EOF) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Do not convert this sentinel check to errors.Is.

Using errors.Is(err, rlp.EOL) (or errors.Is(err, io.EOF)) causes severe decoding bugs.

errors.Is unwraps the error chain. If a nested element decoder encounters malformed or truncated input and returns a wrapped error (e.g. fmt.Errorf("decode field: %w", io.EOF)), errors.Is unwraps it, matches EOL/EOF, and incorrectly treats the data corruption as a clean end of list, swallowing the error and returning nil.

The bare identity check (err == rlp.EOL with //nolint:errorlint) must be preserved so that wrapped decoding errors propagate properly rather than being swallowed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reverted — both lines are back to err != io.EOF. It was a consistency change, not a correctness one, so your call on your PR.

Two things for the record, though, because the stated rationale does not hold at this call site.

r here is bytes.NewReader(w.Bytes()), and (*bytes.Reader).ReadByte has exactly two outcomes:

func (r *Reader) ReadByte() (byte, error) {
	r.prevRune = -1
	if r.i >= int64(len(r.s)) {
		return 0, io.EOF
	}
	b := r.s[r.i]
	r.i++
	return b, nil
}

Bare io.EOF or nil. There is no underlying reader, no nested element decoder, and nothing that could wrap. err != io.EOF and errors.Is(err, io.EOF) are provably identical here, so neither form can swallow a corruption error. (And there is no rlp.EOL in this file — the RLP stream-decoder hazard you describe is real, but it is a different package and a different failure mode.)

Second: this PR makes the opposite choice four times, in blobs_test.go, blocks_by_range_test.go, blocks_by_root_test.go and light_client_test.go, all switched to errors.Is(err, io.EOF). Those read from a libp2p network.Stream obtained via host1.NewStream(...), which can return wrapped errors — so if the unwrapping hazard applies anywhere in this diff, it applies there and not here. Worth picking one form for all five rather than leaving them split.

For what it is worth, errorlint is enabled in .golangci.yml but does not flag the bare comparison — I checked with golangci-lint run --enable-only errorlint ./cl/sentinel/..., which is clean either way. So no //nolint is needed.

AskAlexSharov and others added 3 commits August 26, 2026 13:15
…Attestation

SetValidatorBalance can fail in its antiquary hook, so a target credit can
fail after the source debit landed. Restore the source instead of returning
with the balance destroyed.

The aggregation pool test now allows only ErrIsSuperset, so an unrelated
AddAttestation failure fails the test instead of being masked.
Reverts my errors.Is change at the author's request. The two forms are
equivalent here, and errorlint does not ask for either.
n, err := io.ReadFull(stream, code)
synthesizedEmptySuccess := false
if errors.Is(err, io.EOF) && n == 0 && communication.IsMultiChunkProtocol(topic) {
if err == io.EOF && n == 0 && communication.IsMultiChunkProtocol(topic) { //nolint:errorlint // intentional bare sentinel check

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.

This reverts #23303. Commit 6217798d87e ("all: resolve errorlint linter findings") changed this line from err == io.EOF to errors.Is(err, io.EOF); the //nolint:errorlint puts it back.

Production p2p read path, and the PR body doesn't mention it. If the bare sentinel is right here, the reasoning belongs in the description — an errcheck rollout silently undoing a merged errorlint decision is how the next errorlint pass re-flips it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed - reverts #23303, which mechanically converted every bare ==/!= error check repo-wide to errors.Is with no per-site analysis.

Checked the actual Read internals of both muxers this repo negotiates - go-yamux/v5 and go-libp2p-mplex - and neither wraps io.EOF; both return the bare sentinel. So at this line the two forms behave identically today.

Kept bare anyway: that equivalence relies on an unenforced assumption about third-party muxer internals that a dependency bump or a later refactor could silently break, with no compiler or lint signal. The bare form has no such fragility, and it matches the err == rlp.EOL / //nolint:errorlint convention already used for this exact class of check in execution/types/ and p2p/enr/enr.go.

}
if applyErr = state.IncreaseBalance(s, c.TargetIndex, sourceEffectiveBalance); applyErr != nil {
// Put the source back: a half-applied move destroys balance.
if err := s.SetValidatorBalance(int(c.SourceIndex), vBalance); err != nil {

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.

This rollback branch has no test. The only coverage for ProcessPendingConsolidations is the spectest vector PendingConsolidationTest (cl/spectest/consensus_tests/epoch_processing.go:114), which never makes state.IncreaseBalance fail, so the restore never executes.

It is the one path here where getting it wrong destroys balance, and it was added in review rather than by the rollout itself. A unit test with a mutator that fails IncreaseBalance would pin it.

if err := sw.Flush(); err != nil {
return err
}
return wr.Flush()

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.

Removing the two defers here makes cl/persistence/blob_storage/bucket_store.go:130 false. It still says "EncodeAndWrite flushes in a defer and discards that error, so a short write is only observable on the writer it was handed", which is the stated justification for the errWriter shim on the line below it.

Both flush errors now reach the caller, so w.err != nil at bucket_store.go:136 is only reachable when EncodeAndWrite already returned the same error at 133. Worth fixing the comment and dropping the shim here, since this is the change that invalidated it.

Comment thread cl/sentinel/discovery.go
}

// pruneExcessPeers disconnects excess peers while ensuring no subnet becomes empty
func (s *Sentinel) closePeer(pid peer.ID) {

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.

This was inserted between pruneExcessPeers' doc comment and pruneExcessPeers, so Go now attaches "disconnects excess peers while ensuring no subnet becomes empty" to closePeer — a three-line wrapper that knows nothing about subnets. pruneExcessPeers at 312 has no doc at all.

Move the comment down to 312, or give closePeer its own.

Merged via the queue into erigontech:main with commit 84ddcf5 Aug 26, 2026
139 checks passed
@Sahil-4555
Sahil-4555 deleted the linter/enable-errcheck-cl branch August 26, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants