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
47 changes: 28 additions & 19 deletions packages/builder/src/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ChainForkConfig} from "@lodestar/config";
import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
import {IClock} from "@lodestar/state-transition";
import {BuilderIndex, BuilderStatus} from "@lodestar/types";
import {ErrorAborted, Logger, sleep, toHex} from "@lodestar/utils";
import {ErrorAborted, Logger, TimeoutError, isFetchError, sleep, toHex} from "@lodestar/utils";

export const WAITING_FOR_BUILDER_POLL_MS = 10 * 1000;

Expand Down Expand Up @@ -38,7 +38,7 @@ export async function getBuilderStatus(
id: routes.beacon.BuilderId
): Promise<{status: BuilderStatus; balance: number} | null> {
try {
const builderEntry = await fetchBuilder(api, id);
const builderEntry = await fetchBuilder(api, logger, id);
if (builderEntry) {
return {
status: builderEntry.status,
Expand Down Expand Up @@ -77,7 +77,7 @@ async function waitForBuilder(

let builder: routes.beacon.BuilderResponse | null = null;
try {
builder = await fetchBuilder(api, id);
builder = await fetchBuilder(api, logger, id);
} catch (e) {
// At the fork boundary getStateBuilders("head") can still 400: it serves the head block's
// post-state, which stays pre-gloas until a gloas-epoch block is head. Keep polling on the transient.
Expand Down Expand Up @@ -111,28 +111,37 @@ async function waitForBuilder(

async function fetchBuilder(
api: ApiClient,
logger: Logger,
id: routes.beacon.BuilderId
): Promise<routes.beacon.BuilderResponse | null> {
const builderRes = await api.beacon.getStateBuilders({
stateId: "head",
builderIds: [id],
});
try {
const builderRes = await api.beacon.getStateBuilders({
stateId: "head",
builderIds: [id],
});

const builders = builderRes.value();
const builders = builderRes.value();

if (builders.length === 0) {
return null;
}
if (builders.length === 0) {
return null;
}

const builder = builders[0];
const builder = builders[0];

if (typeof id === "number") {
if (id !== builder.index) {
throw Error(`Index mismatch: got=${builder.index} expected=${id}`);
if (typeof id === "number") {
if (id !== builder.index) {
throw Error(`Index mismatch: got=${builder.index} expected=${id}`);
}
} else if (id !== toHex(builder.builder.pubkey)) {
throw Error(`Pubkey mismatch: got=${toHex(builder.builder.pubkey)} expected=${id}`);
}
} else if (id !== toHex(builder.builder.pubkey)) {
throw Error(`Pubkey mismatch: got=${toHex(builder.builder.pubkey)} expected=${id}`);
}

return builder;
return builder;
} catch (e) {
if (e instanceof TimeoutError || (isFetchError(e) && e.type !== "input")) {
logger.warn("Failed to fetch builder", {message: e.message});
return null;
}
throw e;

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.

what kind of errors do we wanna throw here?

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.

so it's just if pubkey and index mismatch, I suppose

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ErrorAborted, builderRes.value() Api errors and FetchError with type "input"

}
}
47 changes: 46 additions & 1 deletion packages/builder/test/unit/identity.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 {createChainForkConfig} from "@lodestar/config";
import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
import {ErrorAborted, toHex} from "@lodestar/utils";
import {ErrorAborted, FetchError, TimeoutError, toHex} from "@lodestar/utils";
import {WAITING_FOR_BUILDER_POLL_MS, getBuilderStatus, resolveBuilderIdentity} from "../../src/identity.js";
import {getApiClientStub, mockApiErrorResponse, mockApiResponse} from "./utils/apiStub.js";
import {ClockMock} from "./utils/clock.js";
Expand Down Expand Up @@ -179,4 +179,49 @@ describe("Identity", () => {
resolveBuilderIdentity(api, logger, pubkeyString, abortController.signal, clock, config)
).rejects.toThrow(/status 500/);
});

it("keeps polling on getStateBuilders request timeout", async () => {
vi.useFakeTimers();
api.beacon.getStateBuilders.mockRejectedValueOnce(new TimeoutError("getStateBuilders request"));
api.beacon.getStateBuilders.mockResolvedValue(
mockGetStateBuildersResponse(index, {status, pubkey, balance, version})
);
const promise = resolveBuilderIdentity(api, logger, pubkeyString, abortController.signal, clock, config);
await vi.advanceTimersByTimeAsync(WAITING_FOR_BUILDER_POLL_MS);
expect(await promise).toEqual(index);
expect(api.beacon.getStateBuilders).toHaveBeenCalledTimes(2);
});

it("keeps polling on fetch failure", async () => {
vi.useFakeTimers();
api.beacon.getStateBuilders.mockRejectedValueOnce(
new FetchError(
"http://127.0.0.1:9596",
new TypeError("fetch failed", {
cause: Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:9596"), {code: "ECONNREFUSED"}),
})
)
);
api.beacon.getStateBuilders.mockResolvedValue(
mockGetStateBuildersResponse(index, {status, pubkey, balance, version})
);
const promise = resolveBuilderIdentity(api, logger, pubkeyString, abortController.signal, clock, config);
await vi.advanceTimersByTimeAsync(WAITING_FOR_BUILDER_POLL_MS);
expect(await promise).toEqual(index);
expect(api.beacon.getStateBuilders).toHaveBeenCalledTimes(2);
});

it("throws on invalid beacon node url / invalid input", async () => {
api.beacon.getStateBuilders.mockRejectedValue(
new FetchError(
"invalid-url",
new TypeError("Failed to parse URL from invalid-url", {
cause: Object.assign(new Error("Invalid URL"), {input: "invalid-url", code: "ERR_INVALID_URL"}),
})
)
);
await expect(
resolveBuilderIdentity(api, logger, pubkeyString, abortController.signal, clock, config)
).rejects.toThrow(/Failed to parse URL/);
});
});
Loading