@lodestar/beacon-node 1.46.0-dev.ba5ee9aff0 → 1.46.0-dev.cae84b66d5

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 (123) hide show
  1. package/lib/api/impl/config/constants.d.ts +1 -0
  2. package/lib/api/impl/config/constants.d.ts.map +1 -1
  3. package/lib/api/impl/config/constants.js +2 -1
  4. package/lib/api/impl/config/constants.js.map +1 -1
  5. package/lib/api/impl/lodestar/index.d.ts.map +1 -1
  6. package/lib/api/impl/lodestar/index.js +2 -3
  7. package/lib/api/impl/lodestar/index.js.map +1 -1
  8. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  9. package/lib/chain/archiveStore/archiveStore.js +17 -3
  10. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  11. package/lib/chain/archiveStore/historicalState/historicalStateRegen.d.ts.map +1 -1
  12. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js +0 -1
  13. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js.map +1 -1
  14. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +1 -1
  15. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  16. package/lib/chain/bls/multithread/index.d.ts.map +1 -1
  17. package/lib/chain/bls/multithread/index.js +0 -1
  18. package/lib/chain/bls/multithread/index.js.map +1 -1
  19. package/lib/chain/errors/executionPayloadBid.d.ts +8 -1
  20. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  21. package/lib/chain/errors/executionPayloadBid.js +1 -0
  22. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  23. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  24. package/lib/chain/opPools/aggregatedAttestationPool.js +2 -1
  25. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  26. package/lib/chain/prepareNextSlot.js +1 -1
  27. package/lib/chain/prepareNextSlot.js.map +1 -1
  28. package/lib/chain/produceBlock/produceBlockBody.js +3 -3
  29. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  30. package/lib/chain/seenCache/seenExecutionPayloadBids.d.ts +6 -5
  31. package/lib/chain/seenCache/seenExecutionPayloadBids.d.ts.map +1 -1
  32. package/lib/chain/seenCache/seenExecutionPayloadBids.js +16 -8
  33. package/lib/chain/seenCache/seenExecutionPayloadBids.js.map +1 -1
  34. package/lib/chain/validation/blobSidecar.js +1 -1
  35. package/lib/chain/validation/executionPayloadBid.d.ts +0 -5
  36. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  37. package/lib/chain/validation/executionPayloadBid.js +70 -24
  38. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  39. package/lib/chain/validation/proposerPreferences.js +8 -8
  40. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  41. package/lib/chain/validatorMonitor.js +1 -1
  42. package/lib/chain/validatorMonitor.js.map +1 -1
  43. package/lib/metrics/nodeJsMetrics.js +1 -1
  44. package/lib/metrics/nodeJsMetrics.js.map +1 -1
  45. package/lib/network/core/networkCoreWorkerHandler.d.ts.map +1 -1
  46. package/lib/network/core/networkCoreWorkerHandler.js +2 -7
  47. package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
  48. package/lib/network/discv5/index.d.ts.map +1 -1
  49. package/lib/network/discv5/index.js +1 -4
  50. package/lib/network/discv5/index.js.map +1 -1
  51. package/lib/network/gossip/topic.d.ts +4 -4
  52. package/lib/network/libp2p/index.d.ts.map +1 -1
  53. package/lib/network/libp2p/index.js +2 -9
  54. package/lib/network/libp2p/index.js.map +1 -1
  55. package/lib/network/peers/datastore.d.ts +1 -1
  56. package/lib/network/peers/datastore.d.ts.map +1 -1
  57. package/lib/network/peers/datastore.js +1 -1
  58. package/lib/network/peers/datastore.js.map +1 -1
  59. package/lib/network/processor/index.d.ts.map +1 -1
  60. package/lib/network/processor/index.js +2 -1
  61. package/lib/network/processor/index.js.map +1 -1
  62. package/lib/network/reqresp/handlers/lightClientUpdatesByRange.d.ts.map +1 -1
  63. package/lib/network/reqresp/handlers/lightClientUpdatesByRange.js +5 -1
  64. package/lib/network/reqresp/handlers/lightClientUpdatesByRange.js.map +1 -1
  65. package/lib/sync/constants.d.ts +6 -5
  66. package/lib/sync/constants.d.ts.map +1 -1
  67. package/lib/sync/constants.js +6 -5
  68. package/lib/sync/constants.js.map +1 -1
  69. package/lib/sync/range/batch.d.ts +10 -1
  70. package/lib/sync/range/batch.d.ts.map +1 -1
  71. package/lib/sync/range/batch.js +28 -10
  72. package/lib/sync/range/batch.js.map +1 -1
  73. package/lib/sync/range/chain.d.ts.map +1 -1
  74. package/lib/sync/range/chain.js +30 -15
  75. package/lib/sync/range/chain.js.map +1 -1
  76. package/lib/sync/range/utils/batches.d.ts +18 -0
  77. package/lib/sync/range/utils/batches.d.ts.map +1 -1
  78. package/lib/sync/range/utils/batches.js +39 -0
  79. package/lib/sync/range/utils/batches.js.map +1 -1
  80. package/lib/util/profile.d.ts +3 -0
  81. package/lib/util/profile.d.ts.map +1 -1
  82. package/lib/util/profile.js +1 -27
  83. package/lib/util/profile.js.map +1 -1
  84. package/lib/util/sszBytes.js +1 -1
  85. package/package.json +25 -36
  86. package/src/api/impl/config/constants.ts +2 -0
  87. package/src/api/impl/lodestar/index.ts +2 -3
  88. package/src/chain/archiveStore/archiveStore.ts +20 -3
  89. package/src/chain/archiveStore/historicalState/historicalStateRegen.ts +0 -1
  90. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +1 -1
  91. package/src/chain/bls/multithread/index.ts +0 -1
  92. package/src/chain/errors/executionPayloadBid.ts +9 -1
  93. package/src/chain/opPools/aggregatedAttestationPool.ts +3 -1
  94. package/src/chain/prepareNextSlot.ts +1 -1
  95. package/src/chain/produceBlock/produceBlockBody.ts +3 -3
  96. package/src/chain/seenCache/seenExecutionPayloadBids.ts +22 -9
  97. package/src/chain/validation/blobSidecar.ts +1 -1
  98. package/src/chain/validation/executionPayloadBid.ts +87 -26
  99. package/src/chain/validation/proposerPreferences.ts +8 -8
  100. package/src/chain/validatorMonitor.ts +1 -1
  101. package/src/metrics/nodeJsMetrics.ts +1 -1
  102. package/src/network/core/networkCoreWorkerHandler.ts +2 -6
  103. package/src/network/discv5/index.ts +1 -4
  104. package/src/network/libp2p/index.ts +2 -10
  105. package/src/network/peers/datastore.ts +1 -1
  106. package/src/network/processor/index.ts +2 -1
  107. package/src/network/reqresp/handlers/lightClientUpdatesByRange.ts +5 -1
  108. package/src/sync/constants.ts +6 -5
  109. package/src/sync/range/batch.ts +31 -10
  110. package/src/sync/range/chain.ts +36 -16
  111. package/src/sync/range/utils/batches.ts +44 -0
  112. package/src/util/profile.ts +1 -31
  113. package/src/util/sszBytes.ts +1 -1
  114. package/lib/bun-wrappers/prometheus-gc-stats.d.ts +0 -2
  115. package/lib/bun-wrappers/prometheus-gc-stats.d.ts.map +0 -1
  116. package/lib/bun-wrappers/prometheus-gc-stats.js +0 -8
  117. package/lib/bun-wrappers/prometheus-gc-stats.js.map +0 -1
  118. package/lib/network/peers/datastore_bun.d.ts +0 -3
  119. package/lib/network/peers/datastore_bun.d.ts.map +0 -1
  120. package/lib/network/peers/datastore_bun.js +0 -3
  121. package/lib/network/peers/datastore_bun.js.map +0 -1
  122. package/src/bun-wrappers/prometheus-gc-stats.ts +0 -7
  123. package/src/network/peers/datastore_bun.ts +0 -3
