Description
Anchor can skip a locally assigned proposal when the pinned Lighthouse proposer-duty timer wakes at a slot boundary but the Lighthouse slot clock still returns the previous slot.
This was observed on one Anchor operator during a mixed 2 Anchor plus 2 go-ssv stress run. The affected operator processed slot 78 again, sent a stale slot-78 notification, and then slept until the next boundary without ever checking slot 79. The other three operators reached the 3-of-4 threshold and produced canonical block 79.
The bug is most likely owned by Lighthouse validator_services. It is tracked here because Anchor pins and embeds that code, needs a dependency integration path, and can ship a temporary patched Lighthouse revision if required. The observation occurred before the Boole activation in this run, so the failure is not Boole-specific.
Version
- Anchor
unstable: fd3c7f9677b2bbb04fe7d7c2d4eca81313db7b79
- Anchor Lighthouse pin:
b263df596671a2bd42bf1034e1cdc8188ba8a9b0
- Lighthouse current
unstable checked on 2026-08-06: 02e8732a81a68784ce105609942570470a7989f2, with the same scheduler behavior still present
- ssv-mini:
fef3b7d791384911078b2ce977100f87679e5c46
- go-ssv
stage: 874ba316266aec6215bc21c6e2e929e195f08d6b
- Project Docker Rust toolchain:
1.93.1
- Runtime image:
node/anchor:boole-proposer-diagnostic-fd3c7f9-b263df5
- Image ID:
sha256:5a459c08cf1d8fc3d6a4b06f76249c6273f07c1d782f2be5a38d6c288a2aa7ee
- Anchor binary SHA-256:
41c7addc3ef97b8be130e0e34690b43c05fa03714114209c9ed1c108d13303d3
Anchor's pin is visible in Cargo.toml.
Test topology
- 2 Anchor operators, operator IDs 1 and 2
- 2 go-ssv operators, operator IDs 3 and 4
- 10 managed validators, indices 64 through 73
- 3-of-4 threshold
- 74 total preregistered validators
- Fulu at genesis
- Boole at epoch 6
- 12-second slots
- Enclave:
boole-proposer-diagnostic-1
- Enclave UUID:
1ed99ef7ef154ddba6dc20a9c1b6e731
Present Behaviour
Validator index 64, pubkey:
0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416
was assigned to propose at slot 79.
Anchor operator 2 logged:
2026-08-06T23:36:30.002481Z DEBUG Checked cached block proposers slot=78 epoch=2 epoch_cached=true exact_slot_cached_count=1 eligible_proposer_count=1
2026-08-06T23:36:30.045650Z DEBUG Sent block production notification current_slot=78 proposer_count=1
2026-08-06T23:36:30.045791Z DEBUG Received block production notification notification_slot=78
2026-08-06T23:36:30.045812Z WARN Skipping block production for expired slot current_slot=79 notification_slot=78 info="Your machine could be overloaded"
2026-08-06T23:36:30.091232Z DEBUG Downloaded proposer duties epoch=2 num_relevant_duties=7
2026-08-06T23:36:30.095532Z DEBUG Downloaded proposer duties epoch=3 num_relevant_duties=1
2026-08-06T23:36:30.095688Z DEBUG Checked cached block proposers slot=78 epoch=2 epoch_cached=true exact_slot_cached_count=1 eligible_proposer_count=1
There was no slot-79 proposer-cache lookup, slot-79 notification, RANDAO entry, unsigned-block request, or proposal publication on this operator.
For comparison, Anchor operator 1 entered the correct path:
2026-08-06T23:36:30.016695Z DEBUG Checked cached block proposers slot=79 epoch=2 epoch_cached=true exact_slot_cached_count=1 eligible_proposer_count=1
2026-08-06T23:36:30.016750Z DEBUG Sent block production notification current_slot=79 proposer_count=1
2026-08-06T23:36:30.018371Z DEBUG Received block production notification notification_slot=79
2026-08-06T23:36:30.019433Z TRACE Proposer randao reveal entered slot=79
2026-08-06T23:36:30.266510Z TRACE checkpoint="randao_reveal_completed" outcome="success" slot=79
2026-08-06T23:36:30.266529Z INFO Requesting unsigned block slot=79
2026-08-06T23:36:30.492848Z INFO Received unsigned block slot=79
2026-08-06T23:36:30.970861Z DEBUG New head from beacon node block_root=0xaeca1f6a53a616e3eee636a55454a2acf87e5db2044153dfe253bddd2a4258d7 slot=79
go-ssv operator 4 independently showed that its boundary estimator still considered the current slot to be 78, then completed slot 79 successfully:
{"time":"2026-08-06T23:36:30.009266Z","msg":"duty did not complete before slot end (likely stuck)","runner_role":"PROPOSER","slot":79,"estimated_current_slot":"78"}
{"time":"2026-08-06T23:36:31.008601Z","msg":"successfully submitted block proposal","runner_role":"PROPOSER","slot":79,"estimated_current_slot":"79","block_root":"aeca1f6a53a616e3eee636a55454a2acf87e5db2044153dfe253bddd2a4258d7"}
The warning's overload explanation is not supported by the resource sample at 23:36:31Z:
| Service |
CPU |
Memory |
Restarts |
OOM |
| Anchor operator 1 |
1.31% |
42,561,700 bytes |
0 |
false |
| Anchor operator 2 |
0.36% |
43,830,477 bytes |
0 |
false |
There were no monitor alerts at this sample.
Failure mechanism
The pinned Lighthouse proposer task does this once per loop:
- Call
duration_to_next_slot().
- Sleep for that duration.
- Immediately call
poll_beacon_proposers().
- Read
slot_clock.now() once and use that slot for both proposer-cache lookups and notifications.
See the proposer loop and the single current-slot read.
SystemTimeSlotClock::duration_to_next_slot() calculates a duration from SystemTime. The async timer then waits for that duration, but the proposer loop does not verify that the intended next slot has actually started before polling.
In this event, the timer woke at the boundary and now() still returned slot 78. The whole iteration therefore processed slot 78. By the time that iteration finished, the outer loop calculated its next wait from slot 79 to slot 80, so slot 79 was never polled on that operator.
The first missing stage is before Anchor RANDAO, QBFT, and block production. This is not a duty-cache miss, network quorum failure, Boole domain mismatch, or resource saturation event.
Reproduction
The original runtime reproduction is retained locally:
- Params:
/Users/shanemoore/Code/dev/ethereum/ssv-mini-boole-stress/params-boole-proposer-diagnostic.yaml
- Logs and resource samples:
/var/folders/4l/n1rhxk3x4ng6tylztyffl4g80000gn/T/ssv-mini-boole-proposer-diagnostic.XXXXXXXX.kSU2qnePGN
- Resource run ID:
0117faf6-d5e8-420f-956f-88c9fce5a4ce
Run from the pinned ssv-mini checkout:
make run PARAMS_FILE=params-boole-proposer-diagnostic.yaml ENCLAVE_NAME=boole-proposer-diagnostic-1
The diagnostic Lighthouse patch only adds logs around:
- exact-slot proposer-cache lookup,
- block-production notification send,
- block-production notification receive.
It does not change protocol or scheduling behavior.
A deterministic regression test should avoid waiting for a rare live clock boundary:
- Use a controllable
SlotClock, such as ManualSlotClock or a narrow test clock.
- Start the proposer scheduler in slot
S - 1.
- Let its sleep complete while
now() still returns S - 1.
- Then advance the clock to
S.
- Assert that the scheduler waits or retries until
S, emits exactly one notification for S, and does not sleep through to S + 1.
Expected Behaviour
After waiting for the next-slot boundary, the proposer scheduler must confirm that the intended target slot has started before polling duties.
An early timer wake must not cause:
- a stale previous-slot notification,
- an expired-slot warning,
- omission of the target-slot proposer-cache lookup,
- omission of a locally assigned proposal.
Steps to resolve
Likely Lighthouse fix:
- Capture the intended target slot before sleeping.
- After every wake, re-read
slot_clock.now().
- If the clock still reports a slot before the target, wait again using
duration_to_slot(target_slot) or an equivalent bounded retry.
- Poll proposer duties only after the target slot is visible.
- Define conservative behavior when the slot clock is unavailable or moves backwards.
- Add deterministic early-wake regression coverage.
- Confirm that the retry cannot emit duplicate block-production notifications.
Anchor follow-up:
- Track or open the corresponding Lighthouse issue and fix.
- Temporarily pin a patched Lighthouse revision if the upstream release path is too slow.
- Run focused Lighthouse tests and Anchor proposer-service tests.
- Repeat the mixed-client proposer diagnostic and confirm there are no stale notifications or skipped assigned slots.
- Update Anchor's Lighthouse pin after the upstream fix lands.
Acceptance criteria
- An early wake before slot
S cannot cause the scheduler to skip S.
- The assigned proposer for
S receives exactly one usable notification.
- No duplicate proposal work is introduced.
- A deterministic regression test fails on
b263df5 and passes with the fix.
- Anchor integrates the fixed Lighthouse revision.
- The mixed 2 Anchor plus 2 go-ssv diagnostic completes repeated managed proposal duties without this failure signature.
Scope
This issue covers only the early-wakeup proposer scheduling failure at slot 79. The separate post-suspension partial-signature delivery failure observed later at slot 216 has a different mechanism and is intentionally out of scope.
Description
Anchor can skip a locally assigned proposal when the pinned Lighthouse proposer-duty timer wakes at a slot boundary but the Lighthouse slot clock still returns the previous slot.
This was observed on one Anchor operator during a mixed 2 Anchor plus 2 go-ssv stress run. The affected operator processed slot 78 again, sent a stale slot-78 notification, and then slept until the next boundary without ever checking slot 79. The other three operators reached the 3-of-4 threshold and produced canonical block 79.
The bug is most likely owned by Lighthouse
validator_services. It is tracked here because Anchor pins and embeds that code, needs a dependency integration path, and can ship a temporary patched Lighthouse revision if required. The observation occurred before the Boole activation in this run, so the failure is not Boole-specific.Version
unstable:fd3c7f9677b2bbb04fe7d7c2d4eca81313db7b79b263df596671a2bd42bf1034e1cdc8188ba8a9b0unstablechecked on 2026-08-06:02e8732a81a68784ce105609942570470a7989f2, with the same scheduler behavior still presentfef3b7d791384911078b2ce977100f87679e5c46stage:874ba316266aec6215bc21c6e2e929e195f08d6b1.93.1node/anchor:boole-proposer-diagnostic-fd3c7f9-b263df5sha256:5a459c08cf1d8fc3d6a4b06f76249c6273f07c1d782f2be5a38d6c288a2aa7ee41c7addc3ef97b8be130e0e34690b43c05fa03714114209c9ed1c108d13303d3Anchor's pin is visible in Cargo.toml.
Test topology
boole-proposer-diagnostic-11ed99ef7ef154ddba6dc20a9c1b6e731Present Behaviour
Validator index 64, pubkey:
was assigned to propose at slot 79.
Anchor operator 2 logged:
There was no slot-79 proposer-cache lookup, slot-79 notification, RANDAO entry, unsigned-block request, or proposal publication on this operator.
For comparison, Anchor operator 1 entered the correct path:
go-ssv operator 4 independently showed that its boundary estimator still considered the current slot to be 78, then completed slot 79 successfully:
{"time":"2026-08-06T23:36:30.009266Z","msg":"duty did not complete before slot end (likely stuck)","runner_role":"PROPOSER","slot":79,"estimated_current_slot":"78"} {"time":"2026-08-06T23:36:31.008601Z","msg":"successfully submitted block proposal","runner_role":"PROPOSER","slot":79,"estimated_current_slot":"79","block_root":"aeca1f6a53a616e3eee636a55454a2acf87e5db2044153dfe253bddd2a4258d7"}The warning's overload explanation is not supported by the resource sample at
23:36:31Z:There were no monitor alerts at this sample.
Failure mechanism
The pinned Lighthouse proposer task does this once per loop:
duration_to_next_slot().poll_beacon_proposers().slot_clock.now()once and use that slot for both proposer-cache lookups and notifications.See the proposer loop and the single current-slot read.
SystemTimeSlotClock::duration_to_next_slot()calculates a duration fromSystemTime. The async timer then waits for that duration, but the proposer loop does not verify that the intended next slot has actually started before polling.In this event, the timer woke at the boundary and
now()still returned slot 78. The whole iteration therefore processed slot 78. By the time that iteration finished, the outer loop calculated its next wait from slot 79 to slot 80, so slot 79 was never polled on that operator.The first missing stage is before Anchor RANDAO, QBFT, and block production. This is not a duty-cache miss, network quorum failure, Boole domain mismatch, or resource saturation event.
Reproduction
The original runtime reproduction is retained locally:
/Users/shanemoore/Code/dev/ethereum/ssv-mini-boole-stress/params-boole-proposer-diagnostic.yaml/var/folders/4l/n1rhxk3x4ng6tylztyffl4g80000gn/T/ssv-mini-boole-proposer-diagnostic.XXXXXXXX.kSU2qnePGN0117faf6-d5e8-420f-956f-88c9fce5a4ceRun from the pinned ssv-mini checkout:
The diagnostic Lighthouse patch only adds logs around:
It does not change protocol or scheduling behavior.
A deterministic regression test should avoid waiting for a rare live clock boundary:
SlotClock, such asManualSlotClockor a narrow test clock.S - 1.now()still returnsS - 1.S.S, emits exactly one notification forS, and does not sleep through toS + 1.Expected Behaviour
After waiting for the next-slot boundary, the proposer scheduler must confirm that the intended target slot has started before polling duties.
An early timer wake must not cause:
Steps to resolve
Likely Lighthouse fix:
slot_clock.now().duration_to_slot(target_slot)or an equivalent bounded retry.Anchor follow-up:
Acceptance criteria
Scannot cause the scheduler to skipS.Sreceives exactly one usable notification.b263df5and passes with the fix.Scope
This issue covers only the early-wakeup proposer scheduling failure at slot 79. The separate post-suspension partial-signature delivery failure observed later at slot 216 has a different mechanism and is intentionally out of scope.