UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

144 lines 6.71 kB
import { WireFormat } from "@lodestar/api"; import { getClient } from "@lodestar/api/builder"; import { SLOTS_PER_EPOCH } from "@lodestar/params"; import { parseExecutionPayloadAndBlobsBundle, reconstructSignedBlockContents } from "@lodestar/state-transition"; import { toPrintableUrl } from "@lodestar/utils"; import { ValidatorRegistrationCache } from "./cache.js"; export const defaultExecutionBuilderHttpOpts = { enabled: false, url: "http://localhost:8661", timeout: 12000, }; export { BuilderStatus }; var BuilderStatus; (function (BuilderStatus) { /** * Builder is enabled and operational */ BuilderStatus["enabled"] = "enabled"; /** * Builder is disabled due to failed status check */ BuilderStatus["disabled"] = "disabled"; /** * Circuit breaker condition that is triggered when the node determines the chain is unhealthy. * When the circuit breaker is fired, proposers **MUST** not utilize the external builder * network and exclusively build locally. */ BuilderStatus["circuitBreaker"] = "circuit_breaker"; })(BuilderStatus || (BuilderStatus = {})); /** * Expected error if builder does not provide a bid. Most of the time, this * is due to `min-bid` setting on the mev-boost side but in rare cases could * also happen if there are no bids from any of the connected relayers. */ export class NoBidReceived extends Error { constructor() { super("No bid received"); } } /** * Additional duration to account for potential event loop lag which causes * builder blocks to be rejected even though the response was sent in time. */ const EVENT_LOOP_LAG_BUFFER = 250; /** * Duration given to the builder to provide a `SignedBuilderBid` before the deadline * is reached, aborting the external builder flow in favor of the local build process. */ const BUILDER_PROPOSAL_DELAY_TOLERANCE = 1000 + EVENT_LOOP_LAG_BUFFER; export class ExecutionBuilderHttp { api; config; registrations; issueLocalFcUWithFeeRecipient; // Builder needs to be explicity enabled using updateStatus status = BuilderStatus.disabled; faultInspectionWindow; allowedFaults; /** * Determine if SSZ is supported by requesting an SSZ encoded response in the `getHeader` request. * The builder responding with a SSZ serialized `SignedBuilderBid` indicates support to handle the * `SignedBlindedBeaconBlock` as SSZ serialized bytes instead of JSON when calling `submitBlindedBlock`. */ sszSupported = false; constructor(opts, config, metrics = null, logger) { const baseUrl = opts.url; if (!baseUrl) throw Error("No Url provided for executionBuilder"); this.api = getClient({ baseUrl, globalInit: { timeoutMs: opts.timeout, headers: opts.userAgent ? { "User-Agent": opts.userAgent } : undefined, }, }, { config, metrics: metrics?.builderHttpClient, logger }); logger?.info("External builder", { url: toPrintableUrl(baseUrl) }); this.config = config; this.registrations = new ValidatorRegistrationCache(); this.issueLocalFcUWithFeeRecipient = opts.issueLocalFcUWithFeeRecipient; /** * Beacon clients select randomized values from the following ranges when initializing * the circuit breaker (so at boot time and once for each unique boot). * * ALLOWED_FAULTS: between 1 and SLOTS_PER_EPOCH // 4 * FAULT_INSPECTION_WINDOW: between SLOTS_PER_EPOCH and 2 * SLOTS_PER_EPOCH * */ this.faultInspectionWindow = Math.max(opts.faultInspectionWindow ?? SLOTS_PER_EPOCH + Math.floor(Math.random() * SLOTS_PER_EPOCH), SLOTS_PER_EPOCH); // allowedFaults should be < faultInspectionWindow, limiting them to faultInspectionWindow/4 this.allowedFaults = Math.min(opts.allowedFaults ?? Math.floor(this.faultInspectionWindow / 4), Math.floor(this.faultInspectionWindow / 4)); } updateStatus(status) { this.status = status; } async checkStatus() { try { (await this.api.status()).assertOk(); } catch (e) { if (this.status === BuilderStatus.enabled) { // Disable if the status was enabled this.status = BuilderStatus.disabled; } throw e; } } async registerValidator(epoch, registrations) { (await this.api.registerValidator({ registrations })).assertOk(); for (const registration of registrations) { this.registrations.add(epoch, registration.message); } this.registrations.prune(epoch); } getValidatorRegistration(pubkey) { return this.registrations.get(pubkey); } async getHeader(_fork, slot, parentHash, proposerPubkey) { const res = await this.api.getHeader({ slot, parentHash, proposerPubkey }, { timeoutMs: BUILDER_PROPOSAL_DELAY_TOLERANCE }); const signedBuilderBid = res.value(); if (!signedBuilderBid) { throw new NoBidReceived(); } this.sszSupported = res.wireFormat() === WireFormat.ssz; const { header, value: executionPayloadValue } = signedBuilderBid.message; const { blobKzgCommitments } = signedBuilderBid.message; const { executionRequests } = signedBuilderBid.message; return { header, executionPayloadValue, blobKzgCommitments, executionRequests }; } async submitBlindedBlock(signedBlindedBlock) { const res = await this.api.submitBlindedBlock({ signedBlindedBlock }, { retries: 2, requestWireFormat: this.sszSupported ? WireFormat.ssz : WireFormat.json }); const { executionPayload, blobsBundle } = parseExecutionPayloadAndBlobsBundle(res.value()); // for the sake of timely proposals we can skip matching the payload with payloadHeader // if the roots (transactions, withdrawals) don't match, this will likely lead to a block with // invalid signature, but there is no recourse to this anyway so lets just proceed and will // probably need diagonis if this block turns out to be invalid because of some bug // const fork = this.config.getForkName(signedBlindedBlock.data.message.slot); return reconstructSignedBlockContents(fork, signedBlindedBlock.data, executionPayload, blobsBundle); } async submitBlindedBlockNoResponse(signedBlindedBlock) { (await this.api.submitBlindedBlockV2({ signedBlindedBlock }, { retries: 2, requestWireFormat: this.sszSupported ? WireFormat.ssz : WireFormat.json })).assertOk(); } } //# sourceMappingURL=http.js.map