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
Original file line number Diff line number Diff line change
Expand Up @@ -1060,10 +1060,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
const signedEnvelope = sszDeserialize(topic, serializedData);
const envelope = signedEnvelope.message;

// TODO GLOAS: consider optimistically create PayloadEnvelopeInput here similar to how we do that for beacon_block
// so that UnknownBlockSync can handle backward sync
// the problem now is we cannot create a PayloadEnvelopeInput without the beacon block being known, we need at least the proposer index
// we can achieve that by looking into the EpochCache
// unlike BlockInput, we send the envelope into UnknownBlockInput sync
// inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip
try {
await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope);
} catch (e) {
Expand All @@ -1072,7 +1070,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
const slot = signedEnvelope.message.payload.slotNumber;
logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code});
if (e.type.code === ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN) {
// TODO GLOAS: UnknownBlockSync to handle this
chain.emitter.emit(ChainEvent.envelopeUnknownBlock, {
envelope: signedEnvelope,
peer: peerIdStr,
Expand Down
74 changes: 49 additions & 25 deletions packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,40 @@ describe("sync / unknown block sync thru gloas", () => {
}
});

const testCases: {id: string; event: ChainEvent}[] = [
const testCases: {id: string; event: ChainEvent; expectsPayloadImport: boolean}[] = [
{
id: "should do an unknown block parent sync from another BN",
event: ChainEvent.blockUnknownParent,
expectsPayloadImport: false,
},
{
id: "should do an unknown block sync from another BN",
event: ChainEvent.unknownBlockRoot,
expectsPayloadImport: false,
},
{
id: "should do an incompleteBlockInput sync from another BN",
event: ChainEvent.incompleteBlockInput,
expectsPayloadImport: false,
},
{
id: "should do an unknownEnvelopeBlockRoot sync from another BN",
event: ChainEvent.unknownEnvelopeBlockRoot,
expectsPayloadImport: true,
},
{
id: "should do an envelopeUnknownBlock sync from another BN",
event: ChainEvent.envelopeUnknownBlock,
expectsPayloadImport: true,
},
{
id: "should do an incompletePayloadEnvelope sync from another BN",
event: ChainEvent.incompletePayloadEnvelope,
expectsPayloadImport: true,
},
];

for (const {id, event} of testCases) {
for (const {id, event, expectsPayloadImport} of testCases) {
it(id, async () => {
// the node needs time to transpile/initialize bls worker threads
const genesisSlotsDelay = 4;
Expand Down Expand Up @@ -158,15 +168,6 @@ describe("sync / unknown block sync thru gloas", () => {
100000,
({block}) => block === headRootHex
);
const maybeWaitForPayloadImported =
event === ChainEvent.unknownEnvelopeBlockRoot || event === ChainEvent.incompletePayloadEnvelope
? Promise.resolve()
: waitForEvent<routes.events.EventData[routes.events.EventType.executionPayload]>(
bn2.chain.emitter,
routes.events.EventType.executionPayload,
100000,
({blockRoot}) => blockRoot === headRootHex
);

const connected = Promise.all([onPeerConnect(bn2.network), onPeerConnect(bn.network)]);
await connect(bn2.network, bn.network);
Expand All @@ -182,6 +183,15 @@ describe("sync / unknown block sync thru gloas", () => {
forkName: bn.chain.config.getForkName(headSlot),
daOutOfRange: false,
});
const waitForPayloadImported = expectsPayloadImport
? waitForEvent<routes.events.EventData[routes.events.EventType.executionPayload]>(
bn2.chain.emitter,
routes.events.EventType.executionPayload,
100000,
({blockRoot}) => blockRoot === headRootHex
)
: undefined;

switch (event) {
case ChainEvent.blockUnknownParent:
await bn2.chain.processBlock(headInput).catch((e) => {
Expand Down Expand Up @@ -222,6 +232,20 @@ describe("sync / unknown block sync thru gloas", () => {
source: BlockInputSource.gossip,
});
break;
case ChainEvent.envelopeUnknownBlock: {
// Node A produced the head; its cache has the full signed envelope (populated via
// publishExecutionPayloadEnvelope -> payloadInput.addPayloadEnvelope).
const payloadInputOnA = bn.chain.seenPayloadEnvelopeInputCache.get(headRootHex);
if (!payloadInputOnA?.hasPayloadEnvelope()) {
throw Error(`Expected node A to have signed envelope for ${headRootHex}`);
}
bn2.chain.emitter.emit(ChainEvent.envelopeUnknownBlock, {
envelope: payloadInputOnA.getPayloadEnvelope(),
peer: sourcePeerId,
source: BlockInputSource.gossip,
});
break;
}
case ChainEvent.incompletePayloadEnvelope: {
// get the chain started with an unknownBlockRoot
bn2.chain.emitter.emit(ChainEvent.unknownBlockRoot, {
Expand All @@ -235,22 +259,19 @@ describe("sync / unknown block sync thru gloas", () => {
throw Error("Unknown event type");
}

// Wait for the block root to be processed in node B. Payload-aware entrypoints should also import
// the separated payload envelope for the same root.
// Wait for the block root to be processed in node B. The unknown-block-sync flow imports
// parent payloads as needed to satisfy sanity checks but does not fetch the head block's
// own payload envelope (which would normally arrive via gossip on a live network).
await waitForSynced;

switch (event) {
case ChainEvent.incompletePayloadEnvelope: {
// After it syncs, send an incomplete payload envelope
// and assert the payload gets imported
const payloadInput = bn2.chain.seenPayloadEnvelopeInputCache.add({
blockRootHex: headRootHex,
forkName: bn2.config.getForkName(headSlot),
block: head,
sampledColumns: bn2.chain.custodyConfig.sampledColumns,
custodyColumns: bn2.chain.custodyConfig.custodyColumns,
timeCreatedSec: Math.floor(Date.now() / 1000),
});
// After it syncs, retrieve the PayloadEnvelopeInput created during block import
// and emit incompletePayloadEnvelope to exercise the sync handler.
const payloadInput = bn2.chain.seenPayloadEnvelopeInputCache.get(headRootHex);
if (!payloadInput) {
throw Error(`Expected PayloadEnvelopeInput for ${headRootHex} after block sync`);
}
Comment on lines +271 to +274
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The incompletePayloadEnvelope test case triggers a payload sync by emitting an event but does not wait for the sync to complete. Since the sync happens in the background, the test might finish and close the node (via afterEach) before the payload is actually imported. This could lead to flakiness or a false positive where the sync logic is triggered but not actually completed. Consider waiting for the routes.events.EventType.executionPayload event after emitting the trigger to ensure the sync is fully exercised.

bn2.chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
payloadInput,
peer: sourcePeerId,
Expand All @@ -262,8 +283,11 @@ describe("sync / unknown block sync thru gloas", () => {
break;
}

// only await payload import for events that imply importing it
await maybeWaitForPayloadImported;
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.

hmm might be that the test is just not waiting now? tests are passing but mayeb we a not asserting stuff anymore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Case Drives head-payload import? Currently asserted?
blockUnknownParent no — only parent payloads head event only — correct
unknownBlockRoot no — only parent payloads head event only — correct
incompleteBlockInput no — only parent payloads head event only — correct
unknownEnvelopeBlockRoot yes head event only — gap
envelopeUnknownBlock (new) yes head + payload — fixed
incompletePayloadEnvelope yes head + payload — fixed

we are only missing unknownEnvelopeBlockRoot, gonna add that

// for incompletePayloadEnvelope the trigger is in the second switch above
// so we have to assert payload import at the end
if (waitForPayloadImported) {
await waitForPayloadImported;
}
});
}
});
Loading