UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

118 lines 5.91 kB
import { WireFormat } from "@lodestar/api"; import { getClient } from "@lodestar/api/builder"; import { SLOTS_PER_EPOCH } from "@lodestar/params"; import { parseExecutionPayloadAndBlobsBundle, reconstructFullBlockOrContents } 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, }; /** * 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 { constructor(opts, config, metrics = null, logger) { // Builder needs to be explicity enabled using updateStatus this.status = false; /** * 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`. */ this.sszSupported = false; 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 // 2 * 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/2 this.allowedFaults = Math.min(opts.allowedFaults ?? Math.floor(this.faultInspectionWindow / 2), Math.floor(this.faultInspectionWindow / 2)); } updateStatus(shouldEnable) { this.status = shouldEnable; } async checkStatus() { try { (await this.api.status()).assertOk(); } catch (e) { // Disable if the status was enabled this.status = false; 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 contents = blobsBundle ? { blobs: blobsBundle.blobs, kzgProofs: blobsBundle.proofs } : null; return reconstructFullBlockOrContents(signedBlindedBlock.data, { executionPayload, contents }); } async submitBlindedBlockNoResponse(signedBlindedBlock) { (await this.api.submitBlindedBlockV2({ signedBlindedBlock }, { retries: 2, requestWireFormat: this.sszSupported ? WireFormat.ssz : WireFormat.json })).assertOk(); } } //# sourceMappingURL=http.js.map