Repository navigation
Nimbus Validator timeout issues with Obol Charon #4675
Description
Summary
On a steady-state 4-node cluster (hours of uptime, no restarts), a subset of GET /eth/v1/validator/attestation_data requests from the Nimbus validator client are never answered by Charon. They hang until the VC's own client-side timeout cancels them (~4.0s, occasionally ~10.0s for aggregate_beacon_committee_selections), rather than Charon responding or erroring on its own. A packet capture of the relevant beacon-node call in one such window shows the beacon node itself responding in ~30ms, so the request is not waiting on the beacon node. Two pprof goroutine dumps, taken ~16 minutes apart during separate live hangs, both show the request parked inside DutyDB.AwaitAttestation. This reproduces repeatedly (roughly 15-25% of attester duties in the observed windows) and is not limited to a startup window — the two goroutine dumps were both taken well after the cluster had settled.
Charon version
v1.10.3 (obolnetwork/charon/v1.10.3-e60c838, amd64-linux)
Environment
- 4-node DV cluster
- Consensus protocol: QBFT v2.0
- Beacon node behind Charon: Nimbus BN
v26.7.0andv26.8.0 - Validator client: Nimbus VC
v26.7.0andv26.8.0 - CPU, memory, and disk I/O all nominal throughout the observation window
(see Metrics below)
Expected behavior
attestation_data should be answered within tens of milliseconds, as it is for the majority of duties — for example:
11:57:15.000 DEBG qbft QBFT consensus instance starting {"duty": "15119384/attester", ...}
11:57:15.017 DEBG qbft QBFT upon rule triggered {"rule": "justified_pre_prepare", "round": 1, "duty": "15119384/attester"}
11:57:15.020 DEBG qbft QBFT upon rule triggered {"rule": "quorum_prepares", "round": 1, "duty": "15119384/attester"}
11:57:15.046 DEBG qbft QBFT upon rule triggered {"rule": "quorum_commits", "round": 1, "duty": "15119384/attester"}
11:57:15.046 DEBG qbft QBFT consensus decided {"duty": "attester", "slot": 15119384, "round": 1, ...}
11:57:15.080 INFO bcast Successfully submitted v2 attestations to beacon node {"delay": "80.726095ms", "duty": "15119384/attester", ...}
Actual behavior
For the affected duties, no qbft component log line appears at all for that duty — no consensus start, no round activity — while the duties immediately before and after it in the same log complete normally as above. The corresponding attestation_data request instead sits until the VC cancels it:
DEBG vapi Validator api 4xx response {"status_code": 408, "message": "client cancelled request",
"error": "api error[status=408,msg=client cancelled request]: context canceled",
"duration": "4.003078158s", "vapi_endpoint": "attestation_data"}
Evidence
General timing pattern
Across every observed instance (failing and succeeding duties alike), Charon issues GET /eth/v1/beacon/blocks/head/root to the upstream beacon node a consistent ~2.00s after the SSE head event for that slot. For failing duties, this call is the last thing logged before the VC's request is cancelled 4.00-4.05s later; for succeeding duties, consensus has already decided before this call fires. Three concrete, timestamp-correlated instances are below.
Instance 1 — packet capture
Charon debug log (all times CEST, UTC+2):
Sep 01 11:33:26 eth charon[40072]: 11:33:26.650 DEBG vapi Proxying request to beacon node {"url": "http://127.0.0.1:5052", "vapi_proxy_method": "GET", "vapi_proxy_path": "/eth/v1/beacon/blocks/head/root"}
Sep 01 11:33:30 eth charon[40072]: 11:33:30.652 DEBG vapi Validator api 4xx response {"status_code": 408, "message": "client cancelled request", "error": "api error[status=408,msg=client cancelled request]: context canceled", "duration": "4.00201019s", "vapi_endpoint": "attestation_data"}
tcpdump of the same call (frame times are UTC, i.e. 2 hours behind the CEST log timestamps above — 09:33:26.65 in the
capture is the same instant as 11:33:26.650 in the log):
No. Time Source Destination Protocol Length Info
459 09:33:26.650408 127.0.0.1 127.0.0.1 TCP 74 36352 -> 5052 [SYN] ...
460 09:33:26.650419 127.0.0.1 127.0.0.1 TCP 74 5052 -> 36352 [SYN, ACK] ...
461 09:33:26.650427 127.0.0.1 127.0.0.1 TCP 66 36352 -> 5052 [ACK] ...
462 09:33:26.650540 127.0.0.1 127.0.0.1 HTTP 217 GET /eth/v1/beacon/blocks/head/root HTTP/1.1
463 09:33:26.650547 127.0.0.1 127.0.0.1 TCP 66 5052 -> 36352 [ACK] ...
464 09:33:26.680164 127.0.0.1 127.0.0.1 TCP 235 5052 -> 36352 [PSH, ACK] [TCP PDU reassembled in 466]
465 09:33:26.680172 127.0.0.1 127.0.0.1 TCP 66 36352 -> 5052 [ACK] ...
466 09:33:26.680180 127.0.0.1 127.0.0.1 HTTP/JSON 199 HTTP/1.1 200 OK , JSON (application/json)
467 09:33:26.680183 127.0.0.1 127.0.0.1 TCP 66 36352 -> 5052 [ACK] ...
468 09:33:26.680192 127.0.0.1 127.0.0.1 TCP 66 5052 -> 36352 [FIN, ACK] ...
469 09:33:26.680276 127.0.0.1 127.0.0.1 TCP 66 36352 -> 5052 [FIN, ACK] ...
470 09:33:26.680292 127.0.0.1 127.0.0.1 TCP 66 5052 -> 36352 [ACK] ...
The SYN at frame 459 (09:33:26.650408) lines up with the Proxying request log line at 11:33:26.650. The full TCP exchange — connect, request, 200 OK JSON response, teardown — completes by 09:33:26.680292, roughly 30ms after it started. The attestation_data request is not cancelled until 11:33:30.652, exactly 4.00201019s after the Proxying request log line.
Instance 2 — pprof dump correlated with log, duty 15119321
Charon debug log:
11:44:38.056 DEBG app-start SSE head event {"slot": 15119321, ...}
11:44:40.058 DEBG vapi Proxying request to beacon node {"url": "http://127.0.0.1:5052", "vapi_proxy_method": "GET", "vapi_proxy_path": "/eth/v1/beacon/blocks/head/root"}
11:44:44.061 DEBG vapi Validator api 4xx response {"status_code": 408, "duration": "4.003078158s", "vapi_endpoint": "attestation_data"}
11:44:45.009 DEBG vapi Validator api 4xx response {"status_code": 408, "duration": "10.000526116s", "vapi_endpoint": "aggregate_beacon_committee_selections"}
A pprof goroutine dump was taken at 11:44:41 — i.e. during the hang, between the Proxying request line and the eventual 408:
$ date && curl http://127.0.0.1:3700/debug/pprof/goroutine?debug=2
Tue Sep 1 11:44:41 CEST 2026
...
goroutine 3642 [select]:
github.com/obolnetwork/charon/core/dutydb.(*MemDB).AwaitAttestation(0x3c0fc45c3b00, {0x21f49c8, 0x3c0fc4c8fa40}, 0xe6b3d9, 0x0)
/workspace/core/dutydb/memory.go:198 +0x527
github.com/obolnetwork/charon/core/validatorapi.Component.AttestationData({...}, ...)
/workspace/core/validatorapi/validatorapi.go:266 +0x24f
github.com/obolnetwork/charon/core/validatorapi.NewRouter.attestationData.func5(...)
/workspace/core/validatorapi/router.go:524 +0xb9
...
0xe6b3d9 = decimal 15119321, i.e. this goroutine is the exact request shown timing out in the log above.
The same dump also shows a second request, for aggregate_beacon_committee_selections, blocked the same way in a different
component:
goroutine 3424 [select]:
github.com/obolnetwork/charon/core/aggsigdb.(*MemDB).Await(0x3c0fc4a1cba0, {0x21f49c8, 0x3c0fc5501950}, {0x3c0fc52631f0?, 0x0?}, {0x3c0fc52631f0, 0x62})
/workspace/core/aggsigdb/memory.go:106 +0x308
github.com/obolnetwork/charon/core/validatorapi.Component.BeaconCommitteeSelections({...}, ...)
/workspace/core/validatorapi/validatorapi.go:793 +0x827
...
And two further goroutines, for earlier slots, blocked in the same AggSigDB.Await call but with no live HTTP request attached — these were spawned via the async-retry wrapper:
goroutine 1056 [select, 1 minutes]:
github.com/obolnetwork/charon/core/aggsigdb.(*MemDB).Await(...)
/workspace/core/aggsigdb/memory.go:106 +0x308
github.com/obolnetwork/charon/core/fetcher.(*Fetcher).fetchAggregatorData(...)
/workspace/core/fetcher/fetcher.go:291 +0x2ed
github.com/obolnetwork/charon/core/fetcher.(*Fetcher).Fetch(...)
/workspace/core/fetcher/fetcher.go:159 +0xa67
github.com/obolnetwork/charon/app.wireCoreWorkflow.WithAsyncRetry.func12.1.1(...)
/workspace/core/retry.go:17 +0x32
github.com/obolnetwork/charon/app/retry.(*Retryer[...]).DoAsync(...)
/workspace/app/retry/retry.go:135 +0x72e
...
goroutine 2668 [select]:
(identical stack, different slot)
Instance 3 — second pprof dump, ~16 minutes later, duty 15119385
Charon debug log:
11:57:26.421 DEBG app-start SSE head event {"slot": 15119385, ...}
11:57:28.422 DEBG vapi Proxying request to beacon node {"url": "http://127.0.0.1:5052", "vapi_proxy_method": "GET", "vapi_proxy_path": "/eth/v1/beacon/blocks/head/root"}
11:57:32.425 DEBG vapi Validator api 4xx response {"status_code": 408, "duration": "4.003295409s", "vapi_endpoint": "attestation_data"}
pprof dump taken at 11:57:30, again during the hang:
$ date && curl http://127.0.0.1:3700/debug/pprof/goroutine?debug=2
Tue Sep 1 11:57:30 CEST 2026
...
goroutine 34489 [select]:
github.com/obolnetwork/charon/core/dutydb.(*MemDB).AwaitAttestation(0x3c0fc45c3b00, {0x21f49c8, 0x3c0fc576b7a0}, 0xe6b419, 0x0)
/workspace/core/dutydb/memory.go:198 +0x527
github.com/obolnetwork/charon/core/validatorapi.Component.AttestationData({...}, ...)
/workspace/core/validatorapi/validatorapi.go:266 +0x24f
...
0xe6b419 = decimal 15119385, again matching the request shown timing out above. Duties 15119384 and 15119386 (immediately before and after) both completed normally, as shown in the "Expected behavior" section.
Metrics reviewed
/metrics on the beacon node, captured during an active hang window:
ticks_delay(time to run the beacon node'sonSecondloop; nominal is
~1.0):1.000878542- Per-block processing fields logged alongside each
Block resolvedline
(sigVerifyDur,stateVerifyDur,stateDataDur,putBlockDur):
consistently ~10-30ms across both successful and failing slots, with no
correlation to which duties fail process_cpu_seconds_total,nim_gc_mem_bytes,
sqlite3_memory_used_bytes,db_checkpoint_seconds_total: nothing
anomalous in the captured snapshot
What we've ruled out
- Slow response from the upstream beacon node — Instance 1's packet
capture shows the specific call Charon makes to the beacon node completing
in ~30ms. - Network/loopback issues — same packet capture: clean SYN/ACK, request,
200 OK, and teardown with no retransmissions. - Host resource exhaustion — CPU, disk I/O, and memory were all normal
during the observation windows; the beacon node's own event-loop-lag metric
(ticks_delay) stayed at its nominal ~1.0 value. - Other VC — Seems to only happen using Nimbus VC
Additional info
- We have a live, currently-affected v1.10.3 cluster and can gather further
debug logs, additional pprof dumps (CPU, block, or mutex profiles), or test
a patched/instrumented build if that would help. - Happy to attach the complete raw logs and full goroutine dumps (trimmed
above for readability) if useful. - If this is already addressed in a later release, a pointer to
the relevant change would be appreciated so we can upgrade and confirm.
Activity
KaloyanTanev commentedon Sep 8, 2026
Thanks for reporting this @thoutenbos! Let me shed some light on how Charon behaves on
attestation_datarequest from the VC:Having this flow in mind, it seems like steps 1 and 3 are not even started in Charon, which likely means Charon hasn't even scheduled those duties and hasn't initiated a fetch or QBFT instance on first place.
/eth/v1/validator/duties/attester/{epoch}requests failing in the metrics (likely seen asattesterDutiesthroughout Charon)? Or some errors with the duties cache in the logs / metrics?cluster editcommands?validator_keysdirectory matches yourcluster_lock.jsonin terms of amount of keys?thoutenbos commentedon Sep 9, 2026
Thanks for the detailed breakdown of the request flow — that's very helpful context. Answers below, plus one new finding from correlating timestamps across the log lines.
1. All nodes or only some?
One other member had the same behaviour when using Nimbus VC but it was resolved by switching it to Lodestar VC.
2. Specific validators?
No — architecturally this shouldn't be validator-specific.
attestation_datais scoped to (slot, committee_index), not to a validator pubkey, and the 408 cancellation log line itself carries no pubkey or committee_index — juststatus_code,duration,vapi_endpoint. Every duty in a failing slot shares the same underlying fetch.3.
/eth/v1/validator/duties/attester/{epoch}failures or duties-cache errors?vapi_endpoint: duties*log lines in the last 24h, and no errors tied to duty scheduling.Reorg occurred through epoch transition, invalidating duties cache(DEBG, expected behavior) a few times a day — timing doesn't correlate with the steady hourly cancellation rate, so this doesn't look like the driver.4. Cluster edit commands run?
No — confirmed, no
cluster editor other cluster-modifying commands have been run since setup.5.
validator_keys/vscluster_lock.jsonkey count?Confirmed matching.
New finding: timing correlation across ~20 consecutive slots
I pulled every
Slot ticked/SSE head event/Proxying request to beacon node {blocks/head/root}/QBFT consensus decided/Validator api 4xx responseline for a clean 5-minute window (slots 15178348-15178367, 5 of the 20 slots failed) and lined them up by timestamp. Two things stood out:a) The
blocks/head/rootproxy call fires at a fixedhead_event + 2.000-2.002soffset, every single slot, success or failure alike — not "4s into the slot," and not tied to when the head event itself arrived (head event delay varied from 1.4s to 3.5s after slot tick across the sample; the +2.000s gap to the proxy call was constant regardless).b) For every one of the 5 failing slots in this sample,
cancel_time − duration(the implied start of the request Charon is measuring) lands within ~1ms of that sameblocks/head/rootproxy-call timestamp — not the slot tick. Example (slot 15178351):No
QBFT consensus instance startingline, noQBFT consensus decided, nothing else for that duty at all — matches your working theory that steps 1 and 3 never got underway. This held for all 5 failures in the sample (15178351, 352, 357, 358, 361) — none show a subsequent QBFT decision on this node.Happy to pull more samples, raise the log level, or grab another pprof dump around a live hang if useful — let me know what would help narrow it down further.
thoutenbos commentedon Sep 14, 2026
Update to Charon v1.11.0 and the issue remained the same
Replacing Nimbus VC with Lighthouse VC and the issue disappears
KaloyanTanev commentedon Sep 15, 2026
Can I ask you for Nimbus VC, Charon and your BN's logs for a period of 2 epochs, where those issues are apparent? If fine on your end, you can send them over in our Discord server, as I'm not sure a github issue will be able to accommodate the size of the files.
b0a7 commentedon Oct 8, 2026
might be related to status-im/nimbus-eth2#8962 ?
KaloyanTanev commentedon Oct 9, 2026
Highly likely yes. From my observations it was the case that Nimbus was falsely sending attestation duties, irregardless if it had anything scheduled or not.