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