@@ -1,4 +1,5 @@
1
1
  import {PublicKey} from "@chainsafe/blst";
2
+ import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
3
  import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
3
4
  import {
4
5
  computeEpochAtSlot,
@@ -8,7 +9,7 @@ import {
8
9
  isGasLimitTargetCompatible,
9
10
  isStatePostGloas,
10
11
  } from "@lodestar/state-transition";
11
- import {ValidatorIndex, gloas} from "@lodestar/types";
12
+ import {RootHex, Slot, ValidatorIndex, gloas} from "@lodestar/types";
12
13
  import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
13
14
  import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
14
15
  import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
@@ -36,12 +37,45 @@ const BID_INCREMENT_CAP_GWEI = 10_000_000;
36
37
  * Return the minimum value a new bid must have to be forwarded given the current highest bid.
37
38
  * Division before multiplication to stay within safe integer range for max gwei values.
38
39
  */
39
- export function getMinBidValue(currentHighestBid: number): number {
40
+ function getMinBidValue(currentHighestBid: number): number {
40
41
  const relativeIncrement = Math.floor(currentHighestBid / 10_000) * BID_INCREMENT_BPS;
41
42
  const increment = Math.min(Math.max(BID_INCREMENT_FLOOR_GWEI, relativeIncrement), BID_INCREMENT_CAP_GWEI);
42
43
  return currentHighestBid + increment;
43
44
  }
44
45
 
46
+ /**
47
+ * Check whether a bid builds on one of the paths compatible with the local head branch.
48
+ *
49
+ * The direct parent path is always allowed for proposer-boost reorgs. Otherwise the bid
50
+ * must build on the local head's full or empty payload variant, as selected for its slot.
51
+ */
52
+ function isBidCompatibleWithHead(
53
+ forkChoice: IForkChoice,
54
+ head: ProtoBlock,
55
+ bidSlot: Slot,
56
+ bidParentBlockRoot: RootHex,
57
+ bidParentBlockHash: RootHex
58
+ ): boolean {
59
+ const buildsOnParentBlock = bidParentBlockRoot === head.parentRoot;
60
+ const buildsOnParentPayload = bidParentBlockHash === head.parentBlockHash;
61
+
62
+ if (buildsOnParentBlock && buildsOnParentPayload) {
63
+ return true;
64
+ }
65
+
66
+ if (bidParentBlockRoot !== head.blockRoot) {
67
+ return false;
68
+ }
69
+
70
+ const buildsOnHeadPayload = bidParentBlockHash === head.executionPayloadBlockHash;
71
+
72
+ if (forkChoice.shouldBuildOnFull(head, bidSlot)) {
73
+ return buildsOnHeadPayload;
74
+ }
75
+
76
+ return buildsOnParentPayload;
77
+ }
78
+
45
79
  export async function validateApiExecutionPayloadBid(
46
80
  chain: IBeaconChain,
47
81
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
@@ -61,8 +95,8 @@ async function validateExecutionPayloadBid(
61
95
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
62
96
  ): Promise<{proposerIndex: ValidatorIndex}> {
63
97
  const bid = signedExecutionPayloadBid.message;
64
- const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
65
- const parentBlockHashHex = toRootHex(bid.parentBlockHash);
98
+ const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
99
+ const bidParentBlockHash = toRootHex(bid.parentBlockHash);
66
100
 
67
101
  // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
68
102
  if (
@@ -76,14 +110,40 @@ async function validateExecutionPayloadBid(
76
110
  });
77
111
  }
78
112
 
113
+ // [IGNORE] The bid is compatible with the current head branch.
114
+ const head = chain.forkChoice.getHead();
115
+ if (!isBidCompatibleWithHead(chain.forkChoice, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
116
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
117
+ code: ExecutionPayloadBidErrorCode.INCOMPATIBLE_WITH_HEAD,
118
+ slot: bid.slot,
119
+ parentBlockRoot: bidParentBlockRoot,
120
+ parentBlockHash: bidParentBlockHash,
121
+ headBlockRoot: head.blockRoot,
122
+ });
123
+ }
124
+
125
+ // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for
126
+ // the tuple `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`.
127
+ // Entries are only added after signature verification, so known tuples can be dropped before
128
+ // state regeneration and the other expensive validation steps.
129
+ if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex, bidParentBlockHash, bidParentBlockRoot)) {
130
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
131
+ code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN,
132
+ builderIndex: bid.builderIndex,
133
+ slot: bid.slot,
134
+ parentBlockRoot: bidParentBlockRoot,
135
+ parentBlockHash: bidParentBlockHash,
136
+ });
137
+ }
138
+
79
139
  // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
