@lodestar/beacon-node 1.48.0-dev.27e5bfcdb6 → 1.48.0-dev.3211c1bdd4

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 (52) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +0 -7
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/utils.js +14 -0
  6. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  7. package/lib/api/impl/config/index.d.ts.map +1 -1
  8. package/lib/api/impl/config/index.js +4 -1
  9. package/lib/api/impl/config/index.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts +5 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +9 -3
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/index.d.ts.map +1 -1
  15. package/lib/chain/blocks/index.js +74 -7
  16. package/lib/chain/blocks/index.js.map +1 -1
  17. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  18. package/lib/chain/blocks/utils/chainSegment.js +16 -10
  19. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  20. package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
  21. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  22. package/lib/chain/errors/executionPayloadBid.js +1 -0
  23. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  24. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  25. package/lib/chain/validation/executionPayloadBid.js +16 -0
  26. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  27. package/lib/execution/builder/validateBid.d.ts.map +1 -1
  28. package/lib/execution/builder/validateBid.js +3 -0
  29. package/lib/execution/builder/validateBid.js.map +1 -1
  30. package/lib/metrics/metrics/lodestar.d.ts +0 -3
  31. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  32. package/lib/metrics/metrics/lodestar.js +0 -5
  33. package/lib/metrics/metrics/lodestar.js.map +1 -1
  34. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  35. package/lib/network/processor/gossipHandlers.js +378 -278
  36. package/lib/network/processor/gossipHandlers.js.map +1 -1
  37. package/lib/network/processor/index.d.ts.map +1 -1
  38. package/lib/network/processor/index.js +11 -16
  39. package/lib/network/processor/index.js.map +1 -1
  40. package/package.json +14 -14
  41. package/src/api/impl/beacon/blocks/index.ts +0 -10
  42. package/src/api/impl/beacon/state/utils.ts +15 -0
  43. package/src/api/impl/config/index.ts +4 -1
  44. package/src/chain/blocks/importBlock.ts +21 -5
  45. package/src/chain/blocks/index.ts +87 -7
  46. package/src/chain/blocks/utils/chainSegment.ts +16 -11
  47. package/src/chain/errors/executionPayloadBid.ts +2 -0
  48. package/src/chain/validation/executionPayloadBid.ts +18 -0
  49. package/src/execution/builder/validateBid.ts +4 -0
  50. package/src/metrics/metrics/lodestar.ts +0 -5
  51. package/src/network/processor/gossipHandlers.ts +487 -348
  52. package/src/network/processor/index.ts +11 -17
@@ -1,3 +1,5 @@
1
+ import {PayloadStatus} from "@lodestar/fork-choice";
2
+ import {computeEpochAtSlot} from "@lodestar/state-transition";
1
3
  import {SignedBeaconBlock, Slot} from "@lodestar/types";
2
4
  import {isErrorAborted, toRootHex} from "@lodestar/utils";
3
5
  import {Metrics} from "../../metrics/metrics.js";
@@ -5,15 +7,17 @@ import {nextEventLoop} from "../../util/eventLoop.js";
5
7
  import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js";
6
8
  import type {BeaconChain} from "../chain.js";
7
9
  import {BlockError, BlockErrorCode, isBlockErrorAborted} from "../errors/index.js";
10
+ import {ForkchoiceCaller} from "../forkChoice/index.js";
8
11
  import {BlockProcessOpts} from "../options.js";
9
12
  import {IBlockInput} from "./blockInput/types.js";
10
- import {importBlock} from "./importBlock.js";
13
+ import {importBlock, importsBlockAttestations} from "./importBlock.js";
11
14
  import {PayloadError, importExecutionPayload} from "./importExecutionPayload.js";
12
15
  import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
13
16
  import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
14
17
  import {assertLinearChainSegment} from "./utils/chainSegment.js";
15
18
  import {verifyBlocksInEpoch} from "./verifyBlock.js";
16
19
  import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js";
20
+ import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
17
21
 
18
22
  export {AttestationImportOpt, type ImportBlockOpts} from "./types.js";
19
23
 
