@lodestar/beacon-node 1.50.0-dev.d99080f29a → 1.50.0-dev.e8dec5f2fe

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 (193) hide show
  1. package/lib/api/impl/beacon/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/index.js +13 -0
  3. package/lib/api/impl/beacon/index.js.map +1 -1
  4. package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/state/index.js +23 -3
  6. package/lib/api/impl/beacon/state/index.js.map +1 -1
  7. package/lib/api/impl/debug/index.d.ts.map +1 -1
  8. package/lib/api/impl/debug/index.js +26 -18
  9. package/lib/api/impl/debug/index.js.map +1 -1
  10. package/lib/api/impl/validator/index.d.ts +1 -0
  11. package/lib/api/impl/validator/index.d.ts.map +1 -1
  12. package/lib/api/impl/validator/index.js +23 -1
  13. package/lib/api/impl/validator/index.js.map +1 -1
  14. package/lib/chain/archiveStore/archiveStore.d.ts +5 -0
  15. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  16. package/lib/chain/archiveStore/archiveStore.js +14 -24
  17. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  18. package/lib/chain/archiveStore/historicalState/getHistoricalState.d.ts +2 -2
  19. package/lib/chain/archiveStore/historicalState/getHistoricalState.d.ts.map +1 -1
  20. package/lib/chain/archiveStore/historicalState/getHistoricalState.js +5 -7
  21. package/lib/chain/archiveStore/historicalState/getHistoricalState.js.map +1 -1
  22. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js +1 -1
  23. package/lib/chain/archiveStore/historicalState/historicalStateRegen.js.map +1 -1
  24. package/lib/chain/archiveStore/historicalState/types.d.ts +2 -2
  25. package/lib/chain/archiveStore/historicalState/types.d.ts.map +1 -1
  26. package/lib/chain/archiveStore/historicalState/worker.js +1 -1
  27. package/lib/chain/archiveStore/historicalState/worker.js.map +1 -1
  28. package/lib/chain/archiveStore/interface.d.ts +1 -1
  29. package/lib/chain/archiveStore/interface.d.ts.map +1 -1
  30. package/lib/chain/archiveStore/utils/pruneHistory.d.ts +1 -1
  31. package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
  32. package/lib/chain/archiveStore/utils/pruneHistory.js +2 -1
  33. package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
  34. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  35. package/lib/chain/blocks/importBlock.js +7 -3
  36. package/lib/chain/blocks/importBlock.js.map +1 -1
  37. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  38. package/lib/chain/blocks/verifyBlock.js +1 -1
  39. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  40. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts +2 -1
  41. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.d.ts.map +1 -1
  42. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js +5 -4
  43. package/lib/chain/blocks/verifyBlocksStateTransitionOnly.js.map +1 -1
  44. package/lib/chain/chain.d.ts +4 -1
  45. package/lib/chain/chain.d.ts.map +1 -1
  46. package/lib/chain/chain.js +42 -5
  47. package/lib/chain/chain.js.map +1 -1
  48. package/lib/chain/emitter.d.ts +7 -1
  49. package/lib/chain/emitter.d.ts.map +1 -1
  50. package/lib/chain/emitter.js +5 -0
  51. package/lib/chain/emitter.js.map +1 -1
  52. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  53. package/lib/chain/forkChoice/index.js +2 -0
  54. package/lib/chain/forkChoice/index.js.map +1 -1
  55. package/lib/chain/interface.d.ts +1 -0
  56. package/lib/chain/interface.d.ts.map +1 -1
  57. package/lib/chain/opPools/aggregatedAttestationPool.d.ts +4 -2
  58. package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
  59. package/lib/chain/opPools/aggregatedAttestationPool.js +30 -8
  60. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  61. package/lib/chain/opPools/proposerPreferencesPool.d.ts +4 -4
  62. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  63. package/lib/chain/opPools/proposerPreferencesPool.js +14 -11
  64. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  65. package/lib/chain/options.d.ts +2 -1
  66. package/lib/chain/options.d.ts.map +1 -1
  67. package/lib/chain/options.js +1 -1
  68. package/lib/chain/options.js.map +1 -1
  69. package/lib/chain/produceBlock/computeNewStateRoot.js +1 -1
  70. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  71. package/lib/chain/regen/regen.d.ts +2 -0
  72. package/lib/chain/regen/regen.d.ts.map +1 -1
  73. package/lib/chain/regen/regen.js +10 -4
  74. package/lib/chain/regen/regen.js.map +1 -1
  75. package/lib/chain/seenCache/seenGossipBlockInput.d.ts +7 -0
  76. package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
  77. package/lib/chain/seenCache/seenGossipBlockInput.js +21 -0
  78. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  79. package/lib/chain/shufflingCache.d.ts +1 -0
  80. package/lib/chain/shufflingCache.d.ts.map +1 -1
  81. package/lib/chain/shufflingCache.js +4 -0
  82. package/lib/chain/shufflingCache.js.map +1 -1
  83. package/lib/chain/validatorMonitor.d.ts +5 -1
  84. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  85. package/lib/chain/validatorMonitor.js +64 -54
  86. package/lib/chain/validatorMonitor.js.map +1 -1
  87. package/lib/db/beacon.d.ts +2 -0
  88. package/lib/db/beacon.d.ts.map +1 -1
  89. package/lib/db/beacon.js +3 -0
  90. package/lib/db/beacon.js.map +1 -1
  91. package/lib/db/buckets.d.ts +2 -1
  92. package/lib/db/buckets.d.ts.map +1 -1
  93. package/lib/db/buckets.js +1 -0
  94. package/lib/db/buckets.js.map +1 -1
  95. package/lib/db/interface.d.ts +2 -0
  96. package/lib/db/interface.d.ts.map +1 -1
  97. package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
  98. package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
  99. package/lib/db/repositories/earliestAvailableSlot.d.ts +11 -0
  100. package/lib/db/repositories/earliestAvailableSlot.d.ts.map +1 -0
  101. package/lib/db/repositories/earliestAvailableSlot.js +19 -0
  102. package/lib/db/repositories/earliestAvailableSlot.js.map +1 -0
  103. package/lib/execution/engine/disabled.d.ts +0 -2
  104. package/lib/execution/engine/disabled.d.ts.map +1 -1
  105. package/lib/execution/engine/disabled.js +0 -6
  106. package/lib/execution/engine/disabled.js.map +1 -1
  107. package/lib/execution/engine/http.d.ts +1 -3
  108. package/lib/execution/engine/http.d.ts.map +1 -1
  109. package/lib/execution/engine/http.js +2 -17
  110. package/lib/execution/engine/http.js.map +1 -1
  111. package/lib/execution/engine/interface.d.ts +2 -4
  112. package/lib/execution/engine/interface.d.ts.map +1 -1
  113. package/lib/execution/engine/mock.d.ts +0 -2
  114. package/lib/execution/engine/mock.d.ts.map +1 -1
  115. package/lib/execution/engine/mock.js +0 -8
  116. package/lib/execution/engine/mock.js.map +1 -1
  117. package/lib/execution/engine/types.d.ts +0 -8
  118. package/lib/execution/engine/types.d.ts.map +1 -1
  119. package/lib/execution/engine/types.js.map +1 -1
  120. package/lib/metrics/metrics/lodestar.d.ts +1 -0
  121. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  122. package/lib/metrics/metrics/lodestar.js +4 -0
  123. package/lib/metrics/metrics/lodestar.js.map +1 -1
  124. package/lib/metrics/metrics.d.ts +7 -3
  125. package/lib/metrics/metrics.d.ts.map +1 -1
  126. package/lib/metrics/metrics.js +4 -4
  127. package/lib/metrics/metrics.js.map +1 -1
  128. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  129. package/lib/network/processor/gossipHandlers.js +59 -63
  130. package/lib/network/processor/gossipHandlers.js.map +1 -1
  131. package/lib/node/nodejs.d.ts +3 -1
  132. package/lib/node/nodejs.d.ts.map +1 -1
  133. package/lib/node/nodejs.js +27 -4
  134. package/lib/node/nodejs.js.map +1 -1
  135. package/lib/sync/range/batch.d.ts +6 -0
  136. package/lib/sync/range/batch.d.ts.map +1 -1
  137. package/lib/sync/range/batch.js +11 -0
  138. package/lib/sync/range/batch.js.map +1 -1
  139. package/lib/sync/range/chain.d.ts.map +1 -1
  140. package/lib/sync/range/chain.js +6 -1
  141. package/lib/sync/range/chain.js.map +1 -1
  142. package/lib/sync/unknownBlock.d.ts +5 -2
  143. package/lib/sync/unknownBlock.d.ts.map +1 -1
  144. package/lib/sync/unknownBlock.js +44 -20
  145. package/lib/sync/unknownBlock.js.map +1 -1
  146. package/lib/sync/utils/downloadByRange.d.ts +8 -0
  147. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  148. package/lib/sync/utils/downloadByRange.js +32 -9
  149. package/lib/sync/utils/downloadByRange.js.map +1 -1
  150. package/package.json +17 -17
  151. package/src/api/impl/beacon/index.ts +15 -0
  152. package/src/api/impl/beacon/state/index.ts +31 -3
  153. package/src/api/impl/debug/index.ts +28 -19
  154. package/src/api/impl/validator/index.ts +24 -2
  155. package/src/chain/archiveStore/archiveStore.ts +19 -25
  156. package/src/chain/archiveStore/historicalState/getHistoricalState.ts +5 -7
  157. package/src/chain/archiveStore/historicalState/historicalStateRegen.ts +1 -1
  158. package/src/chain/archiveStore/historicalState/types.ts +2 -2
  159. package/src/chain/archiveStore/historicalState/worker.ts +1 -2
  160. package/src/chain/archiveStore/interface.ts +1 -1
  161. package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
  162. package/src/chain/blocks/importBlock.ts +9 -3
  163. package/src/chain/blocks/verifyBlock.ts +1 -0
  164. package/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +6 -3
  165. package/src/chain/chain.ts +59 -4
  166. package/src/chain/emitter.ts +7 -1
  167. package/src/chain/forkChoice/index.ts +2 -0
  168. package/src/chain/interface.ts +1 -0
  169. package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
  170. package/src/chain/opPools/proposerPreferencesPool.ts +14 -10
  171. package/src/chain/options.ts +3 -3
  172. package/src/chain/produceBlock/computeNewStateRoot.ts +1 -1
  173. package/src/chain/regen/regen.ts +12 -4
  174. package/src/chain/seenCache/seenGossipBlockInput.ts +23 -0
  175. package/src/chain/shufflingCache.ts +5 -0
  176. package/src/chain/validatorMonitor.ts +75 -57
  177. package/src/db/beacon.ts +3 -0
  178. package/src/db/buckets.ts +1 -0
  179. package/src/db/interface.ts +2 -0
  180. package/src/db/repositories/earliestAvailableSlot.ts +19 -0
  181. package/src/execution/engine/disabled.ts +0 -8
  182. package/src/execution/engine/http.ts +1 -30
  183. package/src/execution/engine/interface.ts +2 -6
  184. package/src/execution/engine/mock.ts +1 -19
  185. package/src/execution/engine/types.ts +0 -10
  186. package/src/metrics/metrics/lodestar.ts +4 -0
  187. package/src/metrics/metrics.ts +21 -6
  188. package/src/network/processor/gossipHandlers.ts +90 -76
  189. package/src/node/nodejs.ts +35 -3
  190. package/src/sync/range/batch.ts +12 -0
  191. package/src/sync/range/chain.ts +7 -1
  192. package/src/sync/unknownBlock.ts +57 -21
  193. package/src/sync/utils/downloadByRange.ts +47 -8