80
140
  // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
81
141
  // proposer-preferences lookup below from a known fork-choice block.
82
- const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
142
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
83
143
  if (parentBlock === null) {
84
144
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
85
145
  code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
86
- parentBlockRoot: parentBlockRootHex,
146
+ parentBlockRoot: bidParentBlockRoot,
87
147
  });
88
148
  }
89
149
 
@@ -122,7 +182,7 @@ async function validateExecutionPayloadBid(
122
182
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
123
183
  code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
124
184
  slot: bid.slot,
125
- parentBlockRoot: parentBlockRootHex,
185
+ parentBlockRoot: bidParentBlockRoot,
126
186
  dependentRoot: "unknown",
127
187
  });
128
188
  }
@@ -132,7 +192,7 @@ async function validateExecutionPayloadBid(
132
192
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
133
193
  code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
134
194
  slot: bid.slot,
135
- parentBlockRoot: parentBlockRootHex,
195
+ parentBlockRoot: bidParentBlockRoot,
136
196
  dependentRoot: dependentRootHex,
137
197
  });
138
198
  }
@@ -143,7 +203,7 @@ async function validateExecutionPayloadBid(
143
203
  .catch(() => {
144
204
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
145
205
  code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
146
- parentBlockRoot: parentBlockRootHex,
206
+ parentBlockRoot: bidParentBlockRoot,
147
207
  });
148
208
  });
149
209
 
@@ -184,7 +244,7 @@ async function validateExecutionPayloadBid(
184
244
  }
185
245
 
186
246
  // [REJECT] `bid.execution_payment` is zero.
187
- if (bid.executionPayment !== 0) {
247
+ if (bid.executionPayment !== 0n) {
188
248
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
189
249
  code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
190
250
  builderIndex: bid.builderIndex,
@@ -208,18 +268,18 @@ async function validateExecutionPayloadBid(
208
268
  // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent
209
269
  // payload's hash, since the new block doesn't have its own payload). Variant carries the
210
270
  // executed payload's gas_limit, which we use as `parent_gas_limit` below.
211
- const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex);
271
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(bidParentBlockRoot, bidParentBlockHash);
212
272
  if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
213
273
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
214
274
  code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
215
- parentBlockHash: parentBlockHashHex,
275
+ parentBlockHash: bidParentBlockHash,
216
276
  });
