@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
118 lines • 5.91 kB
JavaScript
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