eth/protocols/wit, eth: WIT2 size oracle — accept non-deterministic witnesses within a signed size band - #2416
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v2.10.2-candidate #2416 +/- ##
=====================================================
- Coverage 57.93% 57.53% -0.40%
=====================================================
Files 957 959 +2
Lines 177763 175858 -1905
=====================================================
- Hits 102984 101187 -1797
+ Misses 69118 69006 -112
- Partials 5661 5665 +4
... and 39 files with indirect coverage changes
🚀 New features to boost your workflow:
|
0df3826 to
cb11efb
Compare
…itnesses within a signed size band Witnesses are not deterministic across nodes: BlockSTM speculative reads make honest nodes collect different-but-valid trie-node sets, so a valid witness routinely hashes differently from the BP-signed WitnessHash. WIT2's fetch-time verifyAgainstSignedHash treated any hash divergence as a fault (reject the bytes, strike the serving peer, fall back to WIT1 after two distinct servers), so a valid witness that hashes differently is rejected and its serving peer struck. Replace exact-hash equality with a size oracle: - Sign the witness size. SignedWitnessAnnouncement gains a WitnessSize field and the announce signing pre-image commits to it (keccak(domain || blockHash || blockNumber || witnessHash || witnessSize)). Producers set it from their own witness length. - Accept within a band for import. verifyAgainstSignedHash accepts for import any witness whose encoded size is <= min(3*signedSize, gas-derived absolute ceiling), regardless of hash; content-correctness is still arbitrated by import-time state-root execution. A within-band witness is re-served/relayed only when byte-identical to the BP's (hash match); a valid non-deterministic variant imports locally but is not re-served, so the signed hash stays a faithful identifier of the bytes on the serving/relay fast-path. Blame for content rests with the producer that signed the announcement, not a relaying or serving peer — preserving WIT2's property of relaying a trusted witness before self-validating. - Bound the size. A witness beyond the band is rejected and the serving peer struck (first occurrence per (peer, block), reusing the distinct-server bookkeeping); distinct servers exceeding the band for the same block fall back to WIT1. The retained gas-derived ceiling keeps the accepted size bounded even when the signed size is implausibly large. No WIT2 is deployed yet, so the signed-announce format changes in place. Scope: WIT2 signed-path only. The WIT1 page-count cross-peer verification is a separate change, handled in a follow-up.
cb11efb to
0ed9568
Compare
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
|
@claude review |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
eth/fetcher/witness_manager.go— A maintainer reading this comment believes WIT2 still rejects a witness whose bytes don't hash to the BP-signed WitnessHash, which is no longer true after this PR. The comment at witness_manager.go:704-707 says the encoded witness "must hash to the signed witnessHash", but verifyAgainstSignedHash (witness_manager_wit2.go) now accepts any within-band size regardless of hash match. Fix: update this comment to describe the size-oracle acceptance (only oversized witnesses are rejected; hash divergence is observability-only), so the security invariant documented here matches the code a reader is about to call.Extended reasoning...
processWitnessResponse at witness_manager.go:708 calls verifyAgainstSignedHash immediately below this comment. Before this PR, verifyAgainstSignedHash rejected on hash mismatch, so the comment was accurate. This PR changed verifyAgainstSignedHash so a differing hash within the size band is accepted (ok=true) and only an oversized witness is rejected/struck. The comment block at lines 704-707 was not updated and still states the old byte-correctness invariant. A future reviewer or on-call engineer investigating a witness-serving incident would read this comment, conclude byte-correctness is still enforced pre-import, and misjudge the trust boundary between BP-signed hash and actually-served bytes — exactly the kind of stale security-invariant documentation the repo's own commenting guidance (AGENTS.md 'constraints and assumptions') warns against.
Verification: Severity: nit. The comment at eth/fetcher/witness_manager.go:704-707 states the encoded witness bytes "must hash to the signed witnessHash", and the diff did not touch it (git diff shows the only change to this file is the signedWitnessHashFn type signature adding witnessSize). This comment now contradicts the reworked callee verifyAgainstSignedHash in eth/fetcher/witness_manager_wit2.go,…
The comment above verifyAgainstSignedHash still described the pre-oracle byte-correctness invariant (hash must match). This PR's size-oracle rework now accepts hash divergence within the signed-size band, so the comment misdescribed the actual trust boundary an incident responder would rely on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Reviewed the diff and traced the paths around it. The change of direction is right: with each node persisting its own generated witness ( 1. Rollout note: the format change is not landing on a clean slate (temporary) The description says no WIT2 is deployed, but the WIT2 commit is in This is self-limiting: no jail is involved, redial is gated only by the 35 s dial-history / 30 s inbound-throttle timers, and once every WIT2 node runs the same format the churn stops on its own. So it's a mixed-version window issue, not a design blocker. Please:
2. The import-time arbiter has no consequence The PR leans on "import-time state-root execution remains the content-correctness arbiter", but in Suggestion: when import fails for a block whose fetched witness diverged from the signed hash, strike the serving peer and re-request from another announce-known peer instead of forgetting the hash. If that's deferred to a follow-up, say so in the description so the trade-off is on record. 3. Only one of three exact-hash gates was relaxed
4. Small things
Verified locally: |
…d size, relax the broadcast gates Review follow-ups for the WIT2 size oracle. Import-time consequence. A witness accepted on the size oracle alone (hash differs from the BP-signed one) that then fails import was logged at debug and forgotten, so a peer relaying the valid BP announce could serve up to the size band in unusable bytes per block at no cost — the WIT1-class weakness WIT2 was closing. Now the witness manager records the serving peer, the divergence and the fetch closure on the import op (verifyAgainstSignedHash returns diverged; enqueueOp keeps the op instead of rebuilding it from parts), and on insertChain failure importBlocks strikes the server, excludes it as a witness source for that block (new SetWitnessSourceExcluder hook → handler witnessSourceExclusions, honoured by resolveWitnessFetchPeer at every tier and released on import), and hands the block back to the witness manager for a re-fetch from another peer (new witnessRetry loop case → retryAfterImportFailure), bounded by maxWitnessImportRetries. A BP-identical witness that fails import is still the BP's fault and is handled as before. Signed size sanity. signedSize*3 could wrap for a hostile size and a zero WitnessSize yielded a zero ceiling; both struck honest servers. The band now saturates and a zero size falls back to the absolute cap, and acceptSignedAnnouncement refuses (and strikes the sender for) a WitnessSize of zero or above the gas-derived absolute cap before deferral, so the announcer is the one charged. Broadcast gates. acceptSignedBroadcast and acceptDeferredBroadcast now apply the same size oracle as the fetch path: a within-band body is accepted for import (sender marked as body-holder) regardless of hash, so a pusher's own post-import witness (flushWitnessWaitersForImported) is no longer rejected downstream; only byte-identical bytes are cached for pre-import serving, and an oversized body is dropped. fetchAndVerifyWitness (relay fetch, serve-only) keeps requiring the BP's bytes by design and now documents that limitation. Also refreshes every remaining "byte-correctness" comment to the size-oracle semantics (fetcher, handler, peerset, wit protocol), and renames the broadcast byte-mismatch meter to broadcast_oversize with new hash_divergence, implausible_size, import_failure and import_retry meters. Tests: end-to-end import-failure re-fetch through the real fetcher loop (TestImportFailureWithDivergedWitnessRefetchesFromAnotherPeer — caught the provenance loss in enqueue), charge/retry unit tests, degenerate-size and saturating-multiply cases, implausible announce size (0 and cap+1 struck, cap accepted), divergent/oversized broadcast on both the signed and deferred paths, source exclusion in resolveWitnessFetchPeer, and the exclusion set lifecycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Brme9KQBd7fZBMnMVEhAZU
#2417 changes the NewBlockFetcher call directly above the WIT2 striker wiring; editing the adjacent comment here made the two branches conflict when merged together even though each merges cleanly into the candidate. Leave the striker comment as it was and document the size-oracle extension (import-failure strike + source exclusion) in its own block below. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Brme9KQBd7fZBMnMVEhAZU
|
Re-reviewed One blocking issue remains, in the new import-failure charge.
It checks Two things make this bite in practice:
Sequence on a stateless node with a missing bytecode: block touches the contract, the (valid) witness imports fail on the local gap, the honest server is struck and excluded, the re-fetch reaches a second honest peer whose (valid) witness fails the same way and it is struck too, the retry budget is spent, the block is forgotten. The node falls behind and the downloader eventually heals the code, but the strikes stay on the tracker. At 5 strikes per sliding minute, a contract that appears in most blocks costs two honest peers a strike per block; a node's announce-known WIT2 peers are a small set, so it disconnects and jails its own witness sources within a minute and then has none to fetch from. The same branch also fires on errors that have nothing to do with the witness: chain shutdown mid-import ( To be clear, #2401 does not cause this; before it, the same local gap surfaced as a gas-used or state-root mismatch and would have been charged just the same, only indistinguishably. #2401 is what makes the case cheap to exclude. Suggested fix: gate the charge on witness-attributable failures. Return CI gates still red on this head
On the e2e failure (now green on retry): it was Test 5 (load test with producer rotation), where heimdall rotated the span on 1 of 3 netem windows instead of 3 of 3. No witness, strike, or exclusion warnings appeared in the collected container logs, and the same commit passed on re-run, so I read it as the test's timing sensitivity rather than this change. The |
|
Reviewed the two new commits. Everything from the earlier rounds looks addressed — I re-checked the saturating band math, the announce-time One gap in the new code, in the half of The relaxed broadcast path accepts divergent bodies for import but cannot charge them
So for a broadcast-delivered witness, if op.witness == nil || !op.witnessDiverged || op.witnessPeer == "" {
return false
}A within-band, non-BP-identical witness that arrives by Two things make the asymmetry worth closing rather than accepting:
If the Suggested fix. The provenance is already at hand: Two smaller ones, neither blocking
|
…s could have caused it chargeDivergedWitnessImportFailure struck and excluded the serving peer on any insertChain error. "Diverged" is the normal case on a stateless node (every node persists its own generated witness), so every import failure — including a contract bytecode missing from local disk, which the downloader heals and the witness never carried, or an interrupted insert — would have struck two honest witness sources per block until the node had none left. Gate the charge on a positive allowlist of witness-attributable failures: a missing trie node (incomplete witness), and execution against the witness disagreeing with the header (ErrStatelessStateRootMismatch, ErrGasUsedMismatch, ErrReceiptRootMismatch, ErrBloomMismatch, ErrRequestsHashMismatch, and the fmt-built "invalid merkle root" / stateless self-validation mismatch messages). Everything else — missing code, interrupted or stopped chain, whitelist mismatch, header or DB errors, unknown errors — is not charged. Also: drop the unreachable absBytes == 0 guard in acceptableWitnessSizeCeiling (the page threshold floors at one page); make the deferred-broadcast hash-match test use a size band that would reject the body, so only the hash match admits it; cover the handler size-oracle helpers without a fetcher and the deferred size-band binding's boundary and TTL. Tests: TestChargeDivergedWitnessImportFailureIgnoresNonWitnessErrors, TestIsWitnessAttributableImportError, TestSizeOracleHelpersWithoutFetcher, TestDeferredAnnounceCacheHasWitnessSizeWithin; existing charge/re-fetch tests now use attributable errors.
…/wit2-size-oracle
…ssing from local disk #2401 surfaces a missing contract bytecode on the stateless path as core.ErrStatelessIncompleteState wrapping a *state.MissingCodeError, the same sentinel that wraps a missing trie node. Witnesses carry no code, so the server could not have supplied it and the downloader's self-heal fetches the blob; charging the server would strike and exclude honest witness sources on every block that touches the contract until the node had none left. Check for *state.MissingCodeError first in isWitnessAttributableImportError and return false explicitly, so the wrapped cause — not the shared sentinel — decides attribution: missing node → incomplete witness → charged; missing code → local gap → not charged; the bare sentinel → cause unknown → not charged. Tests: the wrapped MissingCodeError case in TestChargeDivergedWitnessImportFailureIgnoresNonWitnessErrors (no strike, no exclusion, no re-fetch, no budget consumed) and both wrappings of ErrStatelessIncompleteState in TestIsWitnessAttributableImportError.
…ranches Exercise AcceptableWitnessSizeCeiling from its own package (the handler is its only production caller) and the two retryAfterImportFailure early exits — block already known locally, witness marked unavailable — so the new code is fully covered where it lives.
…he sampled mutation survivors Diffguard flagged importBlocks at complexity 21 (threshold 15) after the witness-retry plumbing. Move the goroutine body into runBlockImport, which returns whether the failed import should be retried with a witness from another peer, and the block-tracker log into logTrackedImport; importBlocks now only routes the result to witnessRetry or done. Behaviour is unchanged. Pin the five sampled survivors: assert the diverged flag on both the exact-match and divergent returns of verifyAgainstSignedHash and on the WIT1-only (nil lookup) return; add the exact-boundary case to the saturating multiply (MaxUint64/2 * 2 must multiply, not saturate); and pin that the fetcher's striker and source-excluder callbacks are wired into the handler (StrikeWitnessServer / ExcludeWitnessSource are exported for that test).
|
Addressed in b0f0dff, 95e3f58 and 62954f9 (head 62954f9; Import-failure charge is now gated on witness-attributable errors.
Tests: CI gates.
Also from the first pass: unreachable Devnet: the mixed old/new WIT2 run you suggested is in progress; results will follow in a separate comment. |
|
Mixed old/new WIT2 devnet — results Ran the mixed-version window on a 9-node kurtosis devnet, then upgraded the old nodes in place and kept sampling. Setup (full p2p mesh, so every old↔new pair exists): 4 validators + 2 full-sync relays + 3 stateless ( Phase 1 — mixed window (blocks 205→300)
Phase 2 — after upgrading nodes 3/6/8 in place (blocks 332→428)
Size oracle under load — from a first attempt of the same run (that attempt ran Artifacts (params, two-phase runner, per-phase CSV samples, analyzer, generated summary, first-attempt evidence) are in my |
… quarantine clear and announce conflict Review follow-ups (pratikspatil024, 2026-09-21). The broadcast path accepted a within-band divergent body for import but could not charge it: handleBroadcast set only op.witness, and both cached-witness attach sites rebuilt the op without provenance, so chargeDivergedWitnessImportFailure returned on its first guard and a NewWitness push was the free way to deliver unusable bytes — and the easier one, since the first witness to arrive is the one attached. acceptSignedBroadcast/acceptDeferredBroadcast now report the divergence, InjectWitness carries it with the pusher, handleBroadcast records witnessPeer/witnessDiverged and the announce's fetch closure, and the before-the-block cache keeps peer+diverged so both attach sites build the op via cachedWitness.injectFor with full provenance. An import failure of pushed bytes now strikes and excludes the pusher and re-fetches from another peer, as on the fetch path; a pusher excluded for a block has its further pushes for that block dropped (broadcast_excluded_source_drop) so it cannot beat every honest re-fetch with the same body. verifyAgainstSignedHash cleared the oversize/quarantine state on every in-band acceptance; a divergent in-band body proves nothing about the signed size, so a server alternating oversized and in-band bodies could keep a block out of quarantine indefinitely. Only BP-identical bytes clear it now (the pending-removal exits and TTL sweep still bound the maps). signedWitnessCache.putIfNewer keyed conflicts on WitnessHash alone; the signed WitnessSize decides the accept band, so a same-hash announce with another size is now a conflict too (first commitment wins) instead of replacing the band once the relay window lapses. Tests: push-path import-failure e2e for both attach sites through the real fetcher loop, provenance bit from both accept functions, excluded pusher dropped, quarantine kept across a divergent acceptance, size conflict rejected before and after the relay window.
|
Addressed in 1. Push path could not charge a divergent body — fixed. Confirmed:
2. 3. Build, vet, |
Diffguard's file-size gate allows a file already over 800 lines to grow by 10% of its base size; block_fetcher.go had reached +138 (10.4%) on the candidate base. chargeDivergedWitnessImportFailure and maxWitnessImportRetries move to witness_import_errors.go, which already holds isWitnessAttributableImportError, the predicate the charge is gated on. No behaviour change.
|
@claude review |
There was a problem hiding this comment.
Approving.
Re-checked the three items from my earlier comment against a4d7a6aeb:
- Push-path provenance:
handleBroadcastrecordswitnessPeer/witnessDiverged,cachedWitnesscarries both through to the attach sites, andInjectWitnesstakes the bit from both accept functions — including thehandleInjectedBlock/handleFilterResultsites I had missed. A divergent pushed body that fails import is now charged to the pusher. - The quarantine clear is reached only on a byte-identical match.
putIfNewertreats a same-hash / different-size announce as a conflict.
Also read through isWitnessAttributableImportError. Checking *state.MissingCodeError ahead of the ErrStatelessIncompleteState sentinel is the right ordering given that sentinel wraps both a missing node and missing code, and defaulting unknown errors to "not charged" fails on the safe side.
go test ./eth/fetcher/ ./eth/protocols/wit/ and the eth witness subset pass locally on this head.
One rollout condition, not a code change. The devnet run puts a number on the mixed-version window: old nodes held 2–3 peers instead of 8 while both formats were live on wit/3, returning to 8/8 only after the in-place upgrade. On Amoy that lands on the v2.10.2-preconf* nodes, one of which produces. Please coordinate with whoever owns those hosts so every WIT2-capable Amoy node is upgraded in a single window rather than piecemeal. The description already says this — I'm attaching it to the approval so it doesn't get lost between merge and deploy.
What
WIT2 used the BP-signed witness hash as an exact-match identity: a serving peer whose witness bytes hash differently from the signed
WitnessHashwas rejected and struck, and two such servers fell the block back to WIT1. That assumes witnesses are byte-identical across nodes.They are not. BlockSTM speculative reads make honest nodes collect different-but-valid trie-node sets, so a valid witness routinely hashes differently (observed ±5–9% node-set spread — e.g. one mainnet block seen at 12,551 / 13,194 / 13,688 nodes), and every node persists its own generated witness, so nearly every non-BP server's bytes differ from the BP's. The exact-hash gate therefore rejected valid witnesses and struck the honest peers serving them.
This PR replaces exact-hash equality with a size oracle.
How
SignedWitnessAnnouncementgainsWitnessSize; the announce signing pre-image commits to it:keccak(domain ‖ blockHash ‖ blockNumber ‖ witnessHash ‖ witnessSize). Producers set it from their own witness length. At accept time aWitnessSizeof zero or above the gas-derived absolute witness cap is refused and the sender struck (before deferral, so it never enters the deferred queue or the signed cache): a bad size is the signer's or forwarding relayer's doing, not the servers' it would later mis-judge. Because the size now decides the accept band, the signed-announce cache treats a same-hash announcement with anotherWitnessSizeas a conflict like another hash (first commitment wins), so a second signature cannot move the band under an in-flight fetch.min(3×signedSize, absolute cap)is accepted for import regardless of hash — on the paged-fetch path (verifyAgainstSignedHash) and, with the same rule, on the broadcast paths (acceptSignedBroadcast,acceptDeferredBroadcast), so a peer pushing its own post-import witness to a waiter is no longer rejected downstream. Import-time state-root execution remains the content arbiter. The band math saturates and a zero signed size falls back to the absolute cap, so no input can collapse the ceiling to zero.fetchAndVerifyWitness) keeps requiring the BP's bytes by design and is now documented as only succeeding against the producer or a node still holding the producer's bytes in its own pre-import cache; waiters otherwise fall back to the pull path.verifyAgainstSignedHashreportsdiverged; the fetcher now enqueues the manager's op instead of rebuilding it, which had dropped that provenance), and oninsertChainfailureimportBlocksstrikes the server, excludes it as a witness source for that block (SetWitnessSourceExcluder→ handlerwitnessSourceExclusions, honoured byresolveWitnessFetchPeerat every tier and released on import), and hands the block back to the witness manager to re-fetch from another peer, bounded bymaxWitnessImportRetries = 2so a genuinely invalid block cannot cycle the peer set. A BP-identical witness that fails import is still the BP's fault and is handled as before (logged, forgotten). Strikes are thresholded (5 per sliding minute), so an honest server hit by a bad block is not disconnected.NewWitnessbroadcast:acceptSignedBroadcast/acceptDeferredBroadcastreport the divergence,InjectWitnesscarries it with the pusher intohandleBroadcastand the before-the-block witness cache, and both attach sites record pusher, divergence and fetch closure on the op. An import failure of pushed bytes therefore strikes and excludes the pusher and re-fetches from another peer exactly like the fetch path, and a pusher excluded for a block has its further pushes for that block dropped (eth/wit2/serve/broadcast_excluded_source_drop) so it cannot beat every honest re-fetch with the same body.ErrStatelessStateRootMismatch,ErrGasUsedMismatch,ErrReceiptRootMismatch,ErrBloomMismatch,ErrRequestsHashMismatch, and the fmt-built "invalid merkle root" / stateless self-validation mismatch messages). A contract bytecode missing from local disk — which core, eth: detect and self-heal missing contract code on stateless verification #2401 now surfaces asErrStatelessIncompleteStatewrapping*state.MissingCodeError, and which the downloader heals — is explicitly excluded (errors.Ason the wrapped cause, since the same sentinel wraps a missing trie node), as are interrupted or stopped inserts, whitelist mismatches, header/DB errors and unknown errors. Without the gate, a missing bytecode would have struck and excluded two honest witness sources per block until the node had none left.Rollout
The signed-announce wire format changes in place, and this does not land on a clean slate: the WIT2 commit is in
v2.10.2-beta,v2.10.2-preconfandv2.10.2-preconf2, and preconf builds are running on some Amoy nodes. Old and new builds both advertisewit/3, so an old-formatSignedNewWitnessHashesMsgfails RLP decode on a new node (and vice versa once a new-format BP exists), the handler returns the error and the p2p layer drops the connection. No jail is involved; redial is gated only by the dial-history / inbound-throttle timers, so the churn is self-limiting and stops once every WIT2 node runs the same format.-beta/-preconftags are superseded by this format.wit/4bump was considered and not done: a relay cannot convert a BP-signed size-bearing announce into the legacy pre-image for awit/3peer, so a dual-format window would buy little beyond avoiding the self-limiting reconnect churn. If zero churn is required, that is a separate follow-up.Scope
WIT2 signed-path only. The WIT1 page-count check was #2417 (merged). The branch has
origin/v2.10.2-candidatemerged in (for #2401's typedMissingCodeError), so it carries #2417 and #2401.Metrics
New:
eth/wit2/announce/implausible_size,eth/wit2/serve/broadcast_oversize,eth/wit2/serve/broadcast_hash_divergence,eth/wit2/serve/broadcast_excluded_source_drop,eth/fetcher/witness/oversized,eth/fetcher/witness/hash_divergence,eth/fetcher/witness/import_failure,eth/fetcher/witness/import_retry. Removed:eth/wit2/serve/broadcast_byte_mismatch,eth/fetcher/witness/byte_mismatch(no longer a rejection reason).Tests
eth/protocols/wit: signing digest now coverswitnessSize(stability, domain-separation, per-field sensitivity incl. a new size case).eth/fetcher:body=nil), and not struck (TestVerifyAgainstSignedHashAcceptsDivergentHashWithinBand,TestProcessWitnessResponseDoesNotDropOnByteMismatch); exact match is served (TestVerifyAgainstSignedHashServesOnExactMatch);min(3×S, absolute)incl. the implausibly-large clamp, plus the degenerate inputs:S=0→ absolute cap,S≈MaxUint64saturates (TestAcceptableWitnessSizeCeiling*,TestSaturatingMulUint64);TestImportFailureWithDivergedWitnessRefetchesFromAnotherPeerdrives the real fetcher loop — first import fails with a state-root mismatch, server struck and excluded once, witness re-fetched from a second peer, block imports;TestChargeDivergedWitnessImportFailure(only fetched+diverged is charged, retry budget honoured),TestChargeDivergedWitnessImportFailureIgnoresNonWitnessErrors(a wrapped*state.MissingCodeError, interrupted/stopped inserts,whitelist.ErrMismatch, unknown errors: no strike, no exclusion, no re-fetch, no budget consumed),TestIsWitnessAttributableImportError(the allowlist, incl.ErrStatelessIncompleteStatewrapping a missing node → attributable vs. wrapping missing code → not),TestRetryAfterImportFailureReRegistersPending; push path:TestImportFailureWithDivergedBroadcastWitnessChargesPusher(body cached before its block) andTestImportFailureWithDivergedBroadcastWitnessOnPendingBlockChargesPusher(body attached to a pending block) drive the real fetcher loop — the pusher, not a fetch server, is struck and excluded once, the witness re-fetched, the block imports;TestVerifyAgainstSignedHashDivergentInBandKeepsMismatchState(only an exact match clears the oversize count).eth:TestAcceptSignedAnnouncementRejectsImplausibleWitnessSize(0 and cap+1 refused and struck, not deferred; cap accepted);TestHandleWitnessBroadcastDivergentWithinBandImportsWithoutServing,TestHandleWitnessBroadcastOversizeDropped, and the deferred-path variant/oversize cases inTestHandleWitnessBroadcastAcceptedWhileAnnounceDeferred;TestAcceptSignedBroadcastReportsDivergence/TestAcceptDeferredBroadcastReportsDivergence(the provenance bit handed to the fetcher),TestHandleWitnessBroadcastDivergentBodyImportFailureChargesPusher(wire handler → push → cached body → block → attributable import failure → the pusher struck and excluded, witness re-fetched, block imported),TestHandleWitnessBroadcastDropsExcludedSource,TestSignedWitnessCacheRejectsConflictingWitnessSize(rejected before and after the relay window);TestResolveWitnessFetchPeerSkipsExcludedSource(excluded at every tier, per block, released on import),TestWitnessSourceExclusionSetLifecycle,TestSizeOracleHelpersWithoutFetcher,TestDeferredAnnounceCacheHasWitnessSizeWithin(inclusive boundary, TTL).ethwitness/peer suites,eth/fetcher,eth/protocols/witpass;golangci-lint run ./eth/...clean.Test plan
go build ./...go vet ./eth/...golangci-lint run ./eth/...(0 issues)go test ./eth/fetcher/ ./eth/protocols/wit/go test -race ./eth/fetcher/ -run 'ImportFailureWithDiverged|ChargeDiverged|RetryAfterImportFailure' -count=3go test ./eth/ -run 'Wit2|Witness|SignedWitness|Announce|Broadcast|Relay|Deferred|PeerSet|Peer'verifyAgainstSignedHashreturn, striker/excluder wiring): 37/37 mutants killed.importBlockssplit intorunBlockImport+logTrackedImportto stay under the complexity threshold. New functions are 100% line-covered in their own packages except the pre-existing encode-failure and shutdown branches.v2.10.2-preconf2↔ this PR, full mesh) followed by an in-place upgrade of the old nodes — self-limiting redial churn, 0 jails / strikes / charges, churn stops after the upgrade; results in eth/protocols/wit, eth: WIT2 size oracle — accept non-deterministic witnesses within a signed size band #2416 (comment)