217
277
  }
218
278
 
219
279
  // [IGNORE] `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, target_gas_limit)`,
220
280
  // where `parent_gas_limit` is the `gas_limit` of the parent execution payload and
221
281
  // `target_gas_limit` is `proposer_preferences.target_gas_limit`.
222
- const bidGasLimit = bid.gasLimit;
282
+ const bidGasLimit = Number(bid.gasLimit);
223
283
  const parentGasLimit = parentPayloadVariant.executionPayloadGasLimit;
224
284
  const targetGasLimit = proposerPreferences.message.targetGasLimit;
225
285
  if (!isGasLimitTargetCompatible(parentGasLimit, bidGasLimit, targetGasLimit)) {
@@ -245,23 +305,12 @@ async function validateExecutionPayloadBid(
245
305
  });
246
306
  }
247
307
 
248
- // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot.
249
- if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) {
250
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
251
- code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN,
252
- builderIndex: bid.builderIndex,
253
- slot: bid.slot,
254
- parentBlockRoot: parentBlockRootHex,
255
- parentBlockHash: parentBlockHashHex,
256
- });
257
- }
258
-
259
308
  // [IGNORE] this bid is the highest value bid seen for the tuple
260
309
  // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`.
261
310
  // As a DoS prevention measure, the bid must also exceed the current highest bid by a minimum
262
311
  // increment, see https://github.com/ethereum/consensus-specs/pull/4831. This prevents spam
263
312
  // from builders submitting numerous bids with minimal value increments.
264
- const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex);
313
+ const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, bidParentBlockHash, bidParentBlockRoot);
265
314
  if (bestBid !== null && bid.value < getMinBidValue(bestBid.message.value)) {
266
315
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
267
316
  code: ExecutionPayloadBidErrorCode.BID_TOO_LOW,
@@ -306,8 +355,20 @@ async function validateExecutionPayloadBid(
306
355
  });
307
356
  }
308
357
 
358
+ // Repeat the seen check after the awaited signature verification to prevent concurrent bids
359
+ // for the same builder and tuple from both passing validation.
360
+ if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex, bidParentBlockHash, bidParentBlockRoot)) {
361
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
362
+ code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN,
363
+ builderIndex: bid.builderIndex,
364
+ slot: bid.slot,
365
+ parentBlockRoot: bidParentBlockRoot,
366
+ parentBlockHash: bidParentBlockHash,
367
+ });
368
+ }
369
+
309
370
  // Valid
310
- chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex);
371
+ chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex, bidParentBlockHash, bidParentBlockRoot);
311
372
 
312
373
  return {proposerIndex: proposerPreferences.message.validatorIndex};
313
374
  }
@@ -1,5 +1,5 @@
1
1
  import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
