-
-
Notifications
You must be signed in to change notification settings - Fork 474
feat(builder): add readiness gating and builder identity resolution #9781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
f9c1ca7
6dd7f0c
dd2bc25
4cbb72b
0d22e6d
35aec8d
42c7085
8424ecf
14dbd09
f5f50d0
96dafe8
6764758
e90ebb2
354c6f4
87bade7
d68905f
5492130
d98ca85
0a13a30
4fa1405
926adbc
250ae7b
ded9e62
1b056f9
8c83008
51c3493
300e29c
ce6e28d
0e25f09
7386db0
81ed67f
460616e
251347b
c04b3c3
9916c34
aad1b36
7a469bb
a71c4f0
3fe4ef4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export const defaultOptions = { | ||
| // Source beacon node the builder connects to | ||
| beaconNodeUrl: "http://127.0.0.1:9596", | ||
| requestTimeout: 10_000, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import {ApiClient, routes} from "@lodestar/api"; | ||
| import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params"; | ||
| import {BuilderIndex, BuilderStatus} from "@lodestar/types"; | ||
| import {Logger, sleep, toHex} from "@lodestar/utils"; | ||
|
|
||
| export const WAITING_FOR_BUILDER_POLL_MS = 10 * 1000; | ||
|
|
||
| export async function resolveBuilderIdentity( | ||
| api: ApiClient, | ||
| logger: Logger, | ||
| id: routes.beacon.BuilderId, | ||
| signal: AbortSignal | ||
| ): Promise<BuilderIndex> { | ||
| const builderEntry = await waitForBuilder(api, logger, id, signal); | ||
|
|
||
| if (builderEntry.builder.version !== PAYLOAD_BUILDER_VERSION) { | ||
| throw Error(`Builder version mismatch: got ${builderEntry.builder.version}, expected ${PAYLOAD_BUILDER_VERSION}`); | ||
| } | ||
|
|
||
| logger.info("Builder identity resolved", { | ||
| index: builderEntry.index, | ||
| status: builderEntry.status, | ||
| balanceGwei: builderEntry.builder.balance, | ||
| executionAddress: toHex(builderEntry.builder.executionAddress), | ||
| }); | ||
|
|
||
| return builderEntry.index; | ||
| } | ||
|
|
||
| export async function getBuilderStatus( | ||
| api: ApiClient, | ||
| logger: Logger, | ||
| id: routes.beacon.BuilderId | ||
| ): Promise<{status: BuilderStatus; balance: number} | null> { | ||
| try { | ||
| const builderEntry = await fetchBuilder(api, id); | ||
| if (builderEntry) { | ||
| return { | ||
| status: builderEntry.status, | ||
| balance: builderEntry.builder.balance, | ||
| }; | ||
| } | ||
| logger.warn("Builder status not available in beacon node"); | ||
| return null; | ||
| } catch (e) { | ||
| logger.warn("Couldn't fetch the builder", {}, e as Error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| async function waitForBuilder( | ||
| api: ApiClient, | ||
| logger: Logger, | ||
| id: routes.beacon.BuilderId, | ||
| signal: AbortSignal | ||
| ): Promise<routes.beacon.BuilderResponse> { | ||
| while (true) { | ||
| const builder = await fetchBuilder(api, id); | ||
| if (builder?.status === "active") { | ||
| return builder; | ||
| } | ||
| if (builder?.status === "exited") { | ||
| throw Error(`Builder exited: id=${id}`); | ||
| } | ||
| if (builder?.status === "pending") { | ||
| logger.info("Waiting for builder deposit to be finalized", {id}); | ||
| } else { | ||
| logger.info("Waiting for builder to be known to the beacon node", {id}); | ||
| } | ||
| await sleep(WAITING_FOR_BUILDER_POLL_MS, signal); | ||
| } | ||
| } | ||
|
|
||
| async function fetchBuilder( | ||
| api: ApiClient, | ||
| id: routes.beacon.BuilderId | ||
| ): Promise<routes.beacon.BuilderResponse | null> { | ||
| const builderRes = await api.beacon.getStateBuilders({ | ||
| stateId: "head", | ||
| builderIds: [id], | ||
| }); | ||
|
|
||
| const builders = builderRes.value(); | ||
|
|
||
| if (builders.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| const builder = builders[0]; | ||
|
|
||
| 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}`); | ||
| } | ||
|
|
||
| return builder; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import {ApiClient} from "@lodestar/api"; | ||
| import {Logger, sleep} from "@lodestar/utils"; | ||
|
|
||
| /** The time between polls when waiting for BN to be ready */ | ||
| const WAITING_FOR_NODE_READY_POLL_MS = 5 * 1000; | ||
|
|
||
| export async function waitForNodeReady(api: ApiClient, logger: Logger, signal: AbortSignal): Promise<void> { | ||
| while (!(await isNodeReady(api, logger))) { | ||
| await sleep(WAITING_FOR_NODE_READY_POLL_MS, signal); | ||
| } | ||
| } | ||
|
|
||
| async function isNodeReady(api: ApiClient, logger: Logger): Promise<boolean> { | ||
| try { | ||
| const syncingStatusRes = await api.node.getSyncingStatus(); | ||
|
|
||
| if (!syncingStatusRes.ok) { | ||
| logger.warn("Cannot get node sync status", { | ||
| status: syncingStatusRes.status, | ||
| message: syncingStatusRes.error()?.message, | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| const syncingStatus = syncingStatusRes.value(); | ||
|
|
||
| if (syncingStatus.isSyncing || syncingStatus.elOffline) { | ||
| logger.info( | ||
| syncingStatus.elOffline ? "Beacon node EL is offline, unable to submit bids" : "Beacon node is not ready yet", | ||
| { | ||
| headSlot: syncingStatus.headSlot, | ||
| syncDistance: syncingStatus.syncDistance, | ||
| elOffline: syncingStatus.elOffline, | ||
| } | ||
| ); | ||
| return false; | ||
| } | ||
|
|
||
| if (syncingStatus.isOptimistic) { | ||
| logger.warn("Beacon node head is optimistic, execution payloads are not yet verified - unable to submit bids"); | ||
| return false; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so for the operator it should be clear that in case the EL is syncing we won't be able to submit any bids
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added proper messaging on both |
||
|
|
||
| logger.info("Beacon node is ready", {headSlot: syncingStatus.headSlot}); | ||
|
|
||
| return true; | ||
| } catch (e) { | ||
| logger.warn("Cannot reach the beacon node", {}, e as Error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export async function logNodeVersion(api: ApiClient, logger: Logger): Promise<void> { | ||
|
nflaig marked this conversation as resolved.
|
||
| try { | ||
| const versionRes = await api.node.getNodeVersionV2(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some clients may not have this api yet, but that's fine for now, we might need to bully them a bit to implement it, but since it's gloas related, it's fine to assume it's implemented by clients |
||
| const version = versionRes.value(); | ||
| logger.info("Connected node version", { | ||
| beaconNode: `${version.beaconNode.name}/${version.beaconNode.version}`, | ||
| executionClient: version.executionClient | ||
| ? `${version.executionClient.name}/${version.executionClient.version}` | ||
| : "unknown", | ||
| }); | ||
| } catch (e) { | ||
| logger.warn("Failed to get node version", {}, e as Error); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import {ApiClient} from "@lodestar/api"; | ||
| import {BuilderIndex, BuilderStatus} from "@lodestar/types"; | ||
| import {Logger} from "@lodestar/utils"; | ||
| import {getBuilderStatus} from "../identity.js"; | ||
|
|
||
| /** | ||
| * Service for tracking builder status. | ||
| * Provides regular builder status and balance updates for operator diagnostics. | ||
| */ | ||
| export class BuilderStatusTracker { | ||
|
nflaig marked this conversation as resolved.
|
||
| private readonly api: ApiClient; | ||
| private readonly logger: Logger; | ||
| private readonly index: BuilderIndex; | ||
|
|
||
| private status?: BuilderStatus; | ||
| private balanceGwei?: number; | ||
|
|
||
| constructor(api: ApiClient, logger: Logger, index: BuilderIndex) { | ||
| this.api = api; | ||
| this.logger = logger; | ||
| this.index = index; | ||
| } | ||
|
|
||
| async poll() { | ||
| const builderStatus = await getBuilderStatus(this.api, this.logger, this.index); | ||
| if (builderStatus !== null) { | ||
| if (this.status !== undefined && this.status !== builderStatus.status) { | ||
| this.logger.info("Builder status changed", {from: this.status, to: builderStatus.status}); | ||
| } | ||
| this.status = builderStatus.status; | ||
| this.balanceGwei = builderStatus.balance; | ||
| this.logger.info("Builder status", {status: builderStatus.status, balance: builderStatus.balance}); | ||
| } | ||
| } | ||
|
|
||
| getStatus(): {status: BuilderStatus | undefined; balance: number | undefined} { | ||
| return { | ||
| status: this.status, | ||
| balance: this.balanceGwei, | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.