-
-
Notifications
You must be signed in to change notification settings - Fork 318
/
Copy pathhttp.ts
195 lines (174 loc) Β· 6.67 KB
/
http.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import {WireFormat} from "@lodestar/api";
import {ApiClient as BuilderApi, getClient} from "@lodestar/api/builder";
import {ChainForkConfig} from "@lodestar/config";
import {Logger} from "@lodestar/logger";
import {ForkExecution, SLOTS_PER_EPOCH} from "@lodestar/params";
import {parseExecutionPayloadAndBlobsBundle, reconstructFullBlockOrContents} from "@lodestar/state-transition";
import {
BLSPubkey,
Epoch,
ExecutionPayloadHeader,
Root,
SignedBeaconBlockOrContents,
SignedBlindedBeaconBlock,
Slot,
Wei,
WithOptionalBytes,
bellatrix,
deneb,
electra,
} from "@lodestar/types";
import {toPrintableUrl} from "@lodestar/utils";
import {Metrics} from "../../metrics/metrics.js";
import {ValidatorRegistration, ValidatorRegistrationCache} from "./cache.js";
import {IExecutionBuilder} from "./interface.js";
export type ExecutionBuilderHttpOpts = {
enabled: boolean;
url: string;
timeout?: number;
faultInspectionWindow?: number;
allowedFaults?: number;
// Only required for merge-mock runs, no need to expose it to cli
issueLocalFcUWithFeeRecipient?: string;
// Add User-Agent header to all requests
userAgent?: string;
};
export const defaultExecutionBuilderHttpOpts: ExecutionBuilderHttpOpts = {
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");
}
}
/**
* 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;
export class ExecutionBuilderHttp implements IExecutionBuilder {
readonly api: BuilderApi;
readonly config: ChainForkConfig;
readonly registrations: ValidatorRegistrationCache;
readonly issueLocalFcUWithFeeRecipient?: string;
// Builder needs to be explicity enabled using updateStatus
status = false;
faultInspectionWindow: number;
allowedFaults: number;
/**
* 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`.
*/
private sszSupported = false;
constructor(
opts: ExecutionBuilderHttpOpts,
config: ChainForkConfig,
metrics: Metrics | null = null,
logger?: 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 // 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: boolean): void {
this.status = shouldEnable;
}
async checkStatus(): Promise<void> {
try {
(await this.api.status()).assertOk();
} catch (e) {
// Disable if the status was enabled
this.status = false;
throw e;
}
}
async registerValidator(epoch: Epoch, registrations: bellatrix.SignedValidatorRegistrationV1[]): Promise<void> {
(await this.api.registerValidator({registrations})).assertOk();
for (const registration of registrations) {
this.registrations.add(epoch, registration.message);
}
this.registrations.prune(epoch);
}
getValidatorRegistration(pubkey: BLSPubkey): ValidatorRegistration | undefined {
return this.registrations.get(pubkey);
}
async getHeader(
_fork: ForkExecution,
slot: Slot,
parentHash: Root,
proposerPubkey: BLSPubkey
): Promise<{
header: ExecutionPayloadHeader;
executionPayloadValue: Wei;
blobKzgCommitments?: deneb.BlobKzgCommitments;
executionRequests?: electra.ExecutionRequests;
}> {
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 as deneb.BuilderBid;
const {executionRequests} = signedBuilderBid.message as electra.BuilderBid;
return {header, executionPayloadValue, blobKzgCommitments, executionRequests};
}
async submitBlindedBlock(
signedBlindedBlock: WithOptionalBytes<SignedBlindedBeaconBlock>
): Promise<SignedBeaconBlockOrContents> {
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});
}
}