Skip to content
14 changes: 14 additions & 0 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export enum EventType {
blobSidecar = "blob_sidecar",
/** The node has received a valid DataColumnSidecar (from P2P or API) */
dataColumnSidecar = "data_column_sidecar",
/** The node has verified that the execution payload and blobs for a block are available */
executionPayloadAvailable = "execution_payload_available",
}

export const eventTypes: {[K in EventType]: K} = {
Expand All @@ -108,6 +110,7 @@ export const eventTypes: {[K in EventType]: K} = {
[EventType.payloadAttributes]: EventType.payloadAttributes,
[EventType.blobSidecar]: EventType.blobSidecar,
[EventType.dataColumnSidecar]: EventType.dataColumnSidecar,
[EventType.executionPayloadAvailable]: EventType.executionPayloadAvailable,
};

export type EventData = {
Expand Down Expand Up @@ -157,6 +160,10 @@ export type EventData = {
[EventType.payloadAttributes]: {version: ForkName; data: SSEPayloadAttributes};
[EventType.blobSidecar]: BlobSidecarSSE;
[EventType.dataColumnSidecar]: DataColumnSidecarSSE;
[EventType.executionPayloadAvailable]: {
slot: Slot;
blockRoot: RootHex;
};
};

export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType];
Expand Down Expand Up @@ -311,6 +318,13 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
[EventType.payloadAttributes]: WithVersion((fork) => getPostBellatrixForkTypes(fork).SSEPayloadAttributes),
[EventType.blobSidecar]: blobSidecarSSE,
[EventType.dataColumnSidecar]: dataColumnSidecarSSE,
[EventType.executionPayloadAvailable]: new ContainerType(
{
slot: ssz.Slot,
blockRoot: stringType,
},
{jsonCase: "eth2"}
),

[EventType.lightClientOptimisticUpdate]: WithVersion(
(fork) => getPostAltairForkTypes(fork).LightClientOptimisticUpdate
Expand Down
4 changes: 4 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,8 @@ export const eventTestData: EventData = {
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
],
}),
[EventType.executionPayloadAvailable]: {
slot: 10,
blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
},
};
61 changes: 45 additions & 16 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from "@lodestar/types";
import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils";
import {BlockInputSource, isBlockInputBlobs, isBlockInputColumns} from "../../../../chain/blocks/blockInput/index.js";
import {PayloadEnvelopeInputSource} from "../../../../chain/blocks/payloadEnvelopeInput/index.ts";
import {ImportBlockOpts} from "../../../../chain/blocks/types.js";
import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js";
import {BeaconChain} from "../../../../chain/chain.js";
Expand All @@ -48,6 +49,7 @@ import {
ProduceFullGloas,
} from "../../../../chain/produceBlock/index.js";
import {validateGossipBlock} from "../../../../chain/validation/block.js";
import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js";
import {OpSource} from "../../../../chain/validatorMonitor.js";
import {
computePreFuluKzgCommitmentsInclusionProof,
Expand Down Expand Up @@ -676,6 +678,7 @@ export function getBeaconBlockApi({
}

if (cachedResult.cells && cachedResult.blobsBundle.commitments.length > 0) {
const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
const cellsAndProofs = cachedResult.cells.map((rowCells, rowIndex) => ({
cells: rowCells,
proofs: cachedResult.blobsBundle.proofs.slice(
Expand All @@ -685,24 +688,55 @@ export function getBeaconBlockApi({
}));

dataColumnSidecars = getDataColumnSidecarsForGloas(slot, envelope.beaconBlockRoot, cellsAndProofs);
timer?.();
}
} else {
// TODO GLOAS: will this api be used by builders or only for self-building?
}

// TODO GLOAS: Verify execution payload envelope signature
// For self-builds, the proposer signs with their own validator key
// For external builders, verify using the builder's registered pubkey
// Use verify_execution_payload_envelope_signature(state, signed_envelope)
await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
Copy link
Member

Choose a reason for hiding this comment

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

we should probably run this validation earlier, based on ethereum/beacon-APIs#580 (comment) we wanna implement broadcast_validation so we can mirror what we have implemented in publishBlockV2


// TODO GLOAS: Process execution payload via state transition
// Call process_execution_payload(state, signed_envelope, execution_engine)
// If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
await sleep(msToBlockSlot);
}

// TODO GLOAS: Update fork choice with the execution payload
// Call on_execution_payload(store, signed_envelope) to update fork choice state
const payloadInput = chain.seenPayloadEnvelopeInput.get(blockRootHex);
if (!payloadInput) {
throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`);
}
Copy link
Member

Choose a reason for hiding this comment

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

this can happen if block and payload are submitted in parallel as documented here #8915 we will need a queuing mechanism on the api, we can just throw for now as we currently only submit sequentially which is fine for the devnets


// TODO GLOAS: Add envelope and data columns to block input via seenBlockInputCache
// and trigger block import (Gloas block import requires both beacon block and envelope)
payloadInput.addPayloadEnvelope({
envelope: signedExecutionPayloadEnvelope,
source: PayloadEnvelopeInputSource.api,
seenTimestampSec,
peerIdStr: undefined,
});

if (dataColumnSidecars.length > 0) {
for (const columnSidecar of dataColumnSidecars) {
payloadInput.addColumn({
columnSidecar,
source: PayloadEnvelopeInputSource.api,
seenTimestampSec,
peerIdStr: undefined,
});
}
}

// TODO GLOAS: Unlike publishBlock which gossips and imports in parallel, we import before gossip here.
// The publishExecutionPayloadEnvelope spec says success = "gossip validation + broadcast", so we may
// want to gossip first. Need spec clarification on whether import failure should prevent broadcast.
if (payloadInput.shouldImport()) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't really see a reason why this should work differently than pre-gloas publish block flow, we can gossip and import in parallel

broadcast_validation is separate from this and needs to be validated earlier, the checks are not exactly the same as import checks, depending on which validation rule is chosen

so I think here we can assume all validation checks have passed already

// Signature already verified in validateApiExecutionPayloadEnvelope
await chain.importExecutionPayload(payloadInput, {validSignature: true});
chain.persistPayloadEnvelope(payloadInput);
chain.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
slot,
blockRoot: blockRootHex,
});
}

const valLogMeta = {
slot,
Expand All @@ -712,14 +746,9 @@ export function getBeaconBlockApi({
dataColumns: dataColumnSidecars.length,
};

// If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N.
const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
await sleep(msToBlockSlot);
}

const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec);
chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.api, delaySec, signedExecutionPayloadEnvelope);

chain.logger.info("Publishing execution payload envelope", valLogMeta);

Expand Down
37 changes: 35 additions & 2 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import {
NotReorgedReason,
getSafeExecutionBlockHash,
} from "@lodestar/fork-choice";
import {ForkPostAltair, ForkPostElectra, ForkSeq, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {
ForkPostAltair,
ForkPostElectra,
ForkPostGloas,
ForkSeq,
MAX_SEED_LOOKAHEAD,
SLOTS_PER_EPOCH,
isForkPostGloas,
} from "@lodestar/params";
import {
CachedBeaconStateAltair,
EpochCache,
Expand All @@ -20,7 +28,17 @@ import {
isStartSlotOfEpoch,
isStateValidatorsNodesPopulated,
} from "@lodestar/state-transition";
import {Attestation, BeaconBlock, altair, capella, electra, isGloasBeaconBlock, phase0, ssz} from "@lodestar/types";
import {
Attestation,
BeaconBlock,
SignedBeaconBlock,
altair,
capella,
electra,
isGloasBeaconBlock,
phase0,
ssz,
} from "@lodestar/types";
import {isErrorAborted, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {callInNextEventLoop} from "../../util/eventLoop.js";
Expand Down Expand Up @@ -118,6 +136,21 @@ export async function importBlock(
// Some block event handlers require state being in state cache so need to do this before emitting EventType.block
this.regen.processState(blockRootHex, postState);

// For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import
const forkName = this.config.getForkName(blockSlot);
if (isForkPostGloas(forkName)) {
this.seenPayloadEnvelopeInput.add({
blockRootHex,
block: block as SignedBeaconBlock<ForkPostGloas>,
sampledColumns: this.custodyConfig.sampledColumns,
custodyColumns: this.custodyConfig.custodyColumns,
timeCreatedSec: fullyVerifiedBlock.seenTimestampSec,
});
if (opts.seenTimestampSec !== undefined) {
this.logger.debug("Created PayloadEnvelopeInput for Gloas block", {slot: blockSlot, root: blockRootHex});
}
}

this.metrics?.importBlock.bySource.inc({source: source.source});
this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex});

Expand Down
Loading
Loading