ChainSafe / ChainSafe/lodestar
Attestation data can use a stale cached head on empty slots
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 483
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 150
Description
### Describe the bug
On empty slots, Lodestar can return an outdated head when a validator asks for attestation data.
At the start of each slot, [`updateTime()`](https://github.com/ChainSafe/lodestar/blob/b0ce010779032ffafdf7fc24c4160c37c4314470/packages/fork-choice/src/forkChoice/forkChoice.ts#L1129-L1150) applies queued votes from previous slots. But it only recalculates the head if checkpoints change or fast confirmation runs. Otherwise, the votes are recorded but the cached head stays unchanged.
A [block import](https://github.com/ChainSafe/lodestar/blob/b0ce010779032ffafdf7fc24c4160c37c4314470/packages/beacon-node/src/chain/blocks/importBlock.ts#L300-L301) normally triggers another calculation. If no block is imported and no other head calculation runs, the cached head can remain unchanged until [`PrepareNextSlotScheduler`](https://github.com/ChainSafe/lodestar/blob/b0ce010779032ffafdf7fc24c4160c37c4314470/packages/beacon-node/src/chain/prepareNextSlot.ts#L102-L107) runs at roughly 8 seconds into a 12-second slot.
The attestation deadline is earlier, at roughly 4 seconds. [`produceAttestationData()`](https://github.com/ChainSafe/lodestar/blob/b0ce010779032ffafdf7fc24c4160c37c4314470/packages/beacon-node/src/api/impl/validator/index.ts#L1311-L1314) reads the cached head without refreshing it. A validator can therefore vote for a head that a fresh fork-choice calculation would no longer select.
### Example scenario
Suppose two competing branches, A and B, are already known. Fast confirmation is off, and there is no
checkpoint update or other head calculation before the attestation request.
| Time | Event | Cached head |
| ------------- | ----------------------------------------------------------------- | ------------- |
| Slot N-1, ~8s | Head calculation chooses A. | A |
| Slot N, start | Previous-slot votes are applied. They make B the stronger branch. | A |
| Slot N, ~4s | No block has arrived. A validator asks for attestation data. | A is returned |
| Slot N, ~8s | Scheduled head calculation picks up the changed votes. | B |
Only votes from past slots count here. Votes for slot N
[stay queued until N+1](https://github.com/ChainSafe/lodestar/blob/b0ce010779032ffafdf7fc24c4160c37c4314470/packages/fork-choice/src/forkChoice/forkChoice.ts#L1045-L1065).
If the winning branch does not change, the cached answer is still correct. The concern is when votes
shift support to a competing branch. This report is based on code inspection; the impact on mainnet
has not been measured.
### Expected behavior
Attestation data should use a head that reflects eligible votes already processed by fork choice.
This should also hold when no block arrives in the current slot.
Contributor guide
Research direction
Start with updateTime() in packages/fork-choice/src/forkChoice/forkChoice.ts and trace the cached-head read in produceAttestationData() at packages/beacon-node/src/api/impl/validator/index.ts. Compare the block-import and PrepareNextSlotScheduler paths named in the report; done means attestation data reflects eligible prior-slot votes even when no block arrives in the current slot.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- blockchain, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100