- import {GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {GENESIS_EPOCH, GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
3
3
  import {
4
4
  computeEpochAtSlot,
5
5
  computeStartSlotAtEpoch,
@@ -24,9 +24,10 @@ export async function validateGossipProposerPreferences(
24
24
  const dependentRootHex = toRootHex(dependentRoot);
25
25
  const proposalEpoch = computeEpochAtSlot(proposalSlot);
26
26
 
27
- // [IGNORE] `preferences.proposal_slot` is within the proposer lookahead.
28
- const currentEpoch = chain.clock.currentEpoch;
29
- if (proposalEpoch < currentEpoch || proposalEpoch > currentEpoch + MIN_SEED_LOOKAHEAD) {
27
+ // [IGNORE] `preferences.proposal_slot` is within the proposer lookahead,
28
+ // allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
29
+ const currentEpoch = computeEpochAtSlot(chain.clock.currentSlotWithGossipDisparity);
30
+ if (proposalEpoch > currentEpoch + MIN_SEED_LOOKAHEAD) {
30
31
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
31
32
  code: ProposerPreferencesErrorCode.INVALID_EPOCH,
32
33
  proposalSlot,
@@ -36,12 +37,11 @@ export async function validateGossipProposerPreferences(
36
37
 
37
38
  // [IGNORE] `preferences.proposal_slot` has not already passed, i.e. `proposal_slot > current_slot`,
38
39
  // allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
39
- const currentSlot = chain.clock.currentSlotWithGossipDisparity;
40
- if (proposalSlot <= currentSlot) {
40
+ if (chain.clock.msFromSlot(proposalSlot) > chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY) {
41
41
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
42
42
  code: ProposerPreferencesErrorCode.PROPOSAL_SLOT_PASSED,
43
43
  proposalSlot,
44
- currentSlot,
44
+ currentSlot: chain.clock.currentSlot,
45
45
  });
46
46
  }
47
47
 
@@ -55,7 +55,7 @@ export async function validateGossipProposerPreferences(
55
55
  });
56
56
  }
57
57
 
58
- const dependentEpoch = proposalEpoch - MIN_SEED_LOOKAHEAD;
58
+ const dependentEpoch = Math.max(GENESIS_EPOCH, proposalEpoch - MIN_SEED_LOOKAHEAD);
59
59
  const dependentRootSlot = getDependentRootSlot(dependentEpoch);
60
60
 
61
61
  // [REJECT] The slot of the block with root `preferences.dependent_root` is strictly less than
@@ -469,7 +469,7 @@ export function createValidatorMonitor(
469
469
  proposerIndex,
470
470
  src,
471
471
  builderIndex: bid.builderIndex,
472
- gasLimit: bid.gasLimit,
472
+ gasLimit: Number(bid.gasLimit),
473
473
  value: bid.value.toString(),
474
474
  parentBlockRoot: toRootHex(bid.parentBlockRoot),
475
475
  parentBlockHash: toRootHex(bid.parentBlockHash),
@@ -1,5 +1,5 @@
1
1
  import {Registry, collectDefaultMetrics} from "prom-client";
2
- import {gcStats} from "#prometheus-gc-stats-wrapper";
2
+ import {gcStats} from "@chainsafe/prometheus-gc-stats";
3
3
 
4
4
  export function collectNodeJSMetrics(register: Registry, prefix?: string): () => void {
5
5
  collectDefaultMetrics({
@@ -132,10 +132,6 @@ export class WorkerNetworkCore implements INetworkCore {
132
132
 
133
133
  const workerOpts: ConstructorParameters<typeof Worker>[1] = {
134
134
  workerData,
135
- };
136
- if (globalThis.Bun) {
137
- workerOpts.suppressTranspileTS = true;
138
- } else {
139
135
  /**
140
136
  * maxYoungGenerationSizeMb defaults to 152mb through the cli option defaults.
141
137
  * That default value was determined via https://github.com/ChainSafe/lodestar/issues/2115 and
@@ -147,8 +143,8 @@ export class WorkerNetworkCore implements INetworkCore {
147
143
  * showed that there is a pretty big window of "correct" values but we can always tune as
148
144
  * necessary
149
145
  */
150
- workerOpts.resourceLimits = {maxYoungGenerationSizeMb: opts.maxYoungGenerationSizeMb};
151
- }
146
+ resourceLimits: {maxYoungGenerationSizeMb: opts.maxYoungGenerationSizeMb},
147
+ };
152
148
 
153
149
  const worker = new Worker(path.join(workerDir, "networkCoreWorker.js"), workerOpts);
154
150
 
@@ -51,10 +51,7 @@ export class Discv5Worker extends (EventEmitter as {new (): StrictEventEmitter<E
51
51
  loggerOpts: opts.logger.toOpts(),
52
52
  genesisTime: opts.genesisTime,
53
53
  };
54
- const worker = new Worker("./worker.js", {
55
- suppressTranspileTS: Boolean(globalThis.Bun),
56
- workerData,
57
- } as ConstructorParameters<typeof Worker>[1]);
54
+ const worker = new Worker("./worker.js", {workerData} as ConstructorParameters<typeof Worker>[1]);
58
55
 
59
56
  const workerApi = await spawn<Discv5WorkerApi>(worker, {
60
57
  // A Lodestar Node may do very expensive task at start blocking the event loop and causing
@@ -9,7 +9,7 @@ import {Libp2pInit, createLibp2p} from "libp2p";
9
9
  import {Registry} from "prom-client";
10
10
  import {ENR} from "@chainsafe/enr";
11
11
  import {noise} from "@chainsafe/libp2p-noise";
12
- import {asCrypto, defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
12
+ import {defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
13
13
  import {quic} from "@chainsafe/libp2p-quic";
14
14
  import {Libp2p, LodestarComponents} from "../interface.js";
15
15
  import {NetworkOptions, defaultNetworkOptions} from "../options.js";
@@ -111,14 +111,6 @@ export async function createNodeJsLibp2p(
111
111
  }
112
112
  }
113
113
 
114
- const noiseCrypto = {
115
- ...defaultCrypto,
116
- };
117
- if (globalThis.Bun) {
118
- noiseCrypto.chaCha20Poly1305Decrypt = asCrypto.chaCha20Poly1305Decrypt;
119
- noiseCrypto.chaCha20Poly1305Encrypt = asCrypto.chaCha20Poly1305Encrypt;
120
- }
121
-
122
114
  return createLibp2p({
123
115
  privateKey,
124
116
  nodeInfo: {
@@ -130,7 +122,7 @@ export async function createNodeJsLibp2p(
130
122
  listen: localMultiaddrs,
131
123
  announce: [],
132
124
  },
133
- connectionEncrypters: [noise({crypto: noiseCrypto})],
125
+ connectionEncrypters: [noise({crypto: defaultCrypto})],
134
126
  transports,
135
127
  streamMuxers: [mplex({disconnectThreshold})],
136
128
  peerDiscovery,
@@ -1,7 +1,7 @@
1
1
  import {AbortOptions} from "@libp2p/interface";
2
2
  import {BaseDatastore} from "datastore-core";
3
+ import {LevelDatastore} from "datastore-level";
3
4
  import {Key, KeyQuery, Pair, Query} from "interface-datastore";
4
- import {LevelDatastore} from "#datastore-wrapper";
5
5
 
6
6
  type MemoryItem = {
7
7
  lastAccessedMs: number;
@@ -461,7 +461,8 @@ export class NetworkProcessor {
461
461
  break;
462
462
  }
463
463
  case GossipType.execution_payload_bid: {
464
- // instead of searching for the message root, this searches for the parent root
464
+ // Search for the parent independently of current head compatibility. A bid may arrive
465
+ // before its parent block, so compatibility is checked later during gossip validation.
465
466
  const parentBlockRoot = getParentBlockRootFromSignedExecutionPayloadBidSerialized(message.msg.data);
466
467
  const parentBlockHash = getParentBlockHashFromSignedExecutionPayloadBidSerialized(message.msg.data);
467
468
  if (
@@ -19,8 +19,12 @@ export async function* onLightClientUpdatesByRange(
19
19
  assertLightClientServer(chain.lightClientServer);
20
20
 
21
21
  const count = Math.min(MAX_REQUEST_LIGHT_CLIENT_UPDATES, requestBody.count);
22
+ if (!Number.isSafeInteger(requestBody.startPeriod)) {
23
+ throw new ResponseError(RespStatus.INVALID_REQUEST, "startPeriod out of range");
24
+ }
22
25
  let started = false;
23
- for (let period = requestBody.startPeriod; period < requestBody.startPeriod + count; period++) {
26
+ for (let i = 0; i < count; i++) {
27
+ const period = requestBody.startPeriod + i;
24
28
  try {
25
29
  const update = await chain.lightClientServer.getUpdate(period);
26
30
  const boundary = chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(update.signatureSlot));
@@ -15,12 +15,13 @@ export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
15
15
  export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
16
16
 
17
17
  /**
18
- * Consider batch faulty after downloading and processing this number of times
19
- * as in https://github.com/ChainSafe/lodestar/issues/8147 we cannot proceed the sync chain if there is unknown parent
20
- * from prior batch. For example a peer may send us a non-canonical chain segment or not returning all blocks
21
- * in that case we should throw error and `RangeSync` should remove that error chain and add a new one.
18
+ * Consider a batch faulty once its failed processing attempts EXCEED this number, i.e. the batch is
19
+ * processed up to `MAX_BATCH_PROCESSING_ATTEMPTS + 1` times before teardown (the check is `>`, same
20
+ * convention as `MAX_BATCH_DOWNLOAD_ATTEMPTS`).
21
+ * Some processing failures can be caused by a different batch, so range sync classifies the fault
22
+ * before deciding which batch to redownload.
22
23
  **/
23
- export const MAX_BATCH_PROCESSING_ATTEMPTS = 0;
24
+ export const MAX_BATCH_PROCESSING_ATTEMPTS = 3;
24
25
 
25
26
  /**
26
27
  * Number of slots to offset batches.
@@ -64,6 +64,7 @@ export type DownloadSuccessState = {
64
64
  status: BatchStatus.AwaitingProcessing;
65
65
  blocks: IBlockInput[];
66
66
  payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
67
+ attempt: Attempt;
67
68
  };
68
69
 
69
70
  export type BatchState =
@@ -589,7 +590,11 @@ export class Batch {
589
590
  }
590
591
 
591
592
  if (allComplete) {
592
- this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
593
+ const attempt: Attempt = {
594
+ peers: this.getSuccessfulPeers(),
595
+ hash: hashBlocks(blocks, this.config),
596
+ };
597
+ this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes, attempt};
593
598
  } else {
594
599
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
595
600
  this.requests = this.getRequests(blocks);
@@ -646,15 +651,11 @@ export class Batch {
646
651
  throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingProcessing));
647
652
  }
648
653
 
649
- const blocks = this.state.blocks;
650
- const payloadEnvelopes = this.state.payloadEnvelopes;
651
- const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
652
- // Reset successfulDownloads in case another download attempt needs to be made. When Attempt is successful or not
653
- // the peers that the data came from will be handled by the Attempt that goes for processing.
654
- const peers = this.getSuccessfulPeers();
654
+ const {blocks, payloadEnvelopes, attempt} = this.state;
655
+ // No need to track successfulDownloads anymore, the batch goes to Processing status.
655
656
  this.successfulDownloads.clear();
656
- this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
657
- return {blocks, payloadEnvelopes, peers};
657
+ this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt};
658
+ return {blocks, payloadEnvelopes, peers: attempt.peers};
658
659
  }
659
660
 
660
661
  /**
@@ -673,6 +674,26 @@ export class Batch {
673
674
  };
674
675
  }
675
676
 
677
+ /**
678
+ * Processing -> AwaitingProcessing
679
+ *
680
+ * The batch's own blocks are valid but processing failed because a previous batch did not
681
+ * deliver the parent. Keep the downloaded blocks and process them again once the previous
682
+ * batch is repaired.
683
+ */
684
+ retainForReprocessing(): void {
685
+ if (this.state.status !== BatchStatus.Processing) {
686
+ throw new BatchError(this.wrongStatusErrorType(BatchStatus.Processing));
687
+ }
688
+
689
+ this.state = {
690
+ status: BatchStatus.AwaitingProcessing,
691
+ blocks: this.state.blocks,
692
+ payloadEnvelopes: this.state.payloadEnvelopes,
693
+ attempt: this.state.attempt,
694
+ };
695
+ }
696
+
676
697
  /**
677
698
  * Processing -> AwaitingDownload
678
699
  */
@@ -750,7 +771,7 @@ export enum BatchErrorCode {
750
771
  INVALID_COUNT = "BATCH_ERROR_INVALID_COUNT",
751
772
  MAX_DOWNLOAD_ATTEMPTS = "BATCH_ERROR_MAX_DOWNLOAD_ATTEMPTS",
752
773
  MAX_PROCESSING_ATTEMPTS = "BATCH_ERROR_MAX_PROCESSING_ATTEMPTS",
753
- MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS = "MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS",
774
+ MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS = "BATCH_ERROR_MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS",
754
775
  }
755
776
 
756
777
  type BatchErrorType =
@@ -23,7 +23,9 @@ import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./b
23
23
  import {
24
24
  ChainPeersBalancer,
25
25
  PeerSyncInfo,
26
+ ProcessingFaultKind,
26
27
  batchStartEpochIsAfterSlot,
28
+ classifyProcessingFault,
27
29
  computeHighestTarget,
28
30
  getBatchSlotRange,
29
31
  getNextBatchToProcess,
@@ -708,23 +710,41 @@ export class SyncChain {
708
710
  this.triggerBatchProcessor();
709
711
  } else {
710
712
  this.logger.verbose("Batch process error", logCtx, res.err);
711
- batch.processingError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS
712
-
713
- // At least one block was successfully verified and imported, so we can be sure all
714
- // previous batches are valid and we only need to download the current failed batch.
715
- // TODO: Disabled for now
716
- // if (res.err instanceof ChainSegmentError && res.err.importedBlocks > 0) {
717
- // this.advanceChain(batch.startEpoch);
718
- // }
719
-
720
- // The current batch could not be processed, so either this or previous batches are invalid.
721
- // All previous batches (AwaitingValidation) are potentially faulty and marked for retry.
722
- // Progress will be drop back to `this.startEpoch`
723
- for (const pendingBatch of this.batches.values()) {
724
- if (pendingBatch.startEpoch < batch.startEpoch) {
725
- this.logger.verbose("Batch validation error", {id: this.logId, ...pendingBatch.getMetadata()});
726
- pendingBatch.validationError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS
713
+
714
+ const invalidatePreviousBatches = (): number => {
715
+ let invalidatedCount = 0;
716
+ for (const pendingBatch of this.batches.values()) {
717
+ if (
718
+ pendingBatch.startEpoch < batch.startEpoch &&
719
+ pendingBatch.state.status === BatchStatus.AwaitingValidation
720
+ ) {
721
+ this.logger.verbose("Batch validation error", {id: this.logId, ...pendingBatch.getMetadata()});
722
+ pendingBatch.validationError(res.err);
723
+ invalidatedCount++;
724
+ }
727
725
  }
726
+ return invalidatedCount;
727
+ };
728
+
729
+ const prevBatch = this.batches.get(batch.startEpoch - EPOCHS_PER_BATCH);
730
+ switch (classifyProcessingFault(res.err, batch, prevBatch)) {
731
+ case ProcessingFaultKind.CurrentBatch:
732
+ batch.processingError(res.err);
733
+ break;
734
+
735
+ case ProcessingFaultKind.PreviousBatch:
736
+ // If no previous batch was invalidated, it means the error is on this batch
737
+ if (invalidatePreviousBatches() === 0) {
738
+ batch.processingError(res.err);
739
+ } else {
740
+ batch.retainForReprocessing();
741
+ }
742
+ break;
743
+
744
+ case ProcessingFaultKind.Ambiguous:
745
+ batch.processingError(res.err);
746
+ invalidatePreviousBatches();
747
+ break;
728
748
  }
729
749
  }
730
750
 
@@ -1,6 +1,7 @@
1
1
  import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
2
  import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
3
  import {Epoch, Slot} from "@lodestar/types";
4
+ import {BlockError, BlockErrorCode} from "../../../chain/errors/index.js";
4
5
  import {BATCH_SLOT_OFFSET, EPOCHS_PER_BATCH} from "../../constants.js";
5
6
  import {Batch, BatchStatus} from "../batch.js";
6
7
 
@@ -117,3 +118,46 @@ export function isSyncChainDone(batches: Batch[], lastEpochWithProcessBlocks: Ep
117
118
 
118
119
  return batchStartEpochIsAfterSlot(lastEpochWithProcessBlocks, targetSlot);
119
120
  }
121
+
122
+ /**
123
+ * Where a batch processing error should be attributed.
124
+ */
125
+ export enum ProcessingFaultKind {
126
+ /** This batch's own blocks are bad or incomplete, so redownload this batch. */
127
+ CurrentBatch = "CurrentBatch",
128
+ /** This batch is valid, but a previous batch did not deliver the parent. */
129
+ PreviousBatch = "PreviousBatch",
130
+ /** The fault could be in this batch or a previous batch, so hedge both ways. */
131
+ Ambiguous = "Ambiguous",
132
+ }
133
+
134
+ /**
135
+ * Classify a processChainSegment error for a batch so range sync only redownloads
136
+ * the batch or batches that could actually be at fault.
137
+ * Note that range sync is supposed to only handle linear chain, for forky condition we'll handle it via backward sync
138
+ * in BlockInputSync or the upcoming/TODO beacon_blocks_by_head req/resp.
139
+ */
140
+ export function classifyProcessingFault(err: Error, batch: Batch, prevBatch: Batch | undefined): ProcessingFaultKind {
141
+ if (!(err instanceof BlockError)) {
142
+ return ProcessingFaultKind.CurrentBatch;
143
+ }
144
+
145
+ const {code} = err.type;
146
+ if (code !== BlockErrorCode.PARENT_BLOCK_UNKNOWN && code !== BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) {
147
+ return ProcessingFaultKind.CurrentBatch;
148
+ }
149
+
150
+ const firstErrorSlot = err.signedBlock.message.slot;
151
+ if (firstErrorSlot === batch.startSlot) {
152
+ // note that range sync is for linear chain, so this is clearly an issue of the previous batch
153
+ return prevBatch === undefined ? ProcessingFaultKind.CurrentBatch : ProcessingFaultKind.PreviousBatch;
154
+ }
155
+
156
+ const prevTailSlot = batch.startSlot - 1;
157
+ const prevHasFullTail =
158
+ code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN
159
+ ? (prevBatch?.getPayloadEnvelopes()?.get(prevTailSlot)?.hasPayloadEnvelope() ?? false)
160
+ : (prevBatch?.getBlocks().some((b) => b.slot === prevTailSlot) ?? false);
161
+
162
+ return prevHasFullTail ? ProcessingFaultKind.CurrentBatch : ProcessingFaultKind.Ambiguous;
163
+ }
@@ -8,21 +8,10 @@ export enum ProfileThread {
8
8
  DISC5 = "discv5",
9
9
  }
10
10
 
11
- /**
12
- * The time to take a Bun profile.
13
- * If we increase this time it'll potentiall cause the app to crash.
14
- * If we decrease this time, profile recorded will be fragmented and hard to analyze.
15
- */
16
- const BUN_PROFILE_MS = 3 * 1000;
17
-
18
- export async function profileThread(thread: ProfileThread, durationMs: number, dirpath: string): Promise<string> {
19
- return globalThis.Bun ? profileBun(thread, durationMs) : profileNodeJS(thread, durationMs, dirpath);
20
- }
21
-
22
11
  /**
23
12
  * Take `durationMs` profile of the current thread and return the persisted file path.
24
13
  */
25
- async function profileNodeJS(thread: ProfileThread, durationMs: number, dirpath: string): Promise<string> {
14
+ export async function profileThread(thread: ProfileThread, durationMs: number, dirpath: string): Promise<string> {
26
15
  const inspector = await import("node:inspector");
27
16
 
28
17
  // due to some typing issues, not able to use promisify here
@@ -54,25 +43,6 @@ async function profileNodeJS(thread: ProfileThread, durationMs: number, dirpath:
54
43
  return filePath;
55
44
  }
56
45
 
57
- /**
58
- * Unlike NodeJS, Bun console.profile() api flush data to the inspector,
59
- * so this api returns ms taken of this profile instead of file path.
60
- */
61
- async function profileBun(thread: ProfileThread, durationMs: number): Promise<string> {
62
- const start = Date.now();
63
- let now = Date.now();
64
- while (now - start < durationMs) {
65
- // biome-ignore lint/suspicious/noConsole: need to use console api to profile in Bun
66
- console.profile(String(now));
67
- await sleep(BUN_PROFILE_MS);
68
- // biome-ignore lint/suspicious/noConsole: need to use console api to profile in Bun
69
- console.profileEnd(String(now));
70
- now = Date.now();
71
- }
72
-
73
- return `Successfully take Bun ${thread} thread profile in ${now - start}ms. Check your inspector to see the profile.`;
74
- }
75
-
76
46
  /**
77
47
  * Write heap snapshot of the current thread to the specified file.
78
48
  */