@lodestar/beacon-node 1.43.0-dev.2fba242f5d → 1.43.0-dev.374360e50a

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 (79) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +2 -1
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  5. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  6. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  7. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  8. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  9. package/lib/chain/blocks/importBlock.js +0 -16
  10. package/lib/chain/blocks/importBlock.js.map +1 -1
  11. package/lib/chain/blocks/importExecutionPayload.js +2 -2
  12. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  13. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +3 -0
  14. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  15. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +4 -1
  16. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  17. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  18. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -2
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/chain.d.ts.map +1 -1
  22. package/lib/chain/chain.js +10 -9
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  25. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  26. package/lib/chain/errors/proposerPreferences.js +1 -0
  27. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  28. package/lib/chain/prepareNextSlot.js +1 -1
  29. package/lib/chain/prepareNextSlot.js.map +1 -1
  30. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  31. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  32. package/lib/chain/produceBlock/produceBlockBody.js +1 -0
  33. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  34. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +7 -4
  35. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  36. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +30 -12
  37. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  38. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  39. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  40. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  41. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  42. package/lib/chain/validation/executionPayloadBid.js +11 -8
  43. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  44. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  45. package/lib/chain/validation/proposerPreferences.js +39 -17
  46. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  47. package/lib/network/gossip/topic.d.ts +2 -0
  48. package/lib/network/gossip/topic.d.ts.map +1 -1
  49. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  50. package/lib/network/processor/gossipHandlers.js +14 -0
  51. package/lib/network/processor/gossipHandlers.js.map +1 -1
  52. package/lib/sync/range/batch.d.ts.map +1 -1
  53. package/lib/sync/range/batch.js +54 -17
  54. package/lib/sync/range/batch.js.map +1 -1
  55. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  56. package/lib/sync/utils/downloadByRange.js +2 -4
  57. package/lib/sync/utils/downloadByRange.js.map +1 -1
  58. package/lib/util/sszBytes.d.ts.map +1 -1
  59. package/lib/util/sszBytes.js +8 -6
  60. package/lib/util/sszBytes.js.map +1 -1
  61. package/package.json +15 -15
  62. package/src/api/impl/validator/index.ts +2 -1
  63. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  64. package/src/chain/blocks/importBlock.ts +0 -18
  65. package/src/chain/blocks/importExecutionPayload.ts +2 -2
  66. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +4 -1
  67. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -2
  68. package/src/chain/chain.ts +12 -9
  69. package/src/chain/errors/proposerPreferences.ts +9 -1
  70. package/src/chain/prepareNextSlot.ts +1 -1
  71. package/src/chain/produceBlock/produceBlockBody.ts +2 -0
  72. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +43 -14
  73. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  74. package/src/chain/validation/executionPayloadBid.ts +11 -8
  75. package/src/chain/validation/proposerPreferences.ts +37 -18
  76. package/src/network/processor/gossipHandlers.ts +18 -0
  77. package/src/sync/range/batch.ts +54 -19
  78. package/src/sync/utils/downloadByRange.ts +3 -5
  79. package/src/util/sszBytes.ts +8 -6
@@ -3,12 +3,11 @@ import {
3
3
  computeEpochAtSlot,
4
4
  createSingleSignatureSetFromComponents,
5
5
  getProposerPreferencesSigningRoot,
6
- isStatePostGloas,
7
6
  } from "@lodestar/state-transition";
8
- import {gloas} from "@lodestar/types";
7
+ import {ValidatorIndex, gloas} from "@lodestar/types";
8
+ import {toRootHex} from "@lodestar/utils";
9
9
  import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
- import {RegenCaller} from "../regen/index.js";
12
11
 
