@lodestar/validator 1.45.0-dev.51a1c44b27 → 1.45.0-dev.6568180f96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/metrics.d.ts +3 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +5 -0
- package/lib/metrics.js.map +1 -1
- package/lib/services/block.d.ts +1 -0
- package/lib/services/block.d.ts.map +1 -1
- package/lib/services/block.js +86 -40
- package/lib/services/block.js.map +1 -1
- package/lib/services/emitter.d.ts +0 -4
- package/lib/services/emitter.d.ts.map +1 -1
- package/lib/services/emitter.js +0 -18
- package/lib/services/emitter.js.map +1 -1
- package/lib/services/proposerPreferences.d.ts +6 -1
- package/lib/services/proposerPreferences.d.ts.map +1 -1
- package/lib/services/proposerPreferences.js +20 -3
- package/lib/services/proposerPreferences.js.map +1 -1
- package/lib/services/ptc.d.ts +5 -1
- package/lib/services/ptc.d.ts.map +1 -1
- package/lib/services/ptc.js +32 -13
- package/lib/services/ptc.js.map +1 -1
- package/lib/services/validatorStore.d.ts +1 -1
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +12 -6
- package/lib/services/validatorStore.js.map +1 -1
- package/lib/util/params.js +5 -2
- package/lib/util/params.js.map +1 -1
- package/lib/validator.d.ts +1 -0
- package/lib/validator.d.ts.map +1 -1
- package/lib/validator.js +3 -0
- package/lib/validator.js.map +1 -1
- package/package.json +15 -15
- package/src/metrics.ts +6 -0
- package/src/services/block.ts +111 -51
- package/src/services/emitter.ts +0 -21
- package/src/services/proposerPreferences.ts +20 -3
- package/src/services/ptc.ts +35 -15
- package/src/services/validatorStore.ts +22 -7
- package/src/util/params.ts +5 -2
- package/src/validator.ts +4 -0
package/src/services/block.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ApiClient, routes} from "@lodestar/api";
|
|
2
2
|
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
-
import {BUILDER_INDEX_SELF_BUILD, isForkPostGloas} from "@lodestar/params";
|
|
3
|
+
import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, isForkPostGloas} from "@lodestar/params";
|
|
4
4
|
import {
|
|
5
5
|
BLSPubkey,
|
|
6
6
|
BLSSignature,
|
|
@@ -39,6 +39,7 @@ type DebugLogCtx = {debugLogCtx: Record<string, string | boolean | undefined>};
|
|
|
39
39
|
type BlockProposalOpts = {
|
|
40
40
|
broadcastValidation: routes.beacon.BroadcastValidation;
|
|
41
41
|
blindedLocal: boolean;
|
|
42
|
+
payloadLocal: boolean;
|
|
42
43
|
};
|
|
43
44
|
/**
|
|
44
45
|
* Service that sets up and handles validator block proposal duties.
|
|
@@ -166,11 +167,19 @@ export class BlockProposingService {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/**
|
|
169
|
-
* Gloas
|
|
170
|
-
* 1. Produce beacon block
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
170
|
+
* Gloas block production flow:
|
|
171
|
+
* 1. Produce the beacon block, which commits to an execution payload bid. When self-building with
|
|
172
|
+
* the stateless flow (`payloadLocal=false`), the response also includes the full block contents
|
|
173
|
+
* (execution payload envelope, KZG proofs and blobs).
|
|
174
|
+
* 2. Sign and publish the beacon block.
|
|
175
|
+
* 3. Reveal the execution payload envelope:
|
|
176
|
+
* - Self-build: the proposer signs and publishes the envelope
|
|
177
|
+
* - Stateless (`payloadLocal=false`): envelope and blobs are already available from step 1,
|
|
178
|
+
* publish `SignedExecutionPayloadEnvelopeContents` which can be sent via any beacon node
|
|
179
|
+
* - Stateful (`payloadLocal=true`): fetch the envelope from the beacon node that produced the
|
|
180
|
+
* block, then publish the bare `SignedExecutionPayloadEnvelope` back to it; that node
|
|
181
|
+
* attaches the cached blobs and KZG proofs
|
|
182
|
+
* - Builder bid: the builder reveals the envelope, so the proposer does nothing further
|
|
174
183
|
*/
|
|
175
184
|
private async createAndPublishBlockGloas(pubkey: BLSPubkey, slot: Slot): Promise<void> {
|
|
176
185
|
const pubkeyHex = toPubkeyHex(pubkey);
|
|
@@ -180,8 +189,11 @@ export class BlockProposingService {
|
|
|
180
189
|
const randaoReveal = await this.validatorStore.signRandao(pubkey, slot);
|
|
181
190
|
const graffiti = this.validatorStore.getGraffiti(pubkeyHex);
|
|
182
191
|
const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex);
|
|
192
|
+
const {broadcastValidation, payloadLocal} = this.opts;
|
|
193
|
+
const {selection: builderSelection, boostFactor: builderBoostFactor} =
|
|
194
|
+
this.validatorStore.getBuilderSelectionParams(pubkeyHex, slot);
|
|
183
195
|
|
|
184
|
-
this.logger.debug("Producing block", {...debugLogCtx, feeRecipient});
|
|
196
|
+
this.logger.debug("Producing block", {...debugLogCtx, feeRecipient, payloadLocal, builderSelection});
|
|
185
197
|
this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot));
|
|
186
198
|
|
|
187
199
|
// Step 1: Produce beacon block with execution payload bid
|
|
@@ -191,19 +203,29 @@ export class BlockProposingService {
|
|
|
191
203
|
randaoReveal,
|
|
192
204
|
graffiti,
|
|
193
205
|
feeRecipient,
|
|
206
|
+
includePayload: !payloadLocal,
|
|
207
|
+
builderSelection,
|
|
208
|
+
builderBoostFactor,
|
|
194
209
|
})
|
|
195
210
|
.catch((e: Error) => {
|
|
196
211
|
this.metrics?.blockProposingErrors.inc({error: "produce"});
|
|
197
212
|
throw extendError(e, "Failed to produce block");
|
|
198
213
|
});
|
|
199
|
-
const
|
|
214
|
+
const blockOrContents = blockRes.value();
|
|
200
215
|
const blockMeta = blockRes.meta();
|
|
216
|
+
const {executionPayloadIncluded} = blockMeta;
|
|
217
|
+
const block = executionPayloadIncluded
|
|
218
|
+
? (blockOrContents as BlockContents<ForkPostGloas>).block
|
|
219
|
+
: (blockOrContents as BeaconBlock<ForkPostGloas>);
|
|
201
220
|
const beaconBlockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block);
|
|
202
221
|
const blockRootHex = toRootHex(beaconBlockRoot);
|
|
203
222
|
|
|
204
223
|
this.logger.debug("Produced block", {
|
|
205
224
|
...debugLogCtx,
|
|
225
|
+
executionPayloadValue: prettyWeiToEth(blockMeta.executionPayloadValue),
|
|
206
226
|
consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
|
|
227
|
+
totalBlockValue: prettyWeiToEth(blockMeta.executionPayloadValue + blockMeta.consensusBlockValue),
|
|
228
|
+
executionPayloadIncluded,
|
|
207
229
|
blockRoot: blockRootHex,
|
|
208
230
|
});
|
|
209
231
|
this.metrics?.blocksProduced.inc();
|
|
@@ -211,8 +233,6 @@ export class BlockProposingService {
|
|
|
211
233
|
// Step 2: Sign and publish the beacon block
|
|
212
234
|
const signedBlock = await this.validatorStore.signBlock(pubkey, block, slot, this.logger);
|
|
213
235
|
|
|
214
|
-
const {broadcastValidation} = this.opts;
|
|
215
|
-
|
|
216
236
|
// Publish the block first so it propagates as soon as possible. This reduces the chance other nodes
|
|
217
237
|
// see the payload envelope before the block over gossip and have to queue it. There's also plenty of
|
|
218
238
|
// time left in the slot to propagate the payload, so publishing it in parallel is unnecessary.
|
|
@@ -224,63 +244,105 @@ export class BlockProposingService {
|
|
|
224
244
|
})
|
|
225
245
|
.catch((e: Error) => {
|
|
226
246
|
this.metrics?.blockProposingErrors.inc({error: "publish"});
|
|
227
|
-
throw extendError(e,
|
|
247
|
+
throw extendError(e, `Failed to publish block slot=${slot} blockRoot=${blockRootHex}`);
|
|
228
248
|
})
|
|
229
249
|
).assertOk();
|
|
230
250
|
|
|
231
|
-
this.logger.
|
|
251
|
+
this.logger.info("Published beacon block", {
|
|
252
|
+
...logCtx,
|
|
253
|
+
graffiti,
|
|
254
|
+
executionPayloadValue: prettyWeiToEth(blockMeta.executionPayloadValue),
|
|
255
|
+
consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
|
|
256
|
+
totalBlockValue: prettyWeiToEth(blockMeta.executionPayloadValue + blockMeta.consensusBlockValue),
|
|
257
|
+
blockRoot: blockRootHex,
|
|
258
|
+
broadcastValidation,
|
|
259
|
+
});
|
|
260
|
+
this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
|
|
261
|
+
this.metrics?.blocksPublished.inc();
|
|
232
262
|
|
|
233
263
|
const isSelfBuild = block.body.signedExecutionPayloadBid.message.builderIndex === BUILDER_INDEX_SELF_BUILD;
|
|
234
264
|
|
|
235
265
|
if (isSelfBuild) {
|
|
236
266
|
// Self-build: proposer is responsible for building and publishing the execution payload envelope
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
const flow = executionPayloadIncluded ? "stateless" : "stateful";
|
|
268
|
+
if (executionPayloadIncluded) {
|
|
269
|
+
// Stateless flow: envelope and blobs are already available from block production
|
|
270
|
+
const {executionPayloadEnvelope, kzgProofs, blobs} = blockOrContents as BlockContents<ForkPostGloas>;
|
|
271
|
+
|
|
272
|
+
// Step 3: Sign and publish the envelope with blobs and KZG proofs
|
|
273
|
+
const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(
|
|
274
|
+
pubkey,
|
|
275
|
+
executionPayloadEnvelope,
|
|
276
|
+
slot,
|
|
277
|
+
this.logger
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
(
|
|
281
|
+
await this.api.beacon
|
|
282
|
+
.publishExecutionPayloadEnvelope({
|
|
283
|
+
signedEnvelopeOrContents: {signedExecutionPayloadEnvelope: signedEnvelope, kzgProofs, blobs},
|
|
284
|
+
broadcastValidation,
|
|
285
|
+
})
|
|
286
|
+
.catch((e: Error) => {
|
|
287
|
+
this.metrics?.payloadEnvelopeProposingErrors.inc({error: "publish"});
|
|
288
|
+
throw extendError(
|
|
289
|
+
e,
|
|
290
|
+
`Failed to publish execution payload envelope slot=${slot} blockRoot=${blockRootHex} flow=${flow}`
|
|
291
|
+
);
|
|
292
|
+
})
|
|
293
|
+
).assertOk();
|
|
294
|
+
} else {
|
|
295
|
+
// Stateful flow: fetch the envelope from the same beacon node that produced the block
|
|
296
|
+
const envelopeRes = await this.api.validator
|
|
297
|
+
.getExecutionPayloadEnvelope({
|
|
298
|
+
slot,
|
|
299
|
+
beaconBlockRoot,
|
|
258
300
|
})
|
|
259
301
|
.catch((e: Error) => {
|
|
260
|
-
this.metrics?.
|
|
261
|
-
throw extendError(e,
|
|
262
|
-
})
|
|
263
|
-
|
|
302
|
+
this.metrics?.payloadEnvelopeProposingErrors.inc({error: "produce"});
|
|
303
|
+
throw extendError(e, `Failed to get execution payload envelope slot=${slot} blockRoot=${blockRootHex}`);
|
|
304
|
+
});
|
|
305
|
+
const envelope = envelopeRes.value();
|
|
306
|
+
|
|
307
|
+
this.logger.debug("Retrieved execution payload envelope", debugLogCtx);
|
|
308
|
+
|
|
309
|
+
// Step 3: Sign and publish the envelope, beacon node attaches blobs and KZG proofs from its cache
|
|
310
|
+
const signedEnvelope = await this.validatorStore.signExecutionPayloadEnvelope(
|
|
311
|
+
pubkey,
|
|
312
|
+
envelope,
|
|
313
|
+
slot,
|
|
314
|
+
this.logger
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
(
|
|
318
|
+
await this.api.beacon
|
|
319
|
+
.publishExecutionPayloadEnvelope({
|
|
320
|
+
signedEnvelopeOrContents: signedEnvelope,
|
|
321
|
+
broadcastValidation,
|
|
322
|
+
})
|
|
323
|
+
.catch((e: Error) => {
|
|
324
|
+
this.metrics?.payloadEnvelopeProposingErrors.inc({error: "publish"});
|
|
325
|
+
throw extendError(
|
|
326
|
+
e,
|
|
327
|
+
`Failed to publish execution payload envelope slot=${slot} blockRoot=${blockRootHex} flow=${flow}`
|
|
328
|
+
);
|
|
329
|
+
})
|
|
330
|
+
).assertOk();
|
|
331
|
+
}
|
|
264
332
|
|
|
265
|
-
this.logger.info("Published
|
|
333
|
+
this.logger.info("Published execution payload envelope", {
|
|
266
334
|
...logCtx,
|
|
267
|
-
graffiti,
|
|
268
|
-
consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
|
|
269
335
|
blockRoot: blockRootHex,
|
|
336
|
+
flow,
|
|
270
337
|
});
|
|
271
338
|
} else {
|
|
272
|
-
//
|
|
273
|
-
this.logger.info("
|
|
339
|
+
// Committed to a builder bid, the builder is responsible for revealing the execution payload envelope
|
|
340
|
+
this.logger.info("Execution payload envelope to be revealed by builder", {
|
|
274
341
|
...logCtx,
|
|
275
|
-
graffiti,
|
|
276
342
|
builderIndex: block.body.signedExecutionPayloadBid.message.builderIndex,
|
|
277
|
-
consensusBlockValue: prettyWeiToEth(blockMeta.consensusBlockValue),
|
|
278
343
|
blockRoot: blockRootHex,
|
|
279
344
|
});
|
|
280
345
|
}
|
|
281
|
-
|
|
282
|
-
this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
|
|
283
|
-
this.metrics?.blocksPublished.inc();
|
|
284
346
|
}
|
|
285
347
|
|
|
286
348
|
private publishBlockWrapper = async (
|
|
@@ -353,10 +415,8 @@ function parseProduceBlockResponse(
|
|
|
353
415
|
const executionPayloadSource = response.executionPayloadSource;
|
|
354
416
|
|
|
355
417
|
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
(builderSelection === routes.validator.BuilderSelection.ExecutionOnly &&
|
|
359
|
-
executionPayloadSource === ProducedBlockSource.builder)
|
|
418
|
+
builderSelection === routes.validator.BuilderSelection.ExecutionOnly &&
|
|
419
|
+
executionPayloadSource === ProducedBlockSource.builder
|
|
360
420
|
) {
|
|
361
421
|
throw Error(
|
|
362
422
|
`Block not produced as per desired builderSelection=${builderSelection} executionPayloadSource=${executionPayloadSource}`
|
package/src/services/emitter.ts
CHANGED
|
@@ -50,25 +50,4 @@ export class ValidatorEventEmitter extends (EventEmitter as {
|
|
|
50
50
|
this.on(ValidatorEvent.chainHead, headListener);
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Wait for the first execution payload availability event to come with slot >= provided slot.
|
|
56
|
-
*/
|
|
57
|
-
async waitForExecutionPayloadAvailableSlot(slot: Slot): Promise<void> {
|
|
58
|
-
let payloadListener: (payload: ExecutionPayloadAvailableEventData) => void;
|
|
59
|
-
|
|
60
|
-
const onDone = (): void => {
|
|
61
|
-
this.off(ValidatorEvent.executionPayloadAvailable, payloadListener);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return new Promise((resolve) => {
|
|
65
|
-
payloadListener = (payload): void => {
|
|
66
|
-
if (payload.slot >= slot) {
|
|
67
|
-
onDone();
|
|
68
|
-
resolve();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
this.on(ValidatorEvent.executionPayloadAvailable, payloadListener);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
53
|
}
|
|
@@ -29,7 +29,10 @@ type SubmittedAtEpoch = {dependentRoot: RootHex; slots: Set<Slot>};
|
|
|
29
29
|
* dependent root for an epoch shifts (e.g. after a reorg) — detected by comparing the cached
|
|
30
30
|
* `dependentRoot` reported by `BlockDutiesService` against the one we last submitted under.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* Proposers should broadcast their preferences before the fork so the proposer preference caches
|
|
33
|
+
* of beacon nodes and builders are warm for the first Gloas slots. We start submitting
|
|
34
|
+
* as soon as a duty's proposal slot is in Gloas, which is up to `SUBMIT_BEFORE_PROPOSAL_SLOTS`
|
|
35
|
+
* before the fork, so only the first few Gloas slots are affected by this pre-fork submission.
|
|
33
36
|
*/
|
|
34
37
|
export class ProposerPreferencesService {
|
|
35
38
|
private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
|
|
@@ -44,10 +47,14 @@ export class ProposerPreferencesService {
|
|
|
44
47
|
_metrics: Metrics | null
|
|
45
48
|
) {
|
|
46
49
|
clock.runEverySlot(this.runProposerPreferencesTask);
|
|
50
|
+
clock.runEveryEpoch(this.pruneSubmitted);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
private runProposerPreferencesTask = async (slot: Slot): Promise<void> => {
|
|
50
|
-
|
|
54
|
+
// Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
|
|
55
|
+
// Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
|
|
56
|
+
// submit bids for the first Gloas slots.
|
|
57
|
+
if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
|
|
51
58
|
return;
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -82,6 +89,7 @@ export class ProposerPreferencesService {
|
|
|
82
89
|
for (const duty of dutiesAtEpoch.data) {
|
|
83
90
|
if (duty.slot <= slot) continue;
|
|
84
91
|
if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
|
|
92
|
+
if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
|
|
85
93
|
if (submission.slots.has(duty.slot)) continue;
|
|
86
94
|
|
|
87
95
|
try {
|
|
@@ -110,7 +118,7 @@ export class ProposerPreferencesService {
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
try {
|
|
113
|
-
await this.api.
|
|
121
|
+
await this.api.validator.submitProposerPreferences({signedProposerPreferences: batch});
|
|
114
122
|
// Only mark as submitted after the API call succeeds; a thrown error leaves the
|
|
115
123
|
// slot eligible for retry on the next tick.
|
|
116
124
|
for (const {submission, slot: submittedSlot} of pending) {
|
|
@@ -121,4 +129,13 @@ export class ProposerPreferencesService {
|
|
|
121
129
|
this.logger.error("Error submitting signed proposer preferences", {count: batch.length}, e as Error);
|
|
122
130
|
}
|
|
123
131
|
};
|
|
132
|
+
|
|
133
|
+
/** Drop tracking for past epochs; only currentEpoch and currentEpoch + 1 are ever processed. */
|
|
134
|
+
private pruneSubmitted = async (epoch: Epoch): Promise<void> => {
|
|
135
|
+
for (const trackedEpoch of this.submitted.keys()) {
|
|
136
|
+
if (trackedEpoch < epoch) {
|
|
137
|
+
this.submitted.delete(trackedEpoch);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
124
141
|
}
|
package/src/services/ptc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ApiClient,
|
|
1
|
+
import {ApiClient, routes} from "@lodestar/api";
|
|
2
2
|
import {ChainForkConfig} from "@lodestar/config";
|
|
3
3
|
import {isForkPostGloas} from "@lodestar/params";
|
|
4
4
|
import {Slot, gloas} from "@lodestar/types";
|
|
@@ -7,7 +7,7 @@ import {Metrics} from "../metrics.js";
|
|
|
7
7
|
import {PubkeyHex} from "../types.js";
|
|
8
8
|
import {IClock, LoggerVc} from "../util/index.js";
|
|
9
9
|
import {ChainHeaderTracker} from "./chainHeaderTracker.js";
|
|
10
|
-
import {ValidatorEventEmitter} from "./emitter.js";
|
|
10
|
+
import {ExecutionPayloadAvailableEventData, ValidatorEvent, ValidatorEventEmitter} from "./emitter.js";
|
|
11
11
|
import {PtcDutiesService} from "./ptcDuties.js";
|
|
12
12
|
import {SyncingStatusTracker} from "./syncingStatusTracker.js";
|
|
13
13
|
import {ValidatorStore} from "./validatorStore.js";
|
|
@@ -25,7 +25,7 @@ export class PtcService {
|
|
|
25
25
|
private readonly clock: IClock,
|
|
26
26
|
private readonly validatorStore: ValidatorStore,
|
|
27
27
|
private readonly emitter: ValidatorEventEmitter,
|
|
28
|
-
chainHeadTracker: ChainHeaderTracker,
|
|
28
|
+
private readonly chainHeadTracker: ChainHeaderTracker,
|
|
29
29
|
syncingStatusTracker: SyncingStatusTracker,
|
|
30
30
|
private readonly metrics: Metrics | null
|
|
31
31
|
) {
|
|
@@ -59,19 +59,26 @@ export class PtcService {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const payloadAttestationDueMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
// Submit as soon as the canonical head block's payload is available, or at the deadline
|
|
63
|
+
const payloadAvailable = new AbortController();
|
|
64
|
+
try {
|
|
65
|
+
await Promise.race([
|
|
66
|
+
sleep(payloadAttestationDueMs - this.clock.msFromSlot(slot), signal),
|
|
67
|
+
this.waitForCanonicalPayload(slot, payloadAvailable.signal),
|
|
68
|
+
]);
|
|
69
|
+
} finally {
|
|
70
|
+
payloadAvailable.abort();
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
this.metrics?.ptcStepCallProducePayloadAttestation.observe(
|
|
68
74
|
this.clock.secFromSlot(slot) - payloadAttestationDueMs / 1000
|
|
69
75
|
);
|
|
70
76
|
|
|
71
77
|
try {
|
|
72
|
-
|
|
78
|
+
// No canonical block at slot resolves to `undefined`
|
|
79
|
+
const payloadAttestationData = (await this.api.validator.producePayloadAttestationData({slot})).value();
|
|
73
80
|
// If no beacon block was seen for the assigned slot, do not submit a payload attestation
|
|
74
|
-
if (payloadAttestationData
|
|
81
|
+
if (!payloadAttestationData) {
|
|
75
82
|
this.logger.debug("Skipping payload attestation, no beacon block seen for slot", {slot});
|
|
76
83
|
return;
|
|
77
84
|
}
|
|
@@ -81,12 +88,25 @@ export class PtcService {
|
|
|
81
88
|
}
|
|
82
89
|
};
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Resolve when the payload for the canonical head block at `slot` is available
|
|
93
|
+
*/
|
|
94
|
+
private waitForCanonicalPayload(slot: Slot, signal: AbortSignal): Promise<void> {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const onPayloadAvailable = (payload: ExecutionPayloadAvailableEventData): void => {
|
|
97
|
+
const head = this.chainHeadTracker.getCurrentChainHead(slot);
|
|
98
|
+
if (payload.slot === slot && head !== null && payload.blockRoot === toRootHex(head)) {
|
|
99
|
+
this.emitter.off(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable);
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
signal.addEventListener(
|
|
104
|
+
"abort",
|
|
105
|
+
() => this.emitter.off(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable),
|
|
106
|
+
{once: true}
|
|
107
|
+
);
|
|
108
|
+
this.emitter.on(ValidatorEvent.executionPayloadAvailable, onPayloadAvailable);
|
|
109
|
+
});
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
private async signAndPublishPayloadAttestations(
|
|
@@ -81,7 +81,8 @@ type DefaultProposerConfig = {
|
|
|
81
81
|
feeRecipient: ExecutionAddress;
|
|
82
82
|
builder: {
|
|
83
83
|
gasLimit: number;
|
|
84
|
-
|
|
84
|
+
// Left undefined when not configured so the fork-appropriate default can be resolved per slot
|
|
85
|
+
selection?: routes.validator.BuilderSelection;
|
|
85
86
|
boostFactor: bigint;
|
|
86
87
|
};
|
|
87
88
|
};
|
|
@@ -182,7 +183,7 @@ export class ValidatorStore {
|
|
|
182
183
|
feeRecipient: defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient,
|
|
183
184
|
builder: {
|
|
184
185
|
gasLimit: defaultConfig.builder?.gasLimit ?? defaultOptions.defaultGasLimit,
|
|
185
|
-
selection: defaultConfig.builder?.selection
|
|
186
|
+
selection: defaultConfig.builder?.selection,
|
|
186
187
|
boostFactor: builderBoostFactor,
|
|
187
188
|
},
|
|
188
189
|
};
|
|
@@ -278,9 +279,21 @@ export class ValidatorStore {
|
|
|
278
279
|
delete validatorData.graffiti;
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
getBuilderSelectionParams(
|
|
282
|
+
getBuilderSelectionParams(
|
|
283
|
+
pubkeyHex: PubkeyHex,
|
|
284
|
+
slot?: Slot
|
|
285
|
+
): {selection: routes.validator.BuilderSelection; boostFactor: bigint} {
|
|
286
|
+
// Builder bids post-gloas are in-protocol over p2p, so the default strategy uses them
|
|
287
|
+
// (as if `--builder` was set), unless the validator explicitly opted out. Pre-gloas
|
|
288
|
+
// there is no in-protocol builder, so the default remains local-only (executiononly).
|
|
289
|
+
const defaultSelection =
|
|
290
|
+
slot !== undefined && this.config.getForkSeq(slot) >= ForkSeq.gloas
|
|
291
|
+
? defaultOptions.builderAliasSelection
|
|
292
|
+
: defaultOptions.builderSelection;
|
|
282
293
|
const selection =
|
|
283
|
-
this.validators.get(pubkeyHex)?.builder?.selection ??
|
|
294
|
+
this.validators.get(pubkeyHex)?.builder?.selection ??
|
|
295
|
+
this.defaultProposerConfig.builder.selection ??
|
|
296
|
+
defaultSelection;
|
|
284
297
|
|
|
285
298
|
let boostFactor: bigint;
|
|
286
299
|
switch (selection) {
|
|
@@ -607,9 +620,11 @@ export class ValidatorStore {
|
|
|
607
620
|
const signingSlot = aggregate.data.slot;
|
|
608
621
|
const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF);
|
|
609
622
|
const isPostElectra = this.config.getForkSeq(signingSlot) >= ForkSeq.electra;
|
|
610
|
-
const signingRoot =
|
|
611
|
-
|
|
612
|
-
|
|
623
|
+
const signingRoot = computeSigningRoot(
|
|
624
|
+
this.config.getForkTypes(signingSlot).AggregateAndProof,
|
|
625
|
+
aggregateAndProof,
|
|
626
|
+
domain
|
|
627
|
+
);
|
|
613
628
|
|
|
614
629
|
const signableMessage: SignableMessage = {
|
|
615
630
|
type: isPostElectra ? SignableMessageType.AGGREGATE_AND_PROOF_V2 : SignableMessageType.AGGREGATE_AND_PROOF,
|
package/src/util/params.ts
CHANGED
|
@@ -326,9 +326,12 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
|
|
|
326
326
|
MAX_PAYLOAD_ATTESTATIONS: gloasForkRelevant,
|
|
327
327
|
MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: gloasForkRelevant,
|
|
328
328
|
MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: gloasForkRelevant,
|
|
329
|
-
BUILDER_REGISTRY_LIMIT: gloasForkRelevant,
|
|
330
|
-
BUILDER_PENDING_WITHDRAWALS_LIMIT: gloasForkRelevant,
|
|
331
329
|
MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: gloasForkRelevant,
|
|
330
|
+
MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: false,
|
|
331
|
+
MAX_ATTESTER_SLASHING_SIZE: false,
|
|
332
|
+
MAX_DATA_COLUMN_SIDECAR_SIZE: false,
|
|
333
|
+
MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: false,
|
|
334
|
+
MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: false,
|
|
332
335
|
MIN_BUILDER_WITHDRAWABILITY_DELAY: gloasForkRelevant,
|
|
333
336
|
|
|
334
337
|
// FastConfirmationRule
|
package/src/validator.ts
CHANGED
|
@@ -66,6 +66,7 @@ export type ValidatorOptions = {
|
|
|
66
66
|
distributed?: boolean;
|
|
67
67
|
broadcastValidation?: routes.beacon.BroadcastValidation;
|
|
68
68
|
blindedLocal?: boolean;
|
|
69
|
+
payloadLocal?: boolean;
|
|
69
70
|
externalSigner?: ExternalSignerOptions;
|
|
70
71
|
clock?: ClockOptions;
|
|
71
72
|
};
|
|
@@ -256,6 +257,9 @@ export class Validator {
|
|
|
256
257
|
{
|
|
257
258
|
broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation,
|
|
258
259
|
blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal,
|
|
260
|
+
// Default to keeping the payload local to the beacon node if only a single node is
|
|
261
|
+
// configured, with multiple nodes the stateless flow allows publishing via any of them
|
|
262
|
+
payloadLocal: opts.payloadLocal ?? api.httpClient.urlsInits.length <= 1,
|
|
259
263
|
}
|
|
260
264
|
);
|
|
261
265
|
|