ChainSafe / ChainSafe/lodestar

Attestation data can use a stale cached head on empty slots

Open
#10,038 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.