13
12
  /**
14
13
  * Validates a gossiped `SignedProposerPreferences` per
@@ -19,7 +18,8 @@ export async function validateGossipProposerPreferences(
19
18
  signedProposerPreferences: gloas.SignedProposerPreferences
20
19
  ): Promise<void> {
21
20
  const preferences = signedProposerPreferences.message;
22
- const {proposalSlot, validatorIndex} = preferences;
21
+ const {proposalSlot, validatorIndex, dependentRoot} = preferences;
22
+ const dependentRootHex = toRootHex(dependentRoot);
23
23
  const proposalEpoch = computeEpochAtSlot(proposalSlot);
24
24
 
25
25
  // [IGNORE] `preferences.proposal_slot` is in the current or next epoch.
@@ -42,32 +42,51 @@ export async function validateGossipProposerPreferences(
42
42
  });
43
43
  }
44
44
 
45
- const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipProposerPreferences);
46
- if (!isStatePostGloas(state)) {
47
- throw new Error(`Expected gloas+ state for proposer preferences validation, got fork=${state.forkName}`);
45
+ // [IGNORE] The block with root `dependent_root` has been seen by the node.
46
+ // Resolve the proposer lookahead for the message's branch via head state (fast path) or
47
+ // the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for
48
+ // any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles
49
+ // narrow timing windows where the checkpoint state isn't yet populated.
50
+ const headState = chain.getHeadState();
51
+ let proposers: ValidatorIndex[] | null = null;
52
+ if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) {
53
+ proposers = headState.currentProposers;
54
+ } else if (headState.epoch === proposalEpoch - 1 && headState.nextDecisionRoot === dependentRootHex) {
55
+ proposers = headState.nextProposers;
56
+ } else {
57
+ // Sync lookup only to not trigger disk reload from gossip input.
58
+ const checkpointState = chain.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex});
59
+ if (checkpointState !== null) {
60
+ // State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from
61
+ // the state's perspective) live in `nextProposers`.
62
+ proposers = checkpointState.nextProposers;
63
+ }
64
+ }
65
+ if (proposers === null) {
66
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
67
+ code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT,
68
+ proposalSlot,
69
+ dependentRoot: dependentRootHex,
70
+ });
48
71
  }
49
72
 
50
- // [REJECT] `preferences.validator_index` is present at the correct slot in the current or next
51
- // epoch's portion of `state.proposer_lookahead` i.e. `is_valid_proposal_slot(state, preferences)`
52
- // returns True.
53
- const epochOffset = proposalEpoch - state.epoch;
54
- const proposers = epochOffset === 0 ? state.currentProposers : state.nextProposers;
55
- const expectedProposer = proposers[proposalSlot % SLOTS_PER_EPOCH];
56
- if (epochOffset < 0 || epochOffset > 1 || expectedProposer !== validatorIndex) {
73
+ // [REJECT] `is_valid_proposal_slot(state, preferences)` returns True.
74
+ if (proposers[proposalSlot % SLOTS_PER_EPOCH] !== validatorIndex) {
57
75
  throw new ProposerPreferencesError(GossipAction.REJECT, {
58
76
  code: ProposerPreferencesErrorCode.INVALID_PROPOSER,
59
77
  proposalSlot,
60
78
  validatorIndex,
79
+ dependentRoot: dependentRootHex,
61
80
  });
62
81
  }
63
82
 
64
- // [IGNORE] The `signed_proposer_preferences` is the first valid message received from the validator
65
- // with index `preferences.validator_index` and the given slot `preferences.proposal_slot`.
66
- if (chain.seenProposerPreferences.isKnown(proposalSlot, validatorIndex)) {
83
+ // [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index).
84
+ if (chain.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) {
67
85
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
68
86
  code: ProposerPreferencesErrorCode.ALREADY_KNOWN,
69
87
  proposalSlot,
70
88
  validatorIndex,
89
+ dependentRoot: dependentRootHex,
71
90
  });
72
91
  }
73
92
 
@@ -87,5 +106,5 @@ export async function validateGossipProposerPreferences(
87
106
  }
88
107
 
89
108
  // Valid
90
- chain.seenProposerPreferences.add(proposalSlot, validatorIndex);
109
+ chain.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex);
91
110
  }
@@ -607,6 +607,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
607
607
  // Returns the delay between the start of `block.slot` and `current time`
608
608
  const delaySec = chain.clock.secFromSlot(slot);
609
609
  metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
610
+
611
+ if (isForkPostGloas(blockInput.forkName)) {
612
+ const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
613
+ // This payloadInput should have been created just after gossip validation
614
+ if (!payloadInput) {
615
+ throw Error(
616
+ `PayloadEnvelopeInput not seeded for block ${blockInput.blockRootHex} during gossip processing`
617
+ );
618
+ }
619
+
620
+ // Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments
621
+ // which is all the information we need so there is no reason to delay until execution payload arrives
622
+ // TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here
623
+ // (for example later in the slot). Do not couple retries to incoming gossip columns.
624
+ // Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any
625
+ // in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path.
626
+ chain.getBlobsTracker.triggerGetBlobs(payloadInput);
627
+ }
610
628
  })
611
629
  .catch((e) => {
612
630
  // Adjust verbosity based on error type
@@ -202,6 +202,7 @@ export class Batch {
202
202
  const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
203
203
 
204
204
  // ensure blocks are in slot-wise order
205
+ const isPostGloas = isForkPostGloas(this.forkName);
205
206
  for (const blockInput of blocks) {
206
207
  const blockSlot = blockInput.slot;
207
208
  // check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
@@ -217,21 +218,36 @@ export class Batch {
217
218
  if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
218
219
  blockStartSlot = blockSlot + 1;
219
220
  }
220
- if (
221
- blockInput.hasBlock() &&
222
- envelopeStartSlot === blockSlot &&
223
- envelopesBySlot.get(blockSlot)?.hasPayloadEnvelope()
224
- ) {
225
- envelopeStartSlot = blockSlot + 1;
226
- }
227
- if (!blockInput.hasAllData()) {
228
- if (isBlockInputColumns(blockInput)) {
229
- for (const index of blockInput.getMissingSampledColumnMeta().missing) {
221
+
222
+ // Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
223
+ // which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
224
+ // a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
225
+ if (isPostGloas) {
226
+ // Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
227
+ const payloadInput = envelopesBySlot.get(blockSlot);
228
+ if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
229
+ envelopeStartSlot = blockSlot + 1;
230
+ }
231
+ if (payloadInput && !payloadInput.hasComputedAllData()) {
232
+ for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
230
233
  neededColumns.add(index);
231
234
  }
235
+ } else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
236
+ // Only advance dataStartSlot when we know columns for this slot are complete. If
237
+ // payloadInput is missing entirely we cannot tell, so stop here so the next round
238
+ // re-requests columns (and envelopes) starting at this slot.
239
+ dataStartSlot = blockSlot + 1;
240
+ }
241
+ } else {
242
+ if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
243
+ if (isBlockInputColumns(blockInput)) {
244
+ for (const index of blockInput.getMissingSampledColumnMeta().missing) {
245
+ neededColumns.add(index);
246
+ }
247
+ }
248
+ } else if (dataStartSlot === blockSlot) {
249
+ dataStartSlot = blockSlot + 1;
232
250
  }
233
- } else if (dataStartSlot === blockSlot) {
234
- dataStartSlot = blockSlot + 1;
235
251
  }
236
252
  }
237
253
 
@@ -251,11 +267,15 @@ export class Batch {
251
267
  // range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
252
268
  const count = endSlot - dataStartSlot + 1;
253
269
  if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
254
- requests.columnsRequest = {
255
- count,
256
- startSlot: dataStartSlot,
257
- columns: Array.from(neededColumns),
258
- };
270
+ // Skip the column re-request when we have no specific column indices outstanding.
271
+ // Peer rejects an empty `columns` list
272
+ if (neededColumns.size > 0) {
273
+ requests.columnsRequest = {
274
+ count,
275
+ startSlot: dataStartSlot,
276
+ columns: Array.from(neededColumns),
277
+ };
278
+ }
259
279
  } else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
260
280
  requests.blobsRequest = {
261
281
  count,
@@ -379,7 +399,11 @@ export class Batch {
379
399
  const slots = new Set<number>();
380
400
  for (const block of blocks) {
381
401
  slots.add(block.slot);
382
- if (!block.hasBlockAndAllData()) {
402
+ const dataComplete = isBlockInputColumns(block)
403
+ ? // by_range needs to download all columns
404
+ block.hasBlock() && block.hasComputedAllData()
405
+ : block.hasBlockAndAllData();
406
+ if (!dataComplete) {
383
407
  allComplete = false;
384
408
  }
385
409
  }
@@ -395,11 +419,22 @@ export class Batch {
395
419
  }
396
420
  const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
397
421
 
422
+ if (allComplete && isForkPostGloas(this.forkName)) {
423
+ for (const block of blocks) {
424
+ const payloadInput = newPayloadEnvelopes?.get(block.slot);
425
+ // by_range needs to download all columns
426
+ if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
427
+ allComplete = false;
428
+ break;
429
+ }
430
+ }
431
+ }
432
+
398
433
  if (allComplete) {
399
434
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
400
435
  } else {
401
- this.requests = this.getRequests(blocks);
402
436
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
437
+ this.requests = this.getRequests(blocks);
403
438
  }
404
439
 
405
440
  return this.state as DownloadSuccessState;
@@ -202,12 +202,10 @@ export function cacheByRangeResponses({
202
202
  });
203
203
  }
204
204
 
205
- // Attach envelopes to entries whose envelope was returned by the peer. The returned
206
- // payloadEnvelopes map only contains entries with envelopes ready for importExecutionPayload.
207
- let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null = null;
205
+ let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null =
206
+ existingPayloadEnvelopes !== null ? new Map(existingPayloadEnvelopes) : null;
208
207
  if (downloadedPayloadEnvelopes !== null) {
209
- payloadEnvelopes = new Map(existingPayloadEnvelopes ?? []);
210
-
208
+ payloadEnvelopes ??= new Map();
211
209
  for (const [slot, envelope] of downloadedPayloadEnvelopes) {
212
210
  const blockInput = updatedBatchBlocks.get(slot);
213
211
  if (!blockInput?.hasBlock() || !isForkPostGloas(blockInput.forkName)) {
@@ -558,9 +558,10 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr
558
558
  * └─ ExecutionPayloadEnvelope (starts at byte 100):
559
559
  * ├─ 4 bytes: payload offset
560
560
  * ├─ 4 bytes: executionRequests offset
561
- * ├─ 8 bytes: builderIndex (offset 108-115)
562
- * ├─ 32 bytes: beaconBlockRoot (offset 116-147)
563
- * └─ variable: payload data (starts at envelope + 48)
561
+ * ├─ 8 bytes: builderIndex (offset 108-115)
562
+ * ├─ 32 bytes: beaconBlockRoot (offset 116-147)
563
+ * ├─ 32 bytes: parentBeaconBlockRoot (offset 148-179) new in Gloas alpha.6 (consensus-specs#5152)
564
+ * └─ variable: payload data (starts at envelope + 80)
564
565
  * └─ ExecutionPayload fixed portion includes slotNumber at offset 532
565
566
  */
566
567
  const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4;
@@ -576,12 +577,13 @@ const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
576
577
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
577
578
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116
578
579
 
579
- // Envelope fixed portion (without slot): payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) = 48
580
+ // Envelope fixed portion: payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) + parentBeaconBlockRoot(32) = 80
580
581
  const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE =
581
582
  EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET +
582
583
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
583
584
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE +
584
- ROOT_SIZE; // 48
585
+ ROOT_SIZE +
586
+ ROOT_SIZE; // 80
585
587
 
586
588
  // slotNumber offset within ExecutionPayload fixed portion:
587
589
  // parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) +
@@ -595,7 +597,7 @@ const ENVELOPE_START_IN_SIGNED =
595
597
  SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100
596
598
 
597
599
  const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
598
- ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 532 = 680
600
+ ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 80 + 532 = 712
599
601
 
600
602
  export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null {
601
603
  if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) {