Skip to content
Closed
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
62 changes: 52 additions & 10 deletions packages/beacon-node/src/node/notifier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {BeaconConfig} from "@lodestar/config";
import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice";
import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {ExecutionStatus, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice";
import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
import {
IBeaconStateView,
computeEpochAtSlot,
Expand All @@ -19,6 +19,11 @@ import {TimeSeries} from "../util/timeSeries.js";
/** Create a warning log whenever the peer count is at or below this value */
const WARN_PEER_COUNT = 1;

/** Notifier log point as basis points of the slot: 5000 BPS = half-slot (pre-Gloas). */
const NOTIFIER_LOG_DUE_BPS = 5000;
/** Notifier log point for Gloas: 8333 BPS ≈ 5/6 of slot — past PTC deadline so the head has transitioned from PENDING to FULL/EMPTY. */
const NOTIFIER_LOG_DUE_BPS_GLOAS = 8333;

type NodeNotifierModules = {
network: INetwork;
chain: IBeaconChain;
Expand Down Expand Up @@ -127,8 +132,9 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise<voi
logger.info(`New sync committee period ${period}`);
}

// Log halfway through each slot
await sleep(timeToNextHalfSlot(config, chain, isFirstTime), signal);
// Log once per slot, after the slot's main events have settled
// (halfway for pre-Gloas, 5/6 for Gloas to let PTC votes land — spec PR #4884)
await sleep(timeToNextLogPoint(config, chain, isFirstTime), signal);
isFirstTime = false;
}
} catch (e) {
Expand All @@ -139,22 +145,33 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise<voi
}
}

function timeToNextHalfSlot(config: BeaconConfig, chain: IBeaconChain, isFirstTime: boolean): number {
function timeToNextLogPoint(config: BeaconConfig, chain: IBeaconChain, isFirstTime: boolean): number {
const msPerSlot = config.SLOT_DURATION_MS;
const msPerHalfSlot = msPerSlot / 2;
const msFromGenesis = Date.now() - chain.genesisTime * 1000;
const msToNextSlot =
msFromGenesis < 0
? // For future genesis time, calculate time left in the slot
-msFromGenesis % msPerSlot
: // For past genesis time, calculate time until the next slot
msPerSlot - (msFromGenesis % msPerSlot);

// In Gloas, PTC votes decide payload timeliness around 2/3 of the slot; log at 5/6 so the
// head has transitioned from PENDING to FULL/EMPTY. Pre-Gloas keeps the half-slot cadence.
const currentSlot = chain.clock.currentSlot;
const logDueBps = isForkPostGloas(config.getForkName(currentSlot))
? NOTIFIER_LOG_DUE_BPS_GLOAS
: NOTIFIER_LOG_DUE_BPS;
const msSlotOffset = config.getSlotComponentDurationMs(logDueBps);

if (isFirstTime) {
// at the 1st time we may miss middle of the current clock slot
return msToNextSlot > msPerHalfSlot ? msToNextSlot - msPerHalfSlot : msToNextSlot + msPerHalfSlot;
// at the 1st time we may miss the log point of the current clock slot
const msToSlotEndFromLogPoint = msPerSlot - msSlotOffset;
return msToNextSlot > msToSlotEndFromLogPoint
? msToNextSlot - msToSlotEndFromLogPoint
: msToNextSlot + msSlotOffset;
}
// after the 1st time always wait until middle of next clock slot
return msToNextSlot + msPerHalfSlot;
// after the 1st time always wait until the log point of the next clock slot
return msToNextSlot + msSlotOffset;
}

function getHeadExecutionInfo(
Expand All @@ -167,6 +184,13 @@ function getHeadExecutionInfo(
return [];
}

const fork = config.getForkName(headInfo.slot);

// Gloas: show payload status (PENDING/EMPTY/FULL) instead of EL execution status
if (isForkPostGloas(fork)) {
return getGloasExecutionInfo(headInfo);
}

const executionStatusStr = headInfo.executionStatus.toLowerCase();

// Add execution status to notifier only if head is on/post bellatrix
Expand All @@ -187,3 +211,21 @@ function getHeadExecutionInfo(

return [];
}

/** Gloas-specific execution info showing payload status and block details */
function getGloasExecutionInfo(headInfo: ProtoBlock): string[] {
const payloadStatusStr =
headInfo.payloadStatus === PayloadStatus.FULL
? "full"
: headInfo.payloadStatus === PayloadStatus.EMPTY
? "empty"
: "pending";
Copy link
Copy Markdown

@nflaig nflaig Apr 19, 2026

Choose a reason for hiding this comment

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

I feel like this shouldn't be pending ever, we might need to re-think how out notifier works in gloas, my intuition right now is that instead of printing the log out half way through the slot we should rather do it at 10 seconds (or so) in the slot, and then either print out full or empty. what do you think @lodekeeper ?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, good catch. Pushed fdd80f44d0: in Gloas we now log at 5/6 of the slot (10s on mainnet 12s slots, 5s on future 6s slots) instead of halfway. Pre-Gloas keeps the half-slot cadence to avoid shifting existing behavior.

Rationale for 5/6 rather than a fixed 10s: PTC votes land around 2/3 of the slot, so 5/6 gives ~2s margin for proto-array to update the head from PENDING → FULL/EMPTY. I kept the PENDING branch in getGloasExecutionInfo as a safety net (e.g. if PTC quorum never lands), but it should be rare at 10s.


if (headInfo.payloadStatus === PayloadStatus.FULL && headInfo.executionPayloadBlockHash !== null) {
const hashInfo = prettyBytesShort(headInfo.executionPayloadBlockHash);
const numberInfo = headInfo.executionPayloadNumber;
return [`payload: ${payloadStatusStr}(${numberInfo} ${hashInfo})`];
}

return [`payload: ${payloadStatusStr}`];
}
Loading