@lodestar/beacon-node 1.51.0-dev.e91a5043ed → 1.51.0-dev.e95d4a80f2

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.
Files changed (55) hide show
  1. package/lib/api/impl/api.js +1 -1
  2. package/lib/api/impl/api.js.map +1 -1
  3. package/lib/api/impl/beacon/blocks/index.d.ts +2 -1
  4. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/blocks/index.js +12 -7
  6. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  7. package/lib/api/impl/beacon/index.d.ts +2 -1
  8. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  9. package/lib/api/impl/beacon/index.js +2 -2
  10. package/lib/api/impl/beacon/index.js.map +1 -1
  11. package/lib/api/options.d.ts +2 -0
  12. package/lib/api/options.d.ts.map +1 -1
  13. package/lib/api/options.js +1 -0
  14. package/lib/api/options.js.map +1 -1
  15. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  16. package/lib/chain/archiveStore/utils/archiveBlocks.js +54 -96
  17. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  18. package/lib/chain/regen/interface.d.ts +0 -1
  19. package/lib/chain/regen/interface.d.ts.map +1 -1
  20. package/lib/chain/regen/interface.js +0 -1
  21. package/lib/chain/regen/interface.js.map +1 -1
  22. package/lib/chain/validation/executionPayloadBid.d.ts +0 -15
  23. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  24. package/lib/chain/validation/executionPayloadBid.js +0 -153
  25. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  26. package/lib/db/beacon.d.ts.map +1 -1
  27. package/lib/db/beacon.js +2 -0
  28. package/lib/db/beacon.js.map +1 -1
  29. package/lib/index.d.ts +1 -0
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +2 -0
  32. package/lib/index.js.map +1 -1
  33. package/lib/metrics/metrics/lodestar.d.ts +3 -0
  34. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  35. package/lib/metrics/metrics/lodestar.js +5 -0
  36. package/lib/metrics/metrics/lodestar.js.map +1 -1
  37. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  38. package/lib/network/processor/gossipHandlers.js +27 -18
  39. package/lib/network/processor/gossipHandlers.js.map +1 -1
  40. package/lib/sync/unknownBlock.d.ts.map +1 -1
  41. package/lib/sync/unknownBlock.js +42 -18
  42. package/lib/sync/unknownBlock.js.map +1 -1
  43. package/package.json +15 -15
  44. package/src/api/impl/api.ts +1 -1
  45. package/src/api/impl/beacon/blocks/index.ts +22 -17
  46. package/src/api/impl/beacon/index.ts +3 -1
  47. package/src/api/options.ts +3 -0
  48. package/src/chain/archiveStore/utils/archiveBlocks.ts +64 -126
  49. package/src/chain/regen/interface.ts +0 -1
  50. package/src/chain/validation/executionPayloadBid.ts +0 -180
  51. package/src/db/beacon.ts +5 -0
  52. package/src/index.ts +2 -0
  53. package/src/metrics/metrics/lodestar.ts +5 -0
  54. package/src/network/processor/gossipHandlers.ts +35 -20
  55. package/src/sync/unknownBlock.ts +47 -28
@@ -3,6 +3,7 @@ import {ApplicationMethods} from "@lodestar/api/server";
3
3
  import {isForkPostGloas} from "@lodestar/params";
4
4
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
5
5
  import {toRootHex} from "@lodestar/utils";
6
+ import {ApiOptions} from "../../options.js";
6
7
  import {ApiError} from "../errors.js";
7
8
  import {ApiModules} from "../types.js";
8
9
  import {getBeaconBlockApi} from "./blocks/index.js";
@@ -11,9 +12,10 @@ import {getBeaconRewardsApi} from "./rewards/index.js";
11
12
  import {getBeaconStateApi} from "./state/index.js";
12
13
 