@@ -12,9 +12,10 @@ import {
12
12
  isForkPostElectra,
13
13
  isForkPostGloas,
14
14
  } from "@lodestar/params";
15
- import {computeTimeAtSlot} from "@lodestar/state-transition";
15
+ import {computeTimeAtSlot, signedBlockToSignedHeader} from "@lodestar/state-transition";
16
16
  import {
17
17
  Root,
18
+ RootHex,
18
19
  SignedBeaconBlock,
19
20
  SingleAttestation,
20
21
  Slot,
@@ -78,6 +79,7 @@ import {
78
79
  validateGossipSyncCommittee,
79
80
  validateGossipVoluntaryExit,
80
81
  validateSyncCommitteeGossipContributionAndProof,
82
+ verifyBlockProposerSignature,
81
83
  } from "../../chain/validation/index.js";
82
84
  import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js";
83
85
  import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js";
@@ -148,37 +150,16 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
148
150
  function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers {
149
151
  const {chain, config, metrics, logger, core} = modules;
150
152
 
151
- async function validateBeaconBlock(
153
+ /**
154
+ * Add a gossip block to the seen caches. The proposer signature of the block MUST be verified.
155
+ */
156
+ function cacheSignatureVerifiedBlock(
152
157
  signedBlock: SignedBeaconBlock,
158
+ blockRootHex: RootHex,
153
159
  fork: ForkName,
154
160
  peerIdStr: string,
155
161
  seenTimestampSec: number
156
- ): Promise<IBlockInput> {
157
- const slot = signedBlock.message.slot;
158
- const forkTypes = config.getForkTypes(slot);
159
- const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
160
- const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
161
- const recvToValLatency = Date.now() / 1000 - seenTimestampSec;
162
-
163
- // always set block to seen cache for all forks so that we don't need to download it
164
- // TODO: validate block before adding to cache
165
- // tracked in https://github.com/ChainSafe/lodestar/issues/7957
166
-
167
- const logCtx = {
168
- slot,
169
- root: blockRootHex,
170
- currentSlot: chain.clock.currentSlot,
171
- peerId: peerIdStr,
172
- delaySec,
173
- recvToValLatency,
174
- };
175
-
176
- logger.debug("Received gossip block", {...logCtx});
177
-
178
- // optimistically add gossip block to the seen cache
179
- // if validation fails, we will NOT forward this gossip block to peers
180
- // - if PARENT_BLOCK_UNKNOWN error, blockInput will then be queued inside BlockInputSync. If the gossip block is really invalid, it will be pruned there
181
- // - if other validator errors, blockInput will stay in the seen cache and will be pruned on finalization
162
+ ): BlockInput {
182
163
  const blockInput = chain.seenBlockInputCache.getByBlock({
183
164
  block: signedBlock,
184
165
  blockRootHex,
@@ -187,8 +168,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
187
168
  peerIdStr,
188
169
  });
189
170
 
190
- // Optimistically seed the payload-envelope cache too, mirroring seenBlockInputCache above.
191
- // This ensures we have PayloadEnvelopeInput, even through "PARENT_BLOCK_UNKNOWN" error
171
+ // Also seed the payload-envelope cache, so we have PayloadEnvelopeInput even through "PARENT_BLOCK_UNKNOWN" error
192
172
  // see https://github.com/ChainSafe/lodestar/issues/9475
193
173
  if (isForkPostGloas(fork)) {
194
174
  chain.seenPayloadEnvelopeInputCache.add({
@@ -202,8 +182,37 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
202
182
  });
203
183
  }
204
184
 
185
+ return blockInput;
186
+ }
187
+
188
+ async function validateBeaconBlock(
189
+ signedBlock: SignedBeaconBlock,
190
+ fork: ForkName,
191
+ peerIdStr: string,
192
+ seenTimestampSec: number
193
+ ): Promise<IBlockInput> {
194
+ const slot = signedBlock.message.slot;
195
+ const forkTypes = config.getForkTypes(slot);
196
+ const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
197
+ const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
198
+ const recvToValLatency = Date.now() / 1000 - seenTimestampSec;
199
+
200
+ const logCtx = {
201
+ slot,
202
+ root: blockRootHex,
203
+ currentSlot: chain.clock.currentSlot,
204
+ peerId: peerIdStr,
205
+ delaySec,
206
+ recvToValLatency,
207
+ };
208
+
209
+ logger.debug("Received gossip block", {...logCtx});
210
+
211
+ chain.seenBlockInputCache.markValidatingBlock(blockRootHex);
212
+
205
213
  try {
206
214
  const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork);
215
+ const blockInput = cacheSignatureVerifiedBlock(signedBlock, blockRootHex, fork, peerIdStr, seenTimestampSec);
207
216
 
208
217
  const blockInputMeta = blockInput.getLogMeta();
209
218
 
@@ -229,54 +238,54 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
229
238
  if (e instanceof BlockGossipError) {
230
239
  logger.debug("Gossip block has error", {slot, root: blockRootHex, code: e.type.code});
231
240
  if (
232
- (e.type.code === BlockErrorCode.PARENT_BLOCK_UNKNOWN ||
233
- e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) &&
234
- blockInput
241
+ e.type.code === BlockErrorCode.PARENT_BLOCK_UNKNOWN ||
242
+ e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN
235
243
  ) {
244
+ // The block is retained for unknown parent sync without full validation. Authenticate it first, this
245
+ // does not need the parent state
246
+ const proposerIndex = signedBlock.message.proposerIndex;
247
+ await verifyBlockProposerSignature(chain, signedBlock, blockRootHex);
248
+ chain.seenBlockProposers.observeBlockRoot(
249
+ slot,
250
+ proposerIndex,
251
+ blockRootHex,
252
+ signedBlockToSignedHeader(config, signedBlock)
253
+ );
254
+
255
+ // Check again after the signature verification so concurrent proposals cannot both be retained
256
+ if (chain.seenBlockProposers.isKnown(slot, proposerIndex)) {
257
+ if (chain.seenBlockProposers.isRepeatProposal(slot, proposerIndex, blockRootHex)) {
258
+ throw new BlockGossipError(GossipAction.IGNORE, {
259
+ code: BlockErrorCode.REPEAT_PROPOSAL,
260
+ proposerIndex,
261
+ root: blockRootHex,
262
+ });
263
+ }
264
+ throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRootHex});
265
+ }
266
+ chain.seenBlockProposers.add(slot, proposerIndex, blockRootHex);
267
+
268
+ const blockInput = cacheSignatureVerifiedBlock(signedBlock, blockRootHex, fork, peerIdStr, seenTimestampSec);
236
269
  chain.emitter.emit(ChainEvent.blockUnknownParent, {
237
270
  blockInput,
238
271
  peer: peerIdStr,
239
272
  source: BlockInputSource.gossip,
240
273
  });
241
- // throw error (don't prune the blockInput)
242
274
  throw e;
243
275
  }
244
276
 
245
- // IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated.
246
- // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT
247
- // (provably invalid), unexpected errors and repeat proposals that are not imported prune.
248
- if (e.action === GossipAction.IGNORE) {
249
- // Only a signature-verified sibling is imported by the beacon_block handler, any other repeat proposal
250
- // is dropped from the caches and re-downloaded by sync if it ever becomes relevant. Its signature was not
251
- // verified, so only its own entry is removed, never its claimed ancestors
252
- if (
253
- e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
254
- !chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex)
255
- ) {
256
- chain.seenBlockInputCache.remove(blockRootHex);
257
- if (isForkPostGloas(fork)) {
258
- chain.seenPayloadEnvelopeInputCache.remove(blockRootHex);
259
- }
260
- }
261
- throw e;
277
+ if (e.action === GossipAction.REJECT) {
278
+ chain.persistInvalidSszValue(
279
+ forkTypes.SignedBeaconBlock,
280
+ signedBlock,
281
+ `gossip_reject_slot_${slot}`,
282
+ blockRootHex
283
+ );
262
284
  }
263
-
264
- chain.persistInvalidSszValue(
265
- forkTypes.SignedBeaconBlock,
266
- signedBlock,
267
- `gossip_reject_slot_${slot}`,
268
- blockRootHex
269
- );
270
- }
271
-
272
- // REJECT or unexpected (non-BlockGossipError) error: drop the optimistically-added entries from
273
- // both caches, keeping them consistent. The block may carry any parent root, so only its own entry is removed
274
- chain.seenBlockInputCache.remove(blockRootHex);
275
- if (isForkPostGloas(fork)) {
276
- chain.seenPayloadEnvelopeInputCache.remove(blockRootHex);
277
285
  }
278
286
  throw e;
279
287
  } finally {
288
+ chain.seenBlockInputCache.unmarkValidatingBlock(blockRootHex);
280
289
  // The block received from the network may have established an equivocation, either by conflicting
281
290
  // with a previously observed block root (REPEAT_PROPOSAL) or with a root observed during validation
282
291
  const proposerIndex = signedBlock.message.proposerIndex;
@@ -752,18 +761,23 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
752
761
  e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
753
762
  // Only a signature-verified sibling recorded in the seen cache is imported. The cache holds at most two
754
763
  // roots per proposer and slot, which bounds full imports over gossip to one alternate
755
- chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root)
764
+ chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) &&
765
+ // A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant
766
+ chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null
756
767
  ) {
757
- // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
758
- const blockInput = chain.seenBlockInputCache.get(e.type.root);
759
- if (blockInput) {
760
- // We're returning IGNORE (thrown below), so this block is not forwarded to peers. Unlike the
761
- // happy path there is no rush to forward, so we don't need to wrap in callInNextEventLoop.
762
- chain.serializedCache.set(signedBlock, serializedData);
763
- // this is technically not a valid gossip block but gossip validation is a cheap subset of checks
764
- // this runs the full state transition, so importing an equivocating-but-valid block here is safe.
765
- handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
766
- }
768
+ const blockInput = cacheSignatureVerifiedBlock(
769
+ signedBlock,
770
+ e.type.root,
771
+ topic.boundary.fork,
772
+ peerIdStr,
773
+ seenTimestampSec
774
+ );
775
+ // We're returning IGNORE (thrown below), so this block is not forwarded to peers. Unlike the
776
+ // happy path there is no rush to forward, so we don't need to wrap in callInNextEventLoop.
777
+ chain.serializedCache.set(signedBlock, serializedData);
778
+ // this is technically not a valid gossip block but gossip validation is a cheap subset of checks
779
+ // this runs the full state transition, so importing an equivocating-but-valid block here is safe.
780
+ handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
767
781
  }
