Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {BitArray} from "@chainsafe/ssz";
import {routes} from "@lodestar/api";
import {
AncestorStatus,
EpochDifference,
ExecutionStatus,
ForkChoiceError,
ForkChoiceErrorCode,
Expand All @@ -21,7 +20,6 @@ import {
IBeaconStateView,
RootCache,
computeEpochAtSlot,
computeStartSlotAtEpoch,
computeTimeAtSlot,
isStartSlotOfEpoch,
isStatePostAltair,
Expand Down Expand Up @@ -301,21 +299,6 @@ export async function importBlock(
// Set head state as strong reference
this.regen.updateHeadState(newHead, postState);

try {
this.emitter.emit(routes.events.EventType.head, {
block: newHead.blockRoot,
epochTransition: computeStartSlotAtEpoch(computeEpochAtSlot(newHead.slot)) === newHead.slot,
slot: newHead.slot,
state: newHead.stateRoot,
previousDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.previous),
currentDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.current),
executionOptimistic: isOptimisticBlock(newHead),
});
} catch (e) {
// getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
this.logger.debug("Error emitting head event", {slot: newHead.slot, root: newHead.blockRoot}, e as Error);
}

const delaySec = this.clock.secFromSlot(newHead.slot);
this.logger.verbose("New chain head", {
slot: newHead.slot,
Expand Down
33 changes: 31 additions & 2 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import path from "node:path";
import {PrivateKey} from "@libp2p/interface";
import {Type} from "@chainsafe/ssz";
import {routes} from "@lodestar/api";
import {BeaconConfig} from "@lodestar/config";
import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice";
import {
CheckpointWithHex,
EpochDifference,
ForkChoiceStateGetter,
IForkChoice,
ProtoBlock,
UpdateHeadOpt,
} from "@lodestar/fork-choice";
import {LoggerNode} from "@lodestar/logger/node";
import {
EFFECTIVE_BALANCE_INCREMENT,
Expand Down Expand Up @@ -1179,7 +1187,27 @@ export class BeaconChain implements IBeaconChain {
const timer = this.metrics?.forkChoice.findHead.startTimer({caller});

try {
return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
const prevHead = this.forkChoice.getHead();
const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;

if (head.blockRoot !== prevHead.blockRoot) {
Comment on lines +1191 to +1193

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit head events from proposer-head recomputes too

When the first canonical recompute after attestation weight shifts happens through block production, this new emission path is bypassed: BeaconChain.getProposerHead() calls forkChoice.updateAndGetHead({mode: GetProposerHead}), and ForkChoice.updateAndGetHead() runs updateHead() for that mode before returning. In that scenario the cached canonical head changes, but no head SSE event is emitted, so validators watching head events can still miss the duty-dependent-root update until some later recomputeForkChoiceHead() call.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is intentional, we explicitly don't want to emit a head event on the proposer path when we are proposing a block ourselves

that head change is superseded once we import our own block, which emits the head event with the correct dependent roots, so head event consumers aren't affected in practice

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getProposerHead() also changes the head, it's worth to extract this to a separate method to reuse?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, getProposerHead() is different, could be a proposer boost reorg there
maybe just drop a comment on why we don't need to emit a head event over there

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as noted in the PR description I intentionally don't wanna consider the proposer path, I think it is fine to rely on the block import path in that case, we can add the comment as you suggested

try {
this.emitter.emit(routes.events.EventType.head, {
block: head.blockRoot,
epochTransition: computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot,
slot: head.slot,
state: head.stateRoot,
previousDutyDependentRoot: this.forkChoice.getDependentRoot(head, EpochDifference.previous),
currentDutyDependentRoot: this.forkChoice.getDependentRoot(head, EpochDifference.current),
executionOptimistic: isOptimisticBlock(head),
});
} catch (e) {
// getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
this.logger.debug("Error emitting head event", {slot: head.slot, root: head.blockRoot}, e as Error);
}
}

return head;
} catch (e) {
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
throw e;
Expand Down Expand Up @@ -1209,6 +1237,7 @@ export class BeaconChain implements IBeaconChain {
const secFromSlot = this.clock.secFromSlot(slot);

try {
// Do not emit head event here, when proposing we rely on the one emitted when importing our own block
const {head, isHeadTimely, notReorgedReason} = this.forkChoice.updateAndGetHead({
mode: UpdateHeadOpt.GetProposerHead,
secFromSlot,
Expand Down
Loading