@@ -70,6 +74,7 @@ export async function processBlocks(
70
74
  // No relevant blocks, skip verifyBlocksInEpoch()
71
75
  if (relevantBlocks.length === 0 || parentBlock === null) {
72
76
  // parentBlock can only be null if relevantBlocks are empty
77
+ await importPayloadEnvelopesOfKnownBlocks.call(this, payloadEnvelopes, opts);
73
78
  return;
74
79
  }
75
80
 
@@ -79,9 +84,21 @@ export async function processBlocks(
79
84
  payloadEnvelopes,
80
85
  parentBlock
81
86
  );
82
- if (orphanedPayloads != null) {
87
+ // Orphaned payloads of recent blocks are still imported, a reorg of the child could make the payload canonical
88
+ // again and later blocks build on it. Older ones are skipped: without the block attestations the orphaned FULL
89
+ // variant has no descendants and ties at zero weight with the EMPTY variant carrying the chain, the payload
90
+ // status tiebreaker then picks FULL and parks the head there.
91
+ const blockEpoch = computeEpochAtSlot(relevantBlocks[0].getBlock().message.slot);
92
+ const importsAttestations = importsBlockAttestations(opts, blockEpoch, this.clock.currentEpoch);
93
+ let payloadEnvelopesToImport = payloadEnvelopes;
94
+ if (orphanedPayloads != null && payloadEnvelopes !== null && !importsAttestations) {
95
+ payloadEnvelopesToImport = new Map(payloadEnvelopes);
83
96
  for (const orphaned of orphanedPayloads) {
84
- this.logger.debug("Orphaned payload envelope in chain segment", {
97
+ payloadEnvelopesToImport.delete(orphaned.slot);
98
+ // never validated, drop it from the shared cache so it is not served to peers, by-root sync reloads the
99
+ // entry from db if the payload is ever needed
100
+ this.seenPayloadEnvelopeInputCache.prune(orphaned.payloadEnvelopeInput.blockRootHex);
101
+ this.logger.debug("Skipping orphaned payload envelope in chain segment", {
85
102
  slot: orphaned.slot,
86
103
  blockRoot: orphaned.payloadEnvelopeInput.blockRootHex,
87
104
  });
@@ -97,7 +114,7 @@ export async function processBlocks(
97
114
  proposerBalanceDeltas,
98
115
  segmentExecStatus,
99
116
  indexedAttestationsByBlock,
100
- } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopes, opts);
117
+ } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopesToImport, opts);
101
118
 
102
119
  // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid
103
120
  // and we need to further propagate
@@ -132,8 +149,8 @@ export async function processBlocks(
132
149
  }
133
150
 
134
151
  const slotSet = new Set<Slot>(blocks.map((b) => b.getBlock().message.slot));
135
- if (payloadEnvelopes) {
136
- for (const slot of payloadEnvelopes.keys()) slotSet.add(slot);
152
+ if (payloadEnvelopesToImport) {
153
+ for (const slot of payloadEnvelopesToImport.keys()) slotSet.add(slot);
137
154
  }
138
155
  const slots = Array.from(slotSet).sort((a, b) => a - b);
139
156
  for (const slot of slots) {
@@ -146,7 +163,7 @@ export async function processBlocks(
146
163
  // PayloadEnvelopeInput is shared and may receive an envelope after the DA snapshot was taken.
147
164
  const payloadDA = payloadDAStatuses.get(slot);
148
165
  if (payloadDA !== undefined) {
149
- const payloadInput = payloadEnvelopes?.get(slot);
166
+ const payloadInput = payloadEnvelopesToImport?.get(slot);
150
167
  if (payloadInput === undefined) {
151
168
  throw new Error(`Missing payload input for slot ${slot} after DA verification`);
152
169
  }
@@ -209,6 +226,69 @@ export async function processBlocks(
209
226
  }
210
227
  }
211
228
 
229
+ /**
230
+ * A peer can serve a block without its envelope, the block is then imported with a PENDING payload. Later batches
231
+ * carry the envelope but all their blocks are known, without importing it here every child building on the FULL
232
+ * variant fails with PARENT_PAYLOAD_UNKNOWN and range sync never progresses. If our head already descends from the
233
+ * block's EMPTY variant the chain did not build on the payload, importing it would only add a dead FULL leaf. As for
234
+ * orphaned payloads in a segment this only applies while the block attestations are not imported, a recent payload
235
+ * is imported anyway so a reorg building on it does not have to fetch it through unknown block sync.
236
+ */
237
+ async function importPayloadEnvelopesOfKnownBlocks(
238
+ this: BeaconChain,
239
+ payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
240
+ opts: ImportBlockOpts
241
+ ): Promise<void> {
242
+ if (payloadEnvelopes === null) {
243
+ return;
244
+ }
245
+
246
+ const head = this.forkChoice.getHead();
247
+ const currentEpoch = this.clock.currentEpoch;
248
+ const payloadInputs: PayloadEnvelopeInput[] = [];
249
+ const orphanedSlots: Slot[] = [];
250
+ for (const payloadInput of payloadEnvelopes.values()) {
251
+ const {blockRootHex, slot} = payloadInput;
252
+ if (
253
+ !payloadInput.hasPayloadEnvelope() ||
254
+ !this.forkChoice.hasBlockHex(blockRootHex) ||
255
+ this.forkChoice.getBlockHexAndBlockHash(blockRootHex, payloadInput.getBlockHashHex()) !== null
256
+ ) {
257
+ continue;
258
+ }
259
+ if (
260
+ head.blockRoot !== blockRootHex &&
261
+ !importsBlockAttestations(opts, computeEpochAtSlot(slot), currentEpoch) &&
262
+ this.forkChoice.isDescendant(blockRootHex, PayloadStatus.EMPTY, head.blockRoot, head.payloadStatus)
263
+ ) {
264
+ // never validated, see processBlocks()
265
+ this.seenPayloadEnvelopeInputCache.prune(blockRootHex);
266
+ orphanedSlots.push(slot);
267
+ continue;
268
+ }
269
+ payloadInputs.push(payloadInput);
270
+ }
271
+
272
+ if (orphanedSlots.length > 0) {
273
+ this.logger.debug("Skipping orphaned payload envelopes of known blocks", {slots: orphanedSlots.join(",")});
274
+ }
275
+
276
+ if (payloadInputs.length === 0) {
277
+ return;
278
+ }
279
+
280
+ const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(payloadInputs, new AbortController().signal);
281
+ for (let i = 0; i < payloadInputs.length; i++) {
282
+ this.logger.debug("Importing payload envelope of known block", {
283
+ slot: payloadInputs[i].slot,
284
+ blockRoot: payloadInputs[i].blockRootHex,
285
+ });
286
+ await importExecutionPayload.call(this, payloadInputs[i], dataAvailabilityStatuses[i], {validSignature: false});
287
+ }
288
+ // the new FULL variants are not seen by the head cached from the last block import
289
+ this.recomputeForkChoiceHead(ForkchoiceCaller.importBlock);
290
+ }
291
+
212
292
  function getBlockOrPayloadError(e: unknown, block: SignedBeaconBlock): BlockError | PayloadError {
213
293
  if (e instanceof BlockError) {
214
294
  return e;
@@ -37,17 +37,6 @@ export function assertLinearChainSegment(
37
37
 
38
38
  const warnings: OrphanedPayloadEnvelope[] = [];
39
39
 
40
- let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
41
- if (parentBlock !== null) {
42
- const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
43
- if (parentPayloadInput?.hasPayloadEnvelope()) {
44
- // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
45
- // hash, not its own payload. Its payload envelope is in this batch, so use that blockHash as
46
- // the real parent EL head.
47
- parentExecHash = parentPayloadInput.getBlockHashHex();
48
- }
49
- }
50
-
51
40
  // Track the expected execution payload block hash through the segment, seeded from the FIRST
52
41
  // block's own bid
53
42
  const firstBlockMessage = blocks[0].getBlock().message;
@@ -55,6 +44,22 @@ export function assertLinearChainSegment(
55
44
  ? toRootHex(firstBlockMessage.body.signedExecutionPayloadBid.message.parentBlockHash)
56
45
  : null;
57
46
 
47
+ let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
48
+ if (parentBlock !== null) {
49
+ const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
50
+ if (parentPayloadInput?.hasPayloadEnvelope()) {
51
+ const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
52
+ if (currentExecHash === parentPayloadBlockHash) {
53
+ // Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
54
+ // hash, its own payload is in this batch and is the real parent EL head
55
+ parentExecHash = parentPayloadBlockHash;
56
+ } else {
57
+ // First block builds on the parent's EMPTY variant, the parent's payload in this batch is orphaned
58
+ warnings.push({slot: parentBlock.slot, payloadEnvelopeInput: parentPayloadInput});
59
+ }
60
+ }
61
+ }
62
+
58
63
  // First block must build on the parent's EL head. PARENT_PAYLOAD_UNKNOWN (vs the in-segment
59
64
  // NON_LINEAR_PAYLOAD_ROOTS below) tells range sync this is a parent-link problem, not an in-batch one.
60
65
  if (parentExecHash !== null && currentExecHash !== null && currentExecHash !== parentExecHash) {
@@ -5,6 +5,7 @@ export enum ExecutionPayloadBidErrorCode {
5
5
  BUILDER_NOT_ELIGIBLE = "EXECUTION_PAYLOAD_BID_ERROR_BUILDER_NOT_ELIGIBLE",
6
6
  INVALID_BUILDER_VERSION = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_BUILDER_VERSION",
7
7
  NON_ZERO_EXECUTION_PAYMENT = "EXECUTION_PAYLOAD_BID_ERROR_NON_ZERO_EXECUTION_PAYMENT",
8
+ BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH",
8
9
  INCOMPATIBLE_WITH_HEAD = "EXECUTION_PAYLOAD_BID_ERROR_INCOMPATIBLE_WITH_HEAD",
9
10
  BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN",
10
11
  BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW",
@@ -63,6 +64,7 @@ export type ExecutionPayloadBidErrorType =
63
64
  }
64
65
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
65
66
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
67
+ | {code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH; blockHash: RootHex}
66
68
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
67
69
  | {code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
68
70
  | {
@@ -113,6 +113,15 @@ export async function validateApiExecutionPayloadBid(
113
113
  });
114
114
  }
115
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
+
116
125
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
117
126
  // consensus layer -- i.e. validate that
118
127
  // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
@@ -266,6 +275,15 @@ async function validateExecutionPayloadBid(
266
275
  });
267
276
  }
268
277
 
278
+ // [REJECT] The bid's block hash is not equal to its parent block hash -- i.e.
279
+ // `bid.block_hash != bid.parent_block_hash`.
280
+ if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
281
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
282
+ code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH,
283
+ blockHash: toRootHex(bid.blockHash),
284
+ });
285
+ }
286
+
269
287
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
270
288
  // consensus layer -- i.e. validate that
271
289
  // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
@@ -56,6 +56,10 @@ export async function validateBuilderApiExecutionPayloadBid(
56
56
  );
57
57
  }
58
58
 
59
+ if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
60
+ throw Error(`Bid block hash ${toRootHex(bid.blockHash)} must not equal its parent block hash`);
61
+ }
62
+
59
63
  const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
60
64
  const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
61
65
  if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
@@ -1358,11 +1358,6 @@ export function createLodestarMetrics(
1358
1358
  help: "Total number of InsertOutcome as a result of adding an execution payload bid from gossip to the pool",
1359
1359
  labelNames: ["insertOutcome"],
1360
1360
  }),
1361
- apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
1362
- name: "lodestar_oppool_execution_payload_bid_pool_api_insert_outcome_total",
1363
- help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
1364
- labelNames: ["insertOutcome"],
1365
- }),
1366
1361
  apiValidationTime: register.histogram({
1367
1362
  name: "lodestar_api_execution_payload_bid_validation_time_seconds",
1368
1363
  help: "Time elapsed for signed execution payload bid validation - api path",