13
14
  export function getBeaconApi(
15
+ opts: ApiOptions,
14
16
  modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db" | "sync">
15
17
  ): ApplicationMethods<routes.beacon.Endpoints> {
16
- const block = getBeaconBlockApi(modules);
18
+ const block = getBeaconBlockApi(opts, modules);
17
19
  const pool = getBeaconPoolApi(modules);
18
20
  const state = getBeaconStateApi(modules);
19
21
  const rewards = getBeaconRewardsApi(modules);
@@ -2,6 +2,8 @@ import {BeaconRestApiServerOpts, beaconRestApiServerOpts} from "./rest/index.js"
2
2
 
3
3
  export type ApiOptions = {
4
4
  maxGindicesInProof?: number;
5
+ /** Broadcast execution payload bids submitted via the API without running gossip validation */
6
+ disableBidValidation?: boolean;
5
7
  rest: BeaconRestApiServerOpts;
6
8
  commit?: string;
7
9
  version?: string;
@@ -10,6 +12,7 @@ export type ApiOptions = {
10
12
 
11
13
  export const defaultApiOptions: ApiOptions = {
12
14
  maxGindicesInProof: 512,
15
+ disableBidValidation: false,
13
16
  rest: beaconRestApiServerOpts,
14
17
  version: "dev",
15
18
  private: false,
@@ -1,11 +1,18 @@
1
1
  import path from "node:path";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {KeyValue} from "@lodestar/db";
4
- import {CheckpointWithHex, ExecutionStatus, IForkChoice, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice";
4
+ import {
5
+ CheckpointWithHex,
6
+ ExecutionStatus,
7
+ IForkChoice,
8
+ PayloadStatus,
9
+ ProtoBlock,
10
+ isGloasBlock,
11
+ } from "@lodestar/fork-choice";
5
12
  import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
6
13
  import {computeEpochAtSlot, computeStartSlotAtEpoch, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
7
14
  import {Epoch, Slot} from "@lodestar/types";
8
- import {Logger, fromAsync, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
15
+ import {Logger, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
9
16
  import {IBeaconDb} from "../../../db/index.js";
10
17
  import {BlockArchiveBatchPutBinaryItem, encodeArchivedHeaderEnvelope} from "../../../db/repositories/index.js";
11
18
  import {Metrics} from "../../../metrics/metrics.js";
@@ -136,23 +143,6 @@ export async function archiveBlocks(
136
143
  logger.verbose("Migrated blobSidecars from hot DB to cold DB", {...logCtx, migratedEntries});
137
144
  }
138
145
 
139
- // Keep the normal LevelDB hot-to-cold flow for legacy columns. New columns are written
140
- // directly to flat files, so this only moves fallback data left by older versions.
141
- if (finalizedPostFulu) {
142
- const migratedSlots = await migrateDataColumnSidecarsFromHotToColdDb(
143
- config,
144
- db,
145
- logger,
146
- finalizedCanonicalBlocks,
147
- currentEpoch
148
- );
149
- logger.verbose("Migrated legacy dataColumnSidecars from hot DB to cold DB", {
150
- ...logCtx,
151
- migratedEntries: migratedSlots.length,
152
- slotRange: prettyPrintIndices(migratedSlots),
153
- });
154
- }
155
-
156
146
  if (finalizedPostGloas) {
157
147
  const migratedSlots = await migrateExecutionPayloadEnvelopesFromHotToColdDb(
158
148
  config,
@@ -169,44 +159,34 @@ export async function archiveBlocks(
169
159
  }
170
160
  }
171
161
 
172
- // deleteNonCanonicalBlocks
173
- // loop through forkchoice single time
162
+ const canonicalRoots = new Set(finalizedCanonicalBlocks.map((block) => block.blockRoot));
163
+ const orphanedBlocks: ProtoBlock[] = [];
164
+ const orphanedPayloads: ProtoBlock[] = [];
174
165
 
175
- const nonCanonicalBlockRoots = finalizedNonCanonicalBlocks.map((summary) => fromHex(summary.blockRoot));
176
- if (nonCanonicalBlockRoots.length > 0) {
177
- if (persistOrphanedBlocks) {
178
- // Persist orphaned blocks to disk before deleting them from hot db
179
- await Promise.all(
180
- nonCanonicalBlockRoots.map(async (root, index) => {
181
- const block = finalizedNonCanonicalBlocks[index];
182
- const blockBytes = await db.block.getBinary(root);
183
- const blockLogCtx = {slot: block.slot, root: block.blockRoot};
184
- if (blockBytes) {
185
- await persistOrphanedBlock(block.slot, block.blockRoot, blockBytes, {
186
- persistOrphanedBlocksDir: persistOrphanedBlocksDir ?? "orphaned_blocks",
187
- });
188
- logger.verbose("Persisted orphaned block", {...logCtx, ...blockLogCtx});
189
- } else {
190
- logger.warn("Tried to persist orphaned block but no block found", {...logCtx, ...blockLogCtx});
191
- }
192
- })
193
- );
166
+ for (const block of finalizedNonCanonicalBlocks) {
167
+ if (block.payloadStatus === PayloadStatus.FULL) {
168
+ orphanedPayloads.push(block);
194
169
  }
170
+ // Every Gloas block gets EMPTY before FULL. If only FULL survives pruning, an earlier archive run
171
+ // already handled the root's block-level data.
172
+ const isBlockNode = !isGloasBlock(block) || block.payloadStatus === PayloadStatus.EMPTY;
173
+ if (isBlockNode && !canonicalRoots.has(block.blockRoot)) {
174
+ orphanedBlocks.push(block);
175
+ }
176
+ }
177
+ const orphanedBlockRoots = orphanedBlocks.map((block) => fromHex(block.blockRoot));
195
178
 
196
- const nonCanonicalSlots = finalizedNonCanonicalBlocks.map((summary) => summary.slot).sort((a, b) => a - b);
197
- const nonCanonicalLogCtx = {
179
+ if (orphanedPayloads.length > 0) {
180
+ const orphanedPayloadRoots = orphanedPayloads.map((block) => fromHex(block.blockRoot));
181
+ const orphanedPayloadLogCtx = {
198
182
  ...logCtx,
199
- count: nonCanonicalBlockRoots.length,
200
- slotRange: prettyPrintIndices(nonCanonicalSlots),
183
+ count: orphanedPayloads.length,
184
+ slotRange: prettyPrintIndices(orphanedPayloads.map((block) => block.slot).sort((a, b) => a - b)),
201
185
  };
202
186
 
203
- const columnItems = finalizedNonCanonicalBlocks
204
- // Gloas EMPTY and FULL variants share a block root. EMPTY has no sidecars, so deleting by its root could
205
- // remove the canonical FULL variant's columns. Pre-Gloas blocks are always FULL.
206
- .filter(
207
- (summary) => config.getForkSeq(summary.slot) >= ForkSeq.fulu && summary.payloadStatus === PayloadStatus.FULL
208
- )
209
- .map((summary) => ({slot: summary.slot, blockRoot: summary.blockRoot}));
187
+ const columnItems = orphanedPayloads
188
+ .filter((block) => config.getForkSeq(block.slot) >= ForkSeq.fulu)
189
+ .map((block) => ({slot: block.slot, blockRoot: block.blockRoot}));
210
190
  if (columnItems.length > 0) {
211
191
  // Delete sidecars first so their block roots remain available to retry cleanup after a failure or crash.
212
192
  await db.dataColumns.deleteMany(columnItems);
@@ -217,18 +197,42 @@ export async function archiveBlocks(
217
197
  });
218
198
  }
219
199
 
220
- await db.block.batchDelete(nonCanonicalBlockRoots);
221
- logger.verbose("Deleted non canonical blocks from hot DB", nonCanonicalLogCtx);
222
-
223
200
  if (finalizedPostDeneb) {
224
- await db.blobSidecars.batchDelete(nonCanonicalBlockRoots);
225
- logger.verbose("Deleted non canonical blobSidecars from hot DB", nonCanonicalLogCtx);
201
+ await db.blobSidecars.batchDelete(orphanedPayloadRoots);
202
+ logger.verbose("Deleted non canonical blobSidecars from hot DB", orphanedPayloadLogCtx);
226
203
  }
227
204
 
228
205
  if (finalizedPostGloas) {
229
- await db.executionPayloadEnvelope.batchDelete(nonCanonicalBlockRoots);
230
- logger.verbose("Deleted non canonical executionPayloadEnvelopes from hot DB", nonCanonicalLogCtx);
206
+ await db.executionPayloadEnvelope.batchDelete(orphanedPayloadRoots);
207
+ logger.verbose("Deleted non canonical executionPayloadEnvelopes from hot DB", orphanedPayloadLogCtx);
208
+ }
209
+ }
210
+
211
+ if (orphanedBlocks.length > 0) {
212
+ if (persistOrphanedBlocks) {
213
+ // Persist orphaned blocks to disk before deleting them from hot db
214
+ await Promise.all(
215
+ orphanedBlocks.map(async (block) => {
216
+ const blockBytes = await db.block.getBinary(fromHex(block.blockRoot));
217
+ const blockLogCtx = {slot: block.slot, root: block.blockRoot};
218
+ if (blockBytes) {
219
+ await persistOrphanedBlock(block.slot, block.blockRoot, blockBytes, {
220
+ persistOrphanedBlocksDir: persistOrphanedBlocksDir ?? "orphaned_blocks",
221
+ });
222
+ logger.verbose("Persisted orphaned block", {...logCtx, ...blockLogCtx});
223
+ } else {
224
+ logger.debug("Tried to persist orphaned block but no block found", {...logCtx, ...blockLogCtx});
225
+ }
226
+ })
227
+ );
231
228
  }
229
+
230
+ await db.block.batchDelete(orphanedBlockRoots);
231
+ logger.verbose("Deleted non canonical blocks from hot DB", {
232
+ ...logCtx,
233
+ count: orphanedBlocks.length,
234
+ slotRange: prettyPrintIndices(orphanedBlocks.map((block) => block.slot).sort((a, b) => a - b)),
235
+ });
232
236
  }
233
237
 
234
238
  // Delete expired blobs
@@ -283,9 +287,9 @@ export async function archiveBlocks(
283
287
  }
284
288
  }
285
289
 
286
- // Prunning potential checkpoint data
290
+ // Pruning potential checkpoint data
287
291
  const finalizedCanonicalNonCheckpointBlocks = getNonCheckpointBlocks(finalizedCanonicalBlockRoots);
288
- const nonCheckpointBlockRoots: Uint8Array[] = [...nonCanonicalBlockRoots];
292
+ const nonCheckpointBlockRoots: Uint8Array[] = [...orphanedBlockRoots];
289
293
  for (const block of finalizedCanonicalNonCheckpointBlocks) {
290
294
  nonCheckpointBlockRoots.push(block.root);
291
295
  }
@@ -408,72 +412,6 @@ async function migrateBlobSidecarsFromHotToColdDb(
408
412
  return migratedWrappedBlobSidecars;
409
413
  }
410
414
 
411
- // TODO: This function can be simplified further by reducing layers of promises in a loop
412
- /**
413
- * Post-gloas the data columns of a Gloas block are tied to its execution payload envelope.
414
- * Columns only exist once the FULL variant of the block is in the proto-array. Pre-Gloas
415
- * blocks only have a FULL variant, so the payload-status filter passes them all.
416
- */
417
- async function migrateDataColumnSidecarsFromHotToColdDb(
418
- config: ChainForkConfig,
419
- db: IBeaconDb,
420
- logger: Logger,
421
- canonicalBlocks: ProtoBlock[],
422
- currentEpoch: Epoch
423
- ): Promise<Slot[]> {
424
- const columnBlocks = canonicalBlocks.filter(
425
- (block) => config.getForkSeq(block.slot) < ForkSeq.gloas || block.payloadStatus === PayloadStatus.FULL
426
- );
427
- if (columnBlocks.length === 0) return [];
428
- const blocks: BlockRootSlot[] = columnBlocks.map((block) => ({slot: block.slot, root: fromHex(block.blockRoot)}));
429
-
430
- const migratedSlots: Slot[] = [];
431
- for (let i = 0; i < blocks.length; i += BLOB_SIDECAR_BATCH_SIZE) {
432
- const toIdx = Math.min(i + BLOB_SIDECAR_BATCH_SIZE, blocks.length);
433
- const batch = blocks.slice(i, toIdx);
434
- if (batch.length === 0) break;
435
-
436
- const promises: Promise<void>[] = [];
437
- for (const block of batch) {
438
- const blockEpoch = computeEpochAtSlot(block.slot);
439
- if (
440
- config.getForkSeq(block.slot) < ForkSeq.fulu ||
441
- blockEpoch < getBlobDataRetentionStartEpoch(config, currentEpoch)
442
- ) {
443
- continue;
444
- }
445
-
446
- const dataColumnSidecarBytes = await fromAsync(db.dataColumnSidecar.valuesStreamBinary(block.root));
447
- if (dataColumnSidecarBytes.length === 0) {
448
- logger.debug("DataColumnSidecars in forkchoice but missing in hot db, could be already archived", {
449
- slot: block.slot,
450
- root: toRootHex(block.root),
451
- });
452
- continue;
453
- }
454
- logger.verbose("Migrated legacy dataColumnSidecars for block", {
455
- currentEpoch,
456
- slot: block.slot,
457
- root: toRootHex(block.root),
458
- numSidecars: dataColumnSidecarBytes.length,
459
- });
460
- promises.push(
461
- db.dataColumnSidecarArchive.putManyBinary(
462
- block.slot,
463
- dataColumnSidecarBytes.map((entry) => ({key: entry.id, value: entry.value}))
464
- )
465
- );
466
- migratedSlots.push(block.slot);
467
- }
468
-
469
- await Promise.all(promises);
470
- await db.dataColumnSidecar.deleteMany(batch.map((block) => block.root));
471
- }
472
-
473
- // Ancestor walk is newest to oldest; sort ascending so `prettyPrintIndices` renders cleanly.
474
- return migratedSlots.sort((a, b) => a - b);
475
- }
476
-
477
415
  /**
478
416
  * Post-gloas given a finalized checkpoint at a block root, payload of that block root
479
417
  * is not considered finalized, hence they are archived in the next run.
@@ -25,7 +25,6 @@ export enum RegenCaller {
25
25
  publishDeferredVoluntaryExits = "publishDeferredVoluntaryExits",
26
26
  validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid",
27
27
  validateGossipInclusionList = "validateGossipInclusionList",
28
- validateApiExecutionPayloadBid = "validateApiExecutionPayloadBid",
29
28
  validateGossipPayloadAttestationMessage = "validateGossipPayloadAttestationMessage",
30
29
  validateGossipProposerPreferences = "validateGossipProposerPreferences",
31
30
  onForkChoiceFinalized = "onForkChoiceFinalized",
@@ -83,186 +83,6 @@ function isBidCompatibleWithHead(
83
83
  return buildsOnParentPayload;
84
84
  }
85
85
 
86
- /**
87
- * Validation for bids submitted via the API by the operator's own builder.
88
- *
89
- * Only non-transient checks are applied: slot later than parent, zero execution payment, blob
90
- * commitment count, builder eligibility, version and balance coverage, prev_randao, and signature.
91
- * Transient gossip rules (head compatibility, first bid per tuple, value increment, proposer
92
- * preferences) are not applied, since those only limit forwarding of peers' messages and the
93
- * builder may legitimately bid on a branch this node does not consider head.
94
- * The parent-payload builder-exit rule is omitted because this endpoint only accepts bids from
95
- * the operator's own builder.
96
- *
97
- * Throws on any failed check. Also throws if the bid's parent block is unknown or its state is
98
- * unavailable, since the checks cannot be evaluated against the parent branch.
99
- */
100
- export async function validateApiExecutionPayloadBid(
101
- chain: IBeaconChain,
102
- signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
103
- ): Promise<void> {
104
- const bid = signedExecutionPayloadBid.message;
105
- const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
106
-
107
- // [REJECT] `bid.execution_payment` is zero.
108
- if (bid.executionPayment !== 0n) {
109
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
110
- code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
111
- builderIndex: bid.builderIndex,
112
- executionPayment: bid.executionPayment,
113
- });
114
- }
115
-
116
- // [REJECT] The bid's block hash is not equal to its parent block hash -- i.e.
117
- // `bid.block_hash != bid.parent_block_hash`.
118
- if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
119
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
120
- code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH,
121
- blockHash: toRootHex(bid.blockHash),
122
- });
123
- }
124
-
125
- // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
126
- // consensus layer -- i.e. validate that
127
- // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
128
- const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
129
- const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
130
- if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
131
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
132
- code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
133
- blobKzgCommitmentsLen,
134
- commitmentLimit: maxBlobsPerBlock,
135
- });
136
- }
137
-
138
- // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
139
- if (
140
- !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
141
- !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
142
- ) {
143
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
144
- code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
145
- builderIndex: bid.builderIndex,
146
- slot: bid.slot,
147
- });
148
- }
149
-
150
- // [IGNORE] The bid is received after the inclusion list deadline of `bid.slot - 1`
151
- if (
152
- isForkPostHeze(chain.config.getForkName(bid.slot)) &&
153
- chain.clock.msFromSlot(bid.slot - 1) < chain.config.getInclusionListDueMs(chain.config.getForkName(bid.slot - 1))
154
- ) {
155
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
156
- code: ExecutionPayloadBidErrorCode.BEFORE_INCLUSION_LIST_DEADLINE,
157
- builderIndex: bid.builderIndex,
158
- slot: bid.slot,
159
- });
160
- }
161
-
162
- // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
163
- // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
164
- // proposer-preferences lookup below from a known fork-choice block.
165
- const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
166
- if (parentBlock === null) {
167
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
168
- code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
169
- parentBlockRoot: bidParentBlockRoot,
170
- });
171
- }
172
-
173
- // [REJECT] The bid is for a higher slot than its parent block -- i.e.
174
- // validate that `bid.slot` is greater than the slot of the block with root
175
- // `bid.parent_block_root`.
176
- if (bid.slot <= parentBlock.slot) {
177
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
178
- code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
179
- parentSlot: parentBlock.slot,
180
- slot: bid.slot,
181
- });
182
- }
183
-
184
- const state = await chain.regen
185
- .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateApiExecutionPayloadBid)
186
- .catch(() => {
187
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
188
- code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
189
- parentBlockRoot: bidParentBlockRoot,
190
- });
191
- });
192
-
193
- if (!isStatePostGloas(state)) {
194
- throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
195
- }
196
-
197
- // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
198
- // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
199
- // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
200
- // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
201
- if (bid.builderIndex >= state.getBuildersLength()) {
202
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
203
- code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
204
- builderIndex: bid.builderIndex,
205
- });
206
- }
207
-
208
- // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
209
- // `is_active_builder(state, bid.builder_index)` returns `True`.
210
- const builder = state.getBuilder(bid.builderIndex);
211
- if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
212
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
213
- code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
214
- builderIndex: bid.builderIndex,
215
- });
216
- }
217
-
218
- // [REJECT] The builder version is `PAYLOAD_BUILDER_VERSION` -- i.e.
219
- // `state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION`.
220
- if (builder.version !== PAYLOAD_BUILDER_VERSION) {
221
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
222
- code: ExecutionPayloadBidErrorCode.INVALID_BUILDER_VERSION,
223
- builderIndex: bid.builderIndex,
224
- version: builder.version,
225
- expectedVersion: PAYLOAD_BUILDER_VERSION,
226
- });
227
- }
228
-
229
- // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
230
- // i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
231
- if (!state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
232
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
233
- code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH,
234
- bidValue: bid.value,
235
- builderBalance: builder.balance,
236
- });
237
- }
238
-
239
- // [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that
240
- // `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))`.
241
- const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
242
- if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
243
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
244
- code: ExecutionPayloadBidErrorCode.INVALID_PREV_RANDAO,
245
- builderIndex: bid.builderIndex,
246
- bidPrevRandao: toHex(bid.prevRandao),
247
- expectedPrevRandao: toHex(randaoMix),
248
- });
249
- }
250
-
251
- // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
252
- const signatureSet = createSingleSignatureSetFromComponents(
253
- builder.pubkey,
254
- getExecutionPayloadBidSigningRoot(chain.config, bid),
255
- signedExecutionPayloadBid.signature
256
- );
257
- if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
258
- throw new ExecutionPayloadBidError(GossipAction.REJECT, {
259
- code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE,
260
- builderIndex: bid.builderIndex,
261
- slot: bid.slot,
262
- });
263
- }
264
- }
265
-
266
86
  export async function validateGossipExecutionPayloadBid(
267
87
  chain: IBeaconChain,
268
88
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
package/src/db/beacon.ts CHANGED
@@ -130,6 +130,11 @@ export class BeaconDb implements IBeaconDb {
130
130
  await this.blobSidecars.batchDelete(await this.blobSidecars.keys());
131
131
  // New columns are persisted in flat files, so this bucket only contains legacy hot data.
132
132
  await this.deleteBucketData(Bucket.allForks_dataColumnSidecars);
133
+ // Compact even if empty to reclaim tombstones left by previous startups.
134
+ await this.db.compactRange(
135
+ encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(0)),
136
+ encodeKey(Bucket.allForks_dataColumnSidecars + 1, Buffer.alloc(0))
137
+ );
133
138
  // Prune all hot blocks
134
139
  // TODO: Enable once it's deemed safe
135
140
  // await this.block.batchDelete(await this.block.keys());
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export {persistAnchorState} from "./chain/index.js";
6
6
  export {DbCPStateDatastore} from "./chain/stateCache/datastore/db.js";
7
7
  export {FileCPStateDatastore} from "./chain/stateCache/datastore/file.js";
8
8
  export {BeaconDb, type IBeaconDb} from "./db/index.js";
9
+ // Export execution engine to make it usable by builder
10
+ export {initializeExecutionEngine} from "./execution/index.js";
9
11
  // Export metrics utilities to de-duplicate validator metrics
10
12
  export {
11
13
  type HttpMetricsServer,
@@ -1474,6 +1474,11 @@ export function createLodestarMetrics(
1474
1474
  help: "Total number of InsertOutcome as a result of adding an execution payload bid from gossip to the pool",
1475
1475
  labelNames: ["insertOutcome"],
1476
1476
  }),
1477
+ apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
1478
+ name: "lodestar_oppool_execution_payload_bid_pool_api_insert_outcome_total",
1479
+ help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
1480
+ labelNames: ["insertOutcome"],
1481
+ }),
1477
1482
  apiValidationTime: register.histogram({
1478
1483
  name: "lodestar_api_execution_payload_bid_validation_time_seconds",
1479
1484
  help: "Time elapsed for signed execution payload bid validation - api path",
@@ -24,6 +24,7 @@ import {
24
24
  deneb,
25
25
  fulu,
26
26
  gloas,
27
+ isGloasBeaconBlock,
27
28
  isGloasDataColumnSidecar,
28
29
  ssz,
29
30
  sszTypesFor,
@@ -150,6 +151,31 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
150
151
  */
151
152
  function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers {
152
153
  const {chain, config, metrics, logger, core} = modules;
154
+ const incompletePayloadWaits = new WeakSet<PayloadEnvelopeInput>();
155
+
156
+ function scheduleIncompletePayloadEnvelope(payloadInput: PayloadEnvelopeInput, peer: string): void {
157
+ if (payloadInput.isComplete() || incompletePayloadWaits.has(payloadInput)) {
158
+ return;
159
+ }
160
+ incompletePayloadWaits.add(payloadInput);
161
+
162
+ const fork = config.getForkName(payloadInput.slot);
163
+ const cutoffTimeMs = getCutoffTimeMs(chain, payloadInput.slot, config.getPayloadDueMs(fork));
164
+ payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch(() => {
165
+ if (
166
+ payloadInput.isComplete() ||
167
+ chain.seenPayloadEnvelopeInputCache.get(payloadInput.blockRootHex) !== payloadInput
168
+ ) {
169
+ return;
170
+ }
171
+ chain.logger.debug("Payload envelope incomplete at payload deadline", payloadInput.getLogMeta());
172
+ chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
173
+ payloadInput,
174
+ peer,
175
+ source: BlockInputSource.gossip,
176
+ });
177
+ });
178
+ }
153
179
 
154
180
  /**
155
181
  * Add a gossip block to the seen caches. The proposer signature of the block MUST be verified.
@@ -764,7 +790,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
764
790
  // roots per proposer and slot, which bounds full imports over gossip to one alternate
765
791
  chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) &&
766
792
  // A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant
767
- chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null
793
+ chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null &&
794
+ (!isGloasBeaconBlock(signedBlock.message) ||
795
+ chain.forkChoice.getBlockHexAndBlockHash(
796
+ toRootHex(signedBlock.message.parentRoot),
797
+ toRootHex(signedBlock.message.body.signedExecutionPayloadBid.message.parentBlockHash)
798
+ ) !== null)
768
799
  ) {
769
800
  const blockInput = cacheSignatureVerifiedBlock(
770
801
  signedBlock,
@@ -902,25 +933,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
902
933
  // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
903
934
  // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
904
935
 
905
- if (!payloadInput.isComplete()) {
906
- const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
907
- // do not await here to not delay gossip validation
908
- payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
909
- chain.logger.debug(
910
- "Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope",
911
- {
912
- dataColumnIndex: index,
913
- ...payloadInputMeta,
914
- }
915
- );
916
- // TODO GLOAS: UnknownBlockSync to handle this event
917
- chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
918
- payloadInput,
919
- peer: peerIdStr,
920
- source: BlockInputSource.gossip,
921
- });
922
- });
923
- }
936
+ scheduleIncompletePayloadEnvelope(payloadInput, peerIdStr);
924
937
  } catch (e) {
925
938
  logger.debug(
926
939
  "Error handling gossip data column",
@@ -1330,6 +1343,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1330
1343
  peerIdStr,
1331
1344
  });
1332
1345
 
1346
+ scheduleIncompletePayloadEnvelope(payloadInput, peerIdStr);
1347
+
1333
1348
  chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
1334
1349
  slot,
1335
1350
  builderIndex: envelope.builderIndex,