768
782
  // rethrow so gossipValidatorFn maps IGNORE -> TopicValidatorResult.Ignore (message not forwarded)
769
783
  throw e;
@@ -1,6 +1,12 @@
1
1
  import {setMaxListeners} from "node:events";
2
2
  import {PrivateKey} from "@libp2p/interface";
3
3
  import {Registry} from "prom-client";
4
+ import {
5
+ init as initNativeMetrics,
6
+ registerLocalValidator as registerNativeLocalValidator,
7
+ scrapeMetrics as scrapeNativeMetrics,
8
+ unregisterLocalValidator as unregisterNativeLocalValidator,
9
+ } from "@chainsafe/lodestar-z/metrics";
4
10
  import {type PubkeyCache} from "@chainsafe/lodestar-z/pubkeys";
5
11
  import {hasher} from "@chainsafe/persistent-merkle-tree";
6
12
  import {BeaconApiMethods} from "@lodestar/api/beacon/server";
@@ -8,6 +14,7 @@ import {BeaconConfig} from "@lodestar/config";
8
14
  import type {LoggerNode} from "@lodestar/logger/node";
9
15
  import {ZERO_HASH_HEX} from "@lodestar/params";
10
16
  import {IBeaconStateView, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
17
+ import {Slot} from "@lodestar/types";
11
18
  import {sleep, toRootHex} from "@lodestar/utils";
12
19
  import {ProcessShutdownCallback} from "@lodestar/validator";
13
20
  import {BeaconRestApiServer, getApi} from "../api/index.js";
@@ -55,6 +62,7 @@ export type BeaconNodeInitModules = {
55
62
  peerStoreDir?: string;
56
63
  anchorState: IBeaconStateView;
57
64
  isAnchorStateFinalized: boolean;
65
+ earliestAvailableSlot: Slot;
58
66
  metricsRegistries?: Registry[];
59
67
  };
60
68
 
@@ -153,6 +161,7 @@ export class BeaconNode {
153
161
  peerStoreDir,
154
162
  anchorState,
155
163
  isAnchorStateFinalized,
164
+ earliestAvailableSlot,
156
165
  metricsRegistries = [],
157
166
  }: BeaconNodeInitModules): Promise<T> {
158
167
  if (hasher.name !== "hashtree") {
@@ -171,7 +180,13 @@ export class BeaconNode {
171
180
  // monitoring relies on metrics data
172
181
  opts.monitoring.endpoint
173
182
  ) {
174
- metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries);
183
+ if (opts.chain.nativeStateTransition) {
184
+ initNativeMetrics();
185
+ }
186
+
187
+ metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries, {
188
+ includeStateTransitionMetrics: !opts.chain.nativeStateTransition,
189
+ });
175
190
  initBeaconMetrics(metrics, anchorState);
176
191
  // Since the db is instantiated before this, metrics must be injected manually afterwards
177
192
  db.setMetrics(metrics.db, metrics.flatFileStore);
@@ -185,7 +200,13 @@ export class BeaconNode {
185
200
  config,
186
201
  anchorState.genesisTime,
187
202
  logger.child({module: LoggerModule.vmon}),
188
- opts.validatorMonitor
203
+ opts.validatorMonitor,
204
+ opts.chain.nativeStateTransition
205
+ ? {
206
+ registerLocalValidator: registerNativeLocalValidator,
207
+ unregisterLocalValidator: unregisterNativeLocalValidator,
208
+ }
209
+ : null
189
210
  )
190
211
  : null;
191
212
 
@@ -248,6 +269,7 @@ export class BeaconNode {
248
269
  validatorMonitor,
249
270
  anchorState,
250
271
  isAnchorStateFinalized,
272
+ earliestAvailableSlot,
251
273
  executionEngine: initializeExecutionEngine(executionEngineOpts, {
252
274
  metrics,
253
275
  signal,
@@ -303,7 +325,17 @@ export class BeaconNode {
303
325
  const metricsServer = opts.metrics.enabled
304
326
  ? await getHttpMetricsServer(opts.metrics, {
305
327
  register: (metrics as Metrics).register,
306
- getOtherMetrics: async () => Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]),
328
+ getOtherMetrics: async () => {
329
+ const otherMetrics = await Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]);
330
+ if (opts.chain.nativeStateTransition) {
331
+ try {
332
+ otherMetrics.push(scrapeNativeMetrics());
333
+ } catch (e) {
334
+ logger.warn("Failed to scrape native state-transition metrics", {}, e as Error);
335
+ }
336
+ }
337
+ return otherMetrics;
338
+ },
307
339
  logger: logger.child({module: LoggerModule.metrics}),
308
340
  })
309
341
  : null;
@@ -224,6 +224,7 @@ export class Batch {
224
224
  return {
225
225
  blockRoot: parentBlockRoot,
226
226
  blockRootHex: toRootHex(parentBlockRoot),
227
+ blockHash: this.latestBid.blockHash,
227
228
  kzgCommitments: this.latestBid.blobKzgCommitments,
228
229
  };
229
230
  }
@@ -773,6 +774,7 @@ export class Batch {
773
774
  // remove any downloaded blocks and re-attempt
774
775
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
775
776
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
777
+ this.resetRequests();
776
778
  }
777
779
 
778
780
  private onProcessingError(attempt: FailedAttempt): void {
@@ -784,6 +786,16 @@ export class Batch {
784
786
  // remove any downloaded blocks and re-attempt
785
787
  // TODO(fulu): need to remove the bad blocks from the SeenBlockInputCache
786
788
  this.state = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
789
+ this.resetRequests();
790
+ }
791
+
792
+ /**
793
+ * After a partial download `requests` only cover the data still missing from the downloaded blocks,
794
+ * e.g. a single column. Once those blocks are dropped, request the whole batch again, otherwise
795
+ * every peer fails the retry for lack of blocks to validate the data against.
796
+ */
797
+ private resetRequests(): void {
798
+ this.requests = this.getRequests([]);
787
799
  }
788
800
 
789
801
  /** Helper to construct typed BatchError. Stack traces are correct as the error is thrown above */
@@ -225,6 +225,9 @@ export class SyncChain {
225
225
  Math.floor((localFinalizedEpoch - this.lastEpochWithProcessBlocks) / EPOCHS_PER_BATCH) * EPOCHS_PER_BATCH;
226
226
  this.advanceChain(lastEpochWithProcessBlocksAligned);
227
227
 
228
+ // stopSyncing() cleared the retry timer, but peers can still be in rate limit backoff
229
+ this.scheduleRateLimitBackoffRetry();
230
+
228
231
  // Potentially download new batches and process pending
229
232
  this.triggerBatchDownloader();
230
233
  this.triggerBatchProcessor();
@@ -417,8 +420,10 @@ export class SyncChain {
417
420
  this.rateLimitBackoffTimeout = setTimeout(
418
421
  () => {
419
422
  this.rateLimitBackoffTimeout = undefined;
420
- this.triggerBatchDownloader();
423
+ // Must run before the downloader, it drops expired peers without triggering a download. If it ran
424
+ // after, a backoff expiring in between would leave the peer idle with no retry scheduled.
421
425
  this.scheduleRateLimitBackoffRetry();
426
+ this.triggerBatchDownloader();
422
427
  },
423
428
  Math.max(0, retryAt - now)
424
429
  );
@@ -581,6 +586,7 @@ export class SyncChain {
581
586
  case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
582
587
  case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
583
588
  case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
589
+ case DownloadByRangeErrorCode.INVALID_ENVELOPE_BLOCK_HASH:
584
590
  case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
585
591
  case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
586
592
  case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
@@ -73,7 +73,11 @@ type AdvancePendingBlockResult =
73
73
  | "removed";
74
74
 
75
75
  class UnknownBlockRateLimitedError extends Error {
76
- constructor(message: string) {
76
+ constructor(
77
+ message: string,
78
+ /** Earliest backoff expiry seen when the download was deferred, it is retried once that has passed */
79
+ readonly retryAt: number
80
+ ) {
77
81
  super(message);
78
82
  this.name = "UnknownBlockRateLimitedError";
79
83
  }
@@ -849,17 +853,20 @@ export class BlockInputSync {
849
853
  }
850
854
  };
851
855
 
852
- private scheduleRateLimitBackoffRetry(): void {
856
+ /**
857
+ * Returns false if there is no backoff left to wait for, in which case no retry is scheduled.
858
+ */
859
+ private scheduleRateLimitBackoffRetry(): boolean {
853
860
  this.clearRateLimitBackoffTimer();
854
861
 
855
862
  if (!this.subscribedToNetworkEvents || (this.pendingBlocks.size === 0 && this.pendingPayloads.size === 0)) {
856
- return;
863
+ return false;
857
864
  }
858
865
 
859
866
  const now = Date.now();
860
867
  const retryAt = this.peerBalancer.getNextRateLimitRetryAt();
861
868
  if (retryAt === null) {
862
- return;
869
+ return false;
863
870
  }
864
871
 
865
872
  this.rateLimitBackoffTimeout = setTimeout(
@@ -870,6 +877,7 @@ export class BlockInputSync {
870
877
  },
871
878
  Math.max(0, retryAt - now)
872
879
  );
880
+ return true;
873
881
  }
874
882
 
875
883
  private clearRateLimitBackoffTimer(): void {
@@ -1081,7 +1089,11 @@ export class BlockInputSync {
1081
1089
  pendingBlock.status = PendingBlockInputStatus.pending;
1082
1090
  }
1083
1091
  this.logger.debug("Deferring unknown block download due to peer rate limit", logCtx, res.err);
1084
- this.scheduleRateLimitBackoffRetry();
1092
+ if (!this.scheduleRateLimitBackoffRetry() || res.err.retryAt <= Date.now()) {
1093
+ // The backoff this download waited for expired in the meantime, a timer is only armed if another
1094
+ // peer is still in backoff and would retry it later than needed
1095
+ this.triggerUnknownBlockSearch();
1096
+ }
1085
1097
  return;
1086
1098
  }
1087
1099
 
@@ -1364,6 +1376,14 @@ export class BlockInputSync {
1364
1376
  if (!isPendingPayloadEnvelope(payload)) {
1365
1377
  payload.status = PendingPayloadInputStatus.pending;
1366
1378
  }
1379
+ if (
1380
+ res.err instanceof UnknownBlockRateLimitedError &&
1381
+ (!this.scheduleRateLimitBackoffRetry() || res.err.retryAt <= Date.now())
1382
+ ) {
1383
+ // The backoff this download waited for expired in the meantime, a timer is only armed if another
1384
+ // peer is still in backoff and would retry it later than needed
1385
+ this.triggerUnknownBlockSearch();
1386
+ }
1367
1387
  }
1368
1388
 
1369
1389
  private async processPayload(pendingPayload: PendingPayloadInput): Promise<void> {
@@ -1488,16 +1508,21 @@ export class BlockInputSync {
1488
1508
  const pendingColumns = payloadInput?.hasAllData()
1489
1509
  ? new Set<number>()
1490
1510
  : new Set(payloadInput?.getMissingSampledColumnMeta().missing ?? []);
1511
+ // Same `now` for both calls, a backoff expiring in between would be reported as no peer available
1512
+ const now = Date.now();
1491
1513
  // prefer peers that gossiped this payload root to us (#9923)
1492
1514
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1493
1515
  pendingColumns,
1494
1516
  excludedPeers,
1495
- cacheItem.peerIdStrings
1517
+ cacheItem.peerIdStrings,
1518
+ now
1496
1519
  );
1497
1520
  if (peerMeta === null) {
1498
- if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1521
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers, now);
1522
+ if (retryAt !== null) {
1499
1523
  throw new UnknownBlockRateLimitedError(
1500
- `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1524
+ `Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`,
1525
+ retryAt
1501
1526
  );
1502
1527
  }
1503
1528
 
@@ -1636,9 +1661,11 @@ export class BlockInputSync {
1636
1661
  );
1637
1662
  this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureMaxAttempts}, i - 1);
1638
1663
 
1639
- if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1664
+ const retryAt = deferredByRateLimit ? this.peerBalancer.getNextRateLimitRetryAt() : null;
1665
+ if (retryAt !== null) {
1640
1666
  throw new UnknownBlockRateLimitedError(
1641
- `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
1667
+ `Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`,
1668
+ retryAt
1642
1669
  );
1643
1670
  }
1644
1671
  throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
@@ -1730,16 +1757,21 @@ export class BlockInputSync {
1730
1757
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
1731
1758
  ? new Set(cacheItem.blockInput.getMissingSampledColumnMeta().missing)
1732
1759
  : defaultPendingColumns;
1760
+ // Same `now` for both calls, a backoff expiring in between would be reported as no peer available
1761
+ const now = Date.now();
1733
1762
  // prefer peers that gossiped this block root to us (#9923)
1734
1763
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(
1735
1764
  pendingColumns,
1736
1765
  excludedPeers,
1737
- cacheItem.peerIdStrings
1766
+ cacheItem.peerIdStrings,
1767
+ now
1738
1768
  );
1739
1769
  if (peerMeta === null) {
1740
- if (this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers) !== null) {
1770
+ const retryAt = this.peerBalancer.getNextRateLimitRetryAt(pendingColumns, excludedPeers, now);
1771
+ if (retryAt !== null) {
1741
1772
  throw new UnknownBlockRateLimitedError(
1742
- `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`
1773
+ `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: peers with needed columns are rate-limited`,
1774
+ retryAt
1743
1775
  );
1744
1776
  }
1745
1777
 
@@ -1845,8 +1877,9 @@ export class BlockInputSync {
1845
1877
 
1846
1878
  const message = `Error fetching BlockInput with slot=${slot} root=${rootHex} after ${i - 1} attempts.`;
1847
1879
 
1848
- if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
1849
- throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`);
1880
+ const retryAt = deferredByRateLimit ? this.peerBalancer.getNextRateLimitRetryAt() : null;
1881
+ if (retryAt !== null) {
1882
+ throw new UnknownBlockRateLimitedError(`${message} Peers are rate-limited.`, retryAt);
1850
1883
  }
1851
1884
 
1852
1885
  if (!isPendingBlockInput(cacheItem)) {
@@ -2037,8 +2070,11 @@ export class UnknownBlockPeerBalancer {
2037
2070
  this.rateLimitedUntilByPeer.set(peerId, rateLimitedUntilMs);
2038
2071
  }
2039
2072
 
2040
- getNextRateLimitRetryAt(pendingColumns?: Set<number>, excludedPeers?: Set<PeerIdStr>): number | null {
2041
- const now = Date.now();
2073
+ getNextRateLimitRetryAt(
2074
+ pendingColumns?: Set<number>,
2075
+ excludedPeers?: Set<PeerIdStr>,
2076
+ now = Date.now()
2077
+ ): number | null {
2042
2078
  let retryAt: number | null = null;
2043
2079
 
2044
2080
  for (const [peerId, rateLimitedUntil] of this.rateLimitedUntilByPeer.entries()) {
@@ -2077,9 +2113,10 @@ export class UnknownBlockPeerBalancer {
2077
2113
  bestPeerForPendingColumns(
2078
2114
  pendingColumns: Set<number>,
2079
2115
  excludedPeers: Set<PeerIdStr>,
2080
- preferredPeers?: Set<PeerIdStr>
2116
+ preferredPeers?: Set<PeerIdStr>,
2117
+ now = Date.now()
2081
2118
  ): PeerSyncMeta | null {
2082
- const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers);
2119
+ const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers, now);
2083
2120
  if (eligiblePeers.length === 0) {
2084
2121
  return null;
2085
2122
  }
@@ -2120,8 +2157,7 @@ export class UnknownBlockPeerBalancer {
2120
2157
  return totalActiveRequests;
2121
2158
  }
2122
2159
 
2123
- private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
2124
- const now = Date.now();
2160
+ private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>, now: number): PeerIdStr[] {
2125
2161
  let maxColumnCount = 0;
2126
2162
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
2127
2163
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {