Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
20 changes: 11 additions & 9 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,23 +1112,25 @@ export function getValidatorApi(
notWhileSyncing();
await waitForSlot(slot);

const block = chain.forkChoice.getCanonicalBlockClosestLteSlot(slot);
const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
if (!block) {
throw new ApiError(404, `No canonical block found at or before slot=${slot}`);
// No block is seen at slot. Return 404 so vc can skip casting payload attestation.
throw new ApiError(404, `No canonical block found at slot=${slot}`);
}

const blockIsForSlot = block.slot === slot;
const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
// Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
// into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not
// the input's creation time.
const payloadDueSec = config.getPayloadDueMs() / 1000;
const payloadPresent =
blockIsForSlot &&
payloadInput !== undefined &&
payloadInput.hasPayloadEnvelope() &&
chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec;
const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false);
let payloadPresent = false;
let blobDataAvailable = false;
if (payloadInput !== undefined) {
payloadPresent =
payloadInput.hasPayloadEnvelope() &&
chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec;
blobDataAvailable = payloadInput.hasAllData();
}
Comment thread
ensi321 marked this conversation as resolved.
Outdated

return {
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("api - validator - produceAttestationData", () => {
modules.config = gloasConfig;
api = getValidatorApi(defaultApiOptions, modules);

modules.forkChoice.getCanonicalBlockClosestLteSlot.mockReturnValue({
modules.forkChoice.getCanonicalBlockAtSlot.mockReturnValue({
slot: 0,
blockRoot: ZERO_HASH_HEX,
} as ProtoBlock);
Expand All @@ -86,5 +86,25 @@ describe("api - validator - produceAttestationData", () => {
expect(res.data.payloadPresent).toBe(true);
expect(res.data.blobDataAvailable).toBe(true);
});

it("Should throw 404 when no canonical block has been seen for the assigned slot", async () => {
const gloasConfig = createChainForkConfig({
...defaultChainConfig,
ALTAIR_FORK_EPOCH: 0,
BELLATRIX_FORK_EPOCH: 0,
CAPELLA_FORK_EPOCH: 0,
DENEB_FORK_EPOCH: 0,
ELECTRA_FORK_EPOCH: 0,
FULU_FORK_EPOCH: 0,
GLOAS_FORK_EPOCH: 0,
});
modules = getApiTestModules({config: gloasConfig});
modules.config = gloasConfig;
api = getValidatorApi(defaultApiOptions, modules);

modules.forkChoice.getCanonicalBlockAtSlot.mockReturnValue(null);

await expect(api.producePayloadAttestationData({slot: 1})).rejects.toThrow("No canonical block found at slot=1");
});
});
});
15 changes: 12 additions & 3 deletions packages/validator/src/services/ptc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ApiClient, routes} from "@lodestar/api";
import {ApiClient, HttpStatusCode, routes} from "@lodestar/api";
import {ChainForkConfig} from "@lodestar/config";
import {isForkPostGloas} from "@lodestar/params";
import {Slot, gloas} from "@lodestar/types";
Expand Down Expand Up @@ -70,14 +70,23 @@ export class PtcService {

try {
const payloadAttestationData = await this.producePayloadAttestationData(slot);
// If no beacon block was seen for the assigned slot, do not submit a payload attestation
if (payloadAttestationData === null) {
this.logger.debug("Skipping payload attestation, no beacon block seen for slot", {slot});
return;
}
await this.signAndPublishPayloadAttestations(slot, payloadAttestationData, duties);
} catch (e) {
this.logger.error("Error on PTC routine", {slot}, e as Error);
}
};

private async producePayloadAttestationData(slot: Slot): Promise<gloas.PayloadAttestationData> {
return (await this.api.validator.producePayloadAttestationData({slot})).value();
private async producePayloadAttestationData(slot: Slot): Promise<gloas.PayloadAttestationData | null> {
const res = await this.api.validator.producePayloadAttestationData({slot});
if (!res.ok && res.status === HttpStatusCode.NOT_FOUND) {
Comment thread
nflaig marked this conversation as resolved.
return null;
}
return res.value();
}

private async signAndPublishPayloadAttestations(
Expand Down
36 changes: 34 additions & 2 deletions packages/validator/test/unit/services/ptc.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
import {SecretKey} from "@chainsafe/blst";
import {toHexString} from "@chainsafe/ssz";
import {routes} from "@lodestar/api";
import {HttpStatusCode, routes} from "@lodestar/api";
import {createChainForkConfig} from "@lodestar/config";
import {config as defaultConfig} from "@lodestar/config/default";
import {gloas, ssz} from "@lodestar/types";
Expand All @@ -11,7 +11,7 @@ import {PtcService} from "../../../src/services/ptc.js";
import {PtcDutiesService} from "../../../src/services/ptcDuties.js";
import {SyncingStatusTracker} from "../../../src/services/syncingStatusTracker.js";
import {ValidatorStore} from "../../../src/services/validatorStore.js";
import {getApiClientStub, mockApiResponse} from "../../utils/apiStub.js";
import {getApiClientStub, mockApiErrorResponse, mockApiResponse} from "../../utils/apiStub.js";
import {ClockMock} from "../../utils/clock.js";
import {loggerVc} from "../../utils/logger.js";
import {ZERO_HASH, ZERO_HASH_HEX} from "../../utils/types.js";
Expand Down Expand Up @@ -112,6 +112,38 @@ describe("PtcService", () => {
});
});

it("Should skip submission when no beacon block has been seen for the assigned slot", async () => {
const slot = 0;
const clock = new ClockMock();
const config = createChainForkConfig({...defaultConfig, GLOAS_FORK_EPOCH: 0});
const ptcService = new PtcService(
config,
loggerVc,
api,
clock,
validatorStore,
emitter,
chainHeadTracker,
syncingStatusTracker,
null
);

const duty: routes.validator.PtcDuty = {
slot,
validatorIndex: 0,
pubkey: pubkeys[0],
};

vi.spyOn(ptcService["dutiesService"], "getDutiesAtSlot").mockReturnValue([duty]);
api.validator.producePayloadAttestationData.mockResolvedValue(mockApiErrorResponse(HttpStatusCode.NOT_FOUND));

await clock.tickSlotFns(slot, controller.signal);

expect(api.validator.producePayloadAttestationData).toHaveBeenCalledWith({slot});
expect(validatorStore.signPayloadAttestation).not.toHaveBeenCalled();
expect(api.beacon.submitPayloadAttestationMessages).not.toHaveBeenCalled();
});

it("Should redownload PTC duties when dependent root changes", async () => {
const slot = 0;
const newDependentRoot = "0x1111111111111111111111111111111111111111111111111111111111111111";
Expand Down
4 changes: 4 additions & 0 deletions packages/validator/test/utils/apiStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ export function mockApiResponse<T, M, E extends Endpoint<any, any, any, T, M>>({
apiResponse.meta = () => meta as M;
return apiResponse;
}

export function mockApiErrorResponse<E extends Endpoint>(status: HttpStatusCode): ApiResponse<E> {
return new ApiResponse<E>({} as any, null, new Response(null, {status}));
}
Loading