@lodestar/beacon-node 1.47.0-dev.4546ac1229 → 1.47.0-dev.56250f4ab6

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 (171) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +39 -11
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +13 -1
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/events/index.d.ts.map +1 -1
  8. package/lib/api/impl/events/index.js +2 -0
  9. package/lib/api/impl/events/index.js.map +1 -1
  10. package/lib/api/impl/lodestar/index.js +1 -1
  11. package/lib/api/impl/lodestar/index.js.map +1 -1
  12. package/lib/api/impl/validator/index.d.ts.map +1 -1
  13. package/lib/api/impl/validator/index.js +232 -49
  14. package/lib/api/impl/validator/index.js.map +1 -1
  15. package/lib/api/impl/validator/utils.js +1 -1
  16. package/lib/api/impl/validator/utils.js.map +1 -1
  17. package/lib/api/rest/base.d.ts.map +1 -1
  18. package/lib/api/rest/base.js +7 -0
  19. package/lib/api/rest/base.js.map +1 -1
  20. package/lib/chain/archiveStore/archiveStore.js +1 -1
  21. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  22. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +2 -1
  23. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  24. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +6 -2
  25. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  26. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +14 -5
  27. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  28. package/lib/chain/blocks/payloadEnvelopeInput/types.js +5 -0
  29. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -1
  30. package/lib/chain/chain.d.ts +6 -2
  31. package/lib/chain/chain.d.ts.map +1 -1
  32. package/lib/chain/chain.js +14 -3
  33. package/lib/chain/chain.js.map +1 -1
  34. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  35. package/lib/chain/forkChoice/index.js +4 -0
  36. package/lib/chain/forkChoice/index.js.map +1 -1
  37. package/lib/chain/interface.d.ts +4 -1
  38. package/lib/chain/interface.d.ts.map +1 -1
  39. package/lib/chain/interface.js.map +1 -1
  40. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts +16 -0
  41. package/lib/chain/opPools/deferredVoluntaryExitPool.d.ts.map +1 -0
  42. package/lib/chain/opPools/deferredVoluntaryExitPool.js +57 -0
  43. package/lib/chain/opPools/deferredVoluntaryExitPool.js.map +1 -0
  44. package/lib/chain/opPools/executionPayloadBidPool.d.ts +7 -2
  45. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  46. package/lib/chain/opPools/executionPayloadBidPool.js +4 -4
  47. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  48. package/lib/chain/opPools/index.d.ts +2 -1
  49. package/lib/chain/opPools/index.d.ts.map +1 -1
  50. package/lib/chain/opPools/index.js +1 -0
  51. package/lib/chain/opPools/index.js.map +1 -1
  52. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  53. package/lib/chain/prepareNextSlot.js +23 -5
  54. package/lib/chain/prepareNextSlot.js.map +1 -1
  55. package/lib/chain/produceBlock/produceBlockBody.js +1 -1
  56. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  57. package/lib/chain/regen/interface.d.ts +4 -2
  58. package/lib/chain/regen/interface.d.ts.map +1 -1
  59. package/lib/chain/regen/interface.js +2 -0
  60. package/lib/chain/regen/interface.js.map +1 -1
  61. package/lib/chain/regen/queued.d.ts +2 -2
  62. package/lib/chain/regen/queued.d.ts.map +1 -1
  63. package/lib/chain/regen/queued.js +5 -5
  64. package/lib/chain/regen/queued.js.map +1 -1
  65. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +28 -1
  66. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  67. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +121 -13
  68. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  69. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +5 -1
  70. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  71. package/lib/chain/stateCache/fifoBlockStateCache.js +6 -0
  72. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  73. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +47 -5
  74. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  75. package/lib/chain/stateCache/persistentCheckpointsCache.js +117 -7
  76. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  77. package/lib/chain/stateCache/types.d.ts +3 -3
  78. package/lib/chain/stateCache/types.d.ts.map +1 -1
  79. package/lib/chain/validation/executionPayloadBid.js +2 -2
  80. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  81. package/lib/chain/validation/voluntaryExit.d.ts +9 -1
  82. package/lib/chain/validation/voluntaryExit.d.ts.map +1 -1
  83. package/lib/chain/validation/voluntaryExit.js +46 -1
  84. package/lib/chain/validation/voluntaryExit.js.map +1 -1
  85. package/lib/execution/builder/apiClient.d.ts +64 -0
  86. package/lib/execution/builder/apiClient.d.ts.map +1 -0
  87. package/lib/execution/builder/apiClient.js +184 -0
  88. package/lib/execution/builder/apiClient.js.map +1 -0
  89. package/lib/execution/builder/validateBid.d.ts +22 -0
  90. package/lib/execution/builder/validateBid.d.ts.map +1 -0
  91. package/lib/execution/builder/validateBid.js +111 -0
  92. package/lib/execution/builder/validateBid.js.map +1 -0
  93. package/lib/metrics/metrics/lodestar.d.ts +39 -5
  94. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  95. package/lib/metrics/metrics/lodestar.js +63 -2
  96. package/lib/metrics/metrics/lodestar.js.map +1 -1
  97. package/lib/network/processor/constants.d.ts +7 -0
  98. package/lib/network/processor/constants.d.ts.map +1 -0
  99. package/lib/network/processor/constants.js +7 -0
  100. package/lib/network/processor/constants.js.map +1 -0
  101. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  102. package/lib/network/processor/gossipHandlers.js +6 -2
  103. package/lib/network/processor/gossipHandlers.js.map +1 -1
  104. package/lib/network/processor/gossipQueues/indexed.d.ts.map +1 -1
  105. package/lib/network/processor/gossipQueues/indexed.js +0 -1
  106. package/lib/network/processor/gossipQueues/indexed.js.map +1 -1
  107. package/lib/network/processor/index.d.ts +3 -0
  108. package/lib/network/processor/index.d.ts.map +1 -1
  109. package/lib/network/processor/index.js +41 -17
  110. package/lib/network/processor/index.js.map +1 -1
  111. package/lib/node/deferredVoluntaryExitPublisher.d.ts +15 -0
  112. package/lib/node/deferredVoluntaryExitPublisher.d.ts.map +1 -0
  113. package/lib/node/deferredVoluntaryExitPublisher.js +38 -0
  114. package/lib/node/deferredVoluntaryExitPublisher.js.map +1 -0
  115. package/lib/node/nodejs.d.ts.map +1 -1
  116. package/lib/node/nodejs.js +7 -0
  117. package/lib/node/nodejs.js.map +1 -1
  118. package/lib/sync/types.d.ts +32 -0
  119. package/lib/sync/types.d.ts.map +1 -1
  120. package/lib/sync/types.js +33 -0
  121. package/lib/sync/types.js.map +1 -1
  122. package/lib/sync/unknownBlock.d.ts +6 -2
  123. package/lib/sync/unknownBlock.d.ts.map +1 -1
  124. package/lib/sync/unknownBlock.js +143 -68
  125. package/lib/sync/unknownBlock.js.map +1 -1
  126. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  127. package/lib/sync/utils/downloadByRange.js +16 -11
  128. package/lib/sync/utils/downloadByRange.js.map +1 -1
  129. package/lib/sync/utils/downloadByRoot.d.ts.map +1 -1
  130. package/lib/sync/utils/downloadByRoot.js +3 -1
  131. package/lib/sync/utils/downloadByRoot.js.map +1 -1
  132. package/package.json +34 -34
  133. package/src/api/impl/beacon/blocks/index.ts +46 -14
  134. package/src/api/impl/beacon/pool/index.ts +15 -1
  135. package/src/api/impl/events/index.ts +3 -1
  136. package/src/api/impl/lodestar/index.ts +1 -1
  137. package/src/api/impl/validator/index.ts +285 -49
  138. package/src/api/impl/validator/utils.ts +1 -1
  139. package/src/api/rest/base.ts +7 -0
  140. package/src/chain/archiveStore/archiveStore.ts +1 -1
  141. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +8 -2
  142. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -4
  143. package/src/chain/chain.ts +16 -1
  144. package/src/chain/forkChoice/index.ts +4 -0
  145. package/src/chain/interface.ts +4 -0
  146. package/src/chain/opPools/deferredVoluntaryExitPool.ts +65 -0
  147. package/src/chain/opPools/executionPayloadBidPool.ts +13 -7
  148. package/src/chain/opPools/index.ts +2 -1
  149. package/src/chain/prepareNextSlot.ts +23 -5
  150. package/src/chain/produceBlock/produceBlockBody.ts +1 -1
  151. package/src/chain/regen/interface.ts +4 -2
  152. package/src/chain/regen/queued.ts +5 -5
  153. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +143 -14
  154. package/src/chain/stateCache/fifoBlockStateCache.ts +8 -1
  155. package/src/chain/stateCache/persistentCheckpointsCache.ts +133 -8
  156. package/src/chain/stateCache/types.ts +3 -3
  157. package/src/chain/validation/executionPayloadBid.ts +2 -2
  158. package/src/chain/validation/voluntaryExit.ts +59 -2
  159. package/src/execution/builder/apiClient.ts +244 -0
  160. package/src/execution/builder/validateBid.ts +166 -0
  161. package/src/metrics/metrics/lodestar.ts +72 -7
  162. package/src/network/processor/constants.ts +6 -0
  163. package/src/network/processor/gossipHandlers.ts +6 -2
  164. package/src/network/processor/gossipQueues/indexed.ts +0 -1
  165. package/src/network/processor/index.ts +53 -21
  166. package/src/node/deferredVoluntaryExitPublisher.ts +54 -0
  167. package/src/node/nodejs.ts +8 -0
  168. package/src/sync/types.ts +33 -0
  169. package/src/sync/unknownBlock.ts +167 -73
  170. package/src/sync/utils/downloadByRange.ts +21 -12
  171. package/src/sync/utils/downloadByRoot.ts +3 -1
@@ -0,0 +1,166 @@
1
+ import {routes} from "@lodestar/api";
2
+ import {ProtoBlock} from "@lodestar/fork-choice";
3
+ import {MAX_EXECUTION_PAYMENT, PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
4
+ import {
5
+ computeEpochAtSlot,
6
+ createSingleSignatureSetFromComponents,
7
+ getExecutionPayloadBidSigningRoot,
8
+ isActiveBuilder,
9
+ isGasLimitTargetCompatible,
10
+ isStatePostGloas,
11
+ } from "@lodestar/state-transition";
12
+ import {RootHex, Slot, gloas} from "@lodestar/types";
13
+ import {bigIntMin, byteArrayEquals, prettyGweiToEth, toHex, toRootHex} from "@lodestar/utils";
14
+ import {IBeaconChain} from "../../chain/index.js";
15
+ import {RegenCaller} from "../../chain/regen/index.js";
16
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
17
+
18
+ /** Return the counted bid payment in Gwei, saturated at uint64 max. */
19
+ export function getBuilderBidTotalGwei(bid: gloas.ExecutionPayloadBid, maxExecutionPayment: bigint): bigint {
20
+ return bigIntMin(MAX_EXECUTION_PAYMENT, BigInt(bid.value) + bigIntMin(bid.executionPayment, maxExecutionPayment));
21
+ }
22
+
23
+ /**
24
+ * Validate a bid received from a builder over the builder API in response to a bid request
25
+ * made during block production. Unlike gossip validation, the bid must match the requested
26
+ * slot and parent exactly, may carry a non-zero `executionPayment` which is counted at most at
27
+ * the entry's `maxExecutionPayment`, and is not subject to gossip anti-spam rules.
28
+ *
29
+ * Throws with a description of the failure, the caller drops the bid.
30
+ */
31
+ export async function validateBuilderApiExecutionPayloadBid(
32
+ chain: IBeaconChain,
33
+ signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid,
34
+ request: {
35
+ slot: Slot;
36
+ parentBlock: ProtoBlock;
37
+ parentBlockHash: RootHex;
38
+ parentBlockRoot: RootHex;
39
+ entry: routes.validator.BuilderEntry;
40
+ }
41
+ ): Promise<void> {
42
+ const bid = signedExecutionPayloadBid.message;
43
+ const {slot, parentBlock, parentBlockHash, parentBlockRoot, entry} = request;
44
+
45
+ if (bid.slot !== slot) {
46
+ throw Error(`Bid slot=${bid.slot} does not match requested slot=${slot}`);
47
+ }
48
+
49
+ const bidParentBlockHash = toRootHex(bid.parentBlockHash);
50
+ const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
51
+ if (bidParentBlockHash !== parentBlockHash || bidParentBlockRoot !== parentBlockRoot) {
52
+ throw Error(
53
+ `Bid parent parentBlockHash=${bidParentBlockHash} parentBlockRoot=${bidParentBlockRoot} does not match ` +
54
+ `requested parentBlockHash=${parentBlockHash} parentBlockRoot=${parentBlockRoot}`
55
+ );
56
+ }
57
+
58
+ const totalPayment = getBuilderBidTotalGwei(bid, entry.maxExecutionPayment);
59
+ if (totalPayment < entry.minBid) {
60
+ throw Error(
61
+ `Bid total payment=${prettyGweiToEth(totalPayment)} ` +
62
+ `(value=${prettyGweiToEth(bid.value)} ` +
63
+ `executionPayment=${prettyGweiToEth(bid.executionPayment)}) ` +
64
+ `is below minBid=${prettyGweiToEth(entry.minBid)}`
65
+ );
66
+ }
67
+
68
+ const state = await chain.regen
69
+ .getBlockSlotState(parentBlock, slot, {dontTransferCache: true}, RegenCaller.validateGossipExecutionPayloadBid)
70
+ .catch((e: Error) => {
71
+ throw Error(`Unable to regenerate state to validate bid: ${e.message}`);
72
+ });
73
+
74
+ if (!isStatePostGloas(state)) {
75
+ throw Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
76
+ }
77
+
78
+ if (bid.builderIndex >= state.getBuildersLength()) {
79
+ throw Error(`Bid builderIndex=${bid.builderIndex} is out of bounds`);
80
+ }
81
+
82
+ const builder = state.getBuilder(bid.builderIndex);
83
+ if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
84
+ throw Error(`Bid builderIndex=${bid.builderIndex} is not an active builder`);
85
+ }
86
+
87
+ if (builder.version !== PAYLOAD_BUILDER_VERSION) {
88
+ throw Error(`Invalid builder version=${builder.version} expected=${PAYLOAD_BUILDER_VERSION}`);
89
+ }
90
+
91
+ // A bid not signed by one of the builder pubkeys the entry accepts bids from must not be accepted
92
+ if (
93
+ entry.builderPubkeys.length > 0 &&
94
+ !entry.builderPubkeys.some((pubkey) => byteArrayEquals(pubkey, builder.pubkey))
95
+ ) {
96
+ throw Error(
97
+ `Bid builder pubkey=${toHex(builder.pubkey)} is not in the entry's ` +
98
+ `builderPubkeys=${entry.builderPubkeys.map(toHex).join(",")}`
99
+ );
100
+ }
101
+
102
+ const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
103
+ const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
104
+ if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
105
+ throw Error(`Bid has too many KZG commitments len=${blobKzgCommitmentsLen} limit=${maxBlobsPerBlock}`);
106
+ }
107
+
108
+ // The coverage check only applies to the staked collateral payment, a pure execution
109
+ // layer payment bid has nothing to cover on-chain
110
+ if (bid.value > 0 && !state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
111
+ throw Error(
112
+ `Builder cannot cover bid value=${prettyGweiToEth(bid.value)} ` + `balance=${prettyGweiToEth(builder.balance)}`
113
+ );
114
+ }
115
+
116
+ const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
117
+ if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
118
+ throw Error(`Invalid bid prevRandao=${toHex(bid.prevRandao)} expected=${toHex(randaoMix)}`);
119
+ }
120
+
121
+ // The builder must honor the proposer preferences it learned over gossip
122
+ const bidEpoch = computeEpochAtSlot(bid.slot);
123
+ const dependentRootHex = (() => {
124
+ try {
125
+ return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
126
+ } catch {
127
+ return null;
128
+ }
129
+ })();
130
+ if (dependentRootHex === null) {
131
+ throw Error(`Unable to resolve proposer preferences dependent root for bid slot=${bid.slot}`);
132
+ }
133
+ const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
134
+ if (proposerPreferences === null) {
135
+ throw Error(`No proposer preferences found for bid slot=${bid.slot} dependentRoot=${dependentRootHex}`);
136
+ }
137
+ if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
138
+ throw Error(
139
+ `Bid feeRecipient=${toHex(bid.feeRecipient)} does not match ` +
140
+ `proposer preferences feeRecipient=${toHex(proposerPreferences.message.feeRecipient)}`
141
+ );
142
+ }
143
+
144
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(bidParentBlockRoot, bidParentBlockHash);
145
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
146
+ throw Error(`Unable to resolve parent payload gas limit for bid parentBlockHash=${bidParentBlockHash}`);
147
+ }
148
+ const parentGasLimit = BigInt(parentPayloadVariant.executionPayloadGasLimit);
149
+ const targetGasLimit = proposerPreferences.message.targetGasLimit;
150
+ if (!isGasLimitTargetCompatible(parentGasLimit, bid.gasLimit, targetGasLimit)) {
151
+ throw Error(
152
+ `Bid gasLimit=${bid.gasLimit} is not compatible with ` +
153
+ `parentGasLimit=${parentGasLimit} targetGasLimit=${targetGasLimit}`
154
+ );
155
+ }
156
+
157
+ const signatureSet = createSingleSignatureSetFromComponents(
158
+ builder.pubkey,
159
+ getExecutionPayloadBidSigningRoot(chain.config, bid),
160
+ signedExecutionPayloadBid.signature
161
+ );
162
+
163
+ if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
164
+ throw Error(`Invalid bid signature builderIndex=${bid.builderIndex}`);
165
+ }
166
+ }
@@ -4,7 +4,10 @@ import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
6
  import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
7
- import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
7
+ import {
8
+ PayloadEnvelopeInputPruneReason,
9
+ PayloadEnvelopeInputSource,
10
+ } from "../../chain/blocks/payloadEnvelopeInput/index.js";
8
11
  import {JobQueueItemType} from "../../chain/bls/index.js";
9
12
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
10
13
  import {
@@ -21,7 +24,7 @@ import {ExecutionPayloadStatus} from "../../execution/index.js";
21
24
  import {GossipType} from "../../network/index.js";
22
25
  import {CannotAcceptWorkReason, ReprocessRejectReason} from "../../network/processor/index.js";
23
26
  import {BackfillSyncMethod} from "../../sync/backfill/backfill.js";
24
- import {PendingBlockType} from "../../sync/types.js";
27
+ import {DroppedItemReason, PendingBlockType} from "../../sync/types.js";
25
28
  import {PeerSyncType, RangeSyncType} from "../../sync/utils/remoteSyncType.js";
26
29
  import {AllocSource} from "../../util/bufferPool.js";
27
30
  import {DataColumnReconstructionCode} from "../../util/dataColumns.js";
@@ -101,9 +104,10 @@ export function createLodestarMetrics(
101
104
  help: "Age of the first item of each key in the indexed queues in seconds",
102
105
  buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
103
106
  }),
104
- queueTime: register.histogram({
107
+ queueTime: register.histogram<{topic: GossipType}>({
105
108
  name: "lodestar_gossip_validation_queue_time_seconds",
106
109
  help: "Total time an item stays in queue until it is processed in seconds",
110
+ labelNames: ["topic"],
107
111
  buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
108
112
  }),
109
113
  },
@@ -677,9 +681,15 @@ export function createLodestarMetrics(
677
681
  name: "lodestar_sync_unknown_block_downloaded_blocks_error_total",
678
682
  help: "Total number of downloaded blocks errors in UnknownBlockSync",
679
683
  }),
680
- removedBlocks: register.gauge({
684
+ removedBlocks: register.gauge<{reason: DroppedItemReason}>({
681
685
  name: "lodestar_sync_unknown_block_removed_blocks_total",
682
- help: "Total number of removed bad blocks in UnknownBlockSync",
686
+ help: "Total pending blocks dropped from BlockInputSync without completing, by reason",
687
+ labelNames: ["reason"],
688
+ }),
689
+ removedPayloads: register.gauge<{reason: DroppedItemReason}>({
690
+ name: "lodestar_sync_unknown_block_removed_payloads_total",
691
+ help: "Total pending payloads dropped from BlockInputSync without completing, by reason",
692
+ labelNames: ["reason"],
683
693
  }),
684
694
  elapsedTimeTillReceived: register.histogram({
685
695
  name: "lodestar_sync_unknown_block_elapsed_time_till_received",
@@ -1225,6 +1235,12 @@ export function createLodestarMetrics(
1225
1235
  name: "lodestar_oppool_attester_slashing_pool_size",
1226
1236
  help: "Current size of the AttesterSlashingPool",
1227
1237
  }),
1238
+ deferredVoluntaryExitPool: {
1239
+ size: register.gauge({
1240
+ name: "lodestar_oppool_deferred_voluntary_exit_pool_size",
1241
+ help: "Current size of the DeferredVoluntaryExitPool",
1242
+ }),
1243
+ },
1228
1244
  proposerSlashingPoolSize: register.gauge({
1229
1245
  name: "lodestar_oppool_proposer_slashing_pool_size",
1230
1246
  help: "Current size of the ProposerSlashingPool",
@@ -1429,6 +1445,16 @@ export function createLodestarMetrics(
1429
1445
  help: "Checkpoint state cache size",
1430
1446
  labelNames: ["type"],
1431
1447
  }),
1448
+ persistentTierEpochs: register.gauge<{tier: number}>({
1449
+ name: "lodestar_cp_state_cache_persistent_tier_epochs",
1450
+ help: "Number of epoch keys retained in each on-disk checkpoint state retention tier",
1451
+ labelNames: ["tier"],
1452
+ }),
1453
+ persistentTierStates: register.gauge<{tier: number}>({
1454
+ name: "lodestar_cp_state_cache_persistent_tier_states",
1455
+ help: "Number of persisted checkpoint states retained in each on-disk retention tier",
1456
+ labelNames: ["tier"],
1457
+ }),
1432
1458
  reads: register.avgMinMax({
1433
1459
  name: "lodestar_cp_state_epoch_reads",
1434
1460
  help: "Avg min max of all state cache items total read count",
@@ -1643,9 +1669,15 @@ export function createLodestarMetrics(
1643
1669
  name: "lodestar_seen_payload_envelope_input_cache_serialized_object_refs",
1644
1670
  help: "Number of serialized-cache object refs retained by cached PayloadEnvelopeInputs",
1645
1671
  }),
1646
- created: register.counter({
1672
+ created: register.counter<{source: PayloadEnvelopeInputSource}>({
1647
1673
  name: "lodestar_seen_payload_envelope_input_cache_items_created_total",
1648
- help: "Number of PayloadEnvelopeInputs created",
1674
+ help: "Number of PayloadEnvelopeInputs created by source",
1675
+ labelNames: ["source"],
1676
+ }),
1677
+ pruned: register.counter<{reason: PayloadEnvelopeInputPruneReason}>({
1678
+ name: "lodestar_seen_payload_envelope_input_cache_items_pruned_total",
1679
+ help: "Number of PayloadEnvelopeInputs evicted by reason",
1680
+ labelNames: ["reason"],
1649
1681
  }),
1650
1682
  },
1651
1683
  },
@@ -1755,6 +1787,10 @@ export function createLodestarMetrics(
1755
1787
  name: "lodestar_precompute_next_epoch_transition_waste_total",
1756
1788
  help: "Total number of precomputing next epoch transition wasted",
1757
1789
  }),
1790
+ predictedReorg: register.counter({
1791
+ name: "lodestar_precompute_next_epoch_transition_predicted_reorg_total",
1792
+ help: "Predicted epoch-boundary reorgs where the strong parent was dialed instead of the weak head",
1793
+ }),
1758
1794
  duration: register.histogram({
1759
1795
  name: "lodestar_precompute_next_epoch_transition_duration_seconds",
1760
1796
  help: "Duration of precomputeNextEpochTransition, including epoch transition and hashTreeRoot",
@@ -1994,6 +2030,35 @@ export function createLodestarMetrics(
1994
2030
  }),
1995
2031
  },
1996
2032
 
2033
+ builderApi: {
2034
+ bidRequests: register.counter({
2035
+ name: "lodestar_builder_api_bid_requests_total",
2036
+ help: "Total count of execution payload bid requests sent to external builders",
2037
+ }),
2038
+ bidRequestErrors: register.counter({
2039
+ name: "lodestar_builder_api_bid_request_errors_total",
2040
+ help: "Total count of failed execution payload bid requests to external builders",
2041
+ }),
2042
+ bidsReceived: register.counter({
2043
+ name: "lodestar_builder_api_bids_received_total",
2044
+ help: "Total count of execution payload bids received from external builders",
2045
+ }),
2046
+ bidsDiscarded: register.counter({
2047
+ name: "lodestar_builder_api_bids_discarded_total",
2048
+ help: "Total count of execution payload bids from external builders discarded due to failed validation",
2049
+ }),
2050
+ blockSubmissions: register.counter<{status: "success" | "error"}>({
2051
+ name: "lodestar_builder_api_block_submissions_total",
2052
+ help: "Total count of signed beacon blocks submitted to external builders",
2053
+ labelNames: ["status"],
2054
+ }),
2055
+ preferencesForwarded: register.counter<{status: "success" | "error"}>({
2056
+ name: "lodestar_builder_api_preferences_forwarded_total",
2057
+ help: "Total count of builder preferences forwarded to external builders",
2058
+ labelNames: ["status"],
2059
+ }),
2060
+ },
2061
+
1997
2062
  db: {
1998
2063
  dbReadReq: register.gauge<{bucket: string}>({
1999
2064
  name: "lodestar_db_read_req_total",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Max distinct peers tracked per unknown block/payload root.
3
+ * Normally we have 8 mesh peers per topic, and multi topics may point to the same root.
4
+ * Having 32 peers per root seems enough to search for missing blocks/payloads.
5
+ **/
6
+ export const MAX_PEERS_PER_ROOT = 32;
@@ -196,7 +196,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
196
196
  forkName: fork,
197
197
  sampledColumns: chain.custodyConfig.sampledColumns,
198
198
  custodyColumns: chain.custodyConfig.custodyColumns,
199
- timeCreatedSec: seenTimestampSec,
199
+ seenTimestampSec,
200
+ source: PayloadEnvelopeInputSource.gossip,
200
201
  });
201
202
  }
202
203
 
@@ -640,6 +641,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
640
641
  metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
641
642
 
642
643
  if (isForkPostGloas(blockInput.forkName)) {
644
+ // we should have the payloadInput in the seen cache so no need getOrReload() here
643
645
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
644
646
  // This payloadInput should have been created just after gossip validation
645
647
  if (!payloadInput) {
@@ -1189,6 +1191,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1189
1191
  chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope);
1190
1192
 
1191
1193
  const blockRootHex = toRootHex(envelope.beaconBlockRoot);
1194
+ // a gossip payload cannot be `MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE` slots after the block
1195
+ // otherwise it'll get to UnknownBlockInput flow
1192
1196
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
1193
1197
 
1194
1198
  if (!payloadInput) {
@@ -1302,7 +1306,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1302
1306
 
1303
1307
  // Handle valid payload bid by storing in a bid pool
1304
1308
  try {
1305
- const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid);
1309
+ const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid, Math.floor(elapsedSec * 1000));
1306
1310
  metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome});
1307
1311
  } catch (e) {
1308
1312
  logger.error("Error adding to executionPayloadBid pool", {}, e as Error);
@@ -86,7 +86,6 @@ export class IndexedGossipQueueMinSize<T extends {indexed?: string; queueAddedMs
86
86
  const now = Date.now();
87
87
  // here we mutate item, which is used for gossip validation later
88
88
  item.indexed = key;
89
- item.queueAddedMs = now;
90
89
  let queueItem = this.indexedItems.get(key);
91
90
  if (queueItem == null) {
92
91
  queueItem = {firstSeenMs: now, listItems: new LinkedList<T>()};
@@ -30,6 +30,7 @@ import {
30
30
  GossipValidatorBatchFn,
31
31
  GossipValidatorFn,
32
32
  } from "../gossip/interface.js";
33
+ import {MAX_PEERS_PER_ROOT} from "./constants.js";
33
34
  import {createExtractBlockSlotRootFns} from "./extractSlotRootFns.js";
34
35
  import {GossipHandlerOpts, ValidatorFnsModules, getGossipHandlers} from "./gossipHandlers.js";
35
36
  import {createGossipQueues} from "./gossipQueues/index.js";
@@ -191,8 +192,12 @@ export class NetworkProcessor {
191
192
  // in that case PendingGossipsubMessage needs to be stored in this Map and reprocessed once the payload comes
192
193
  private readonly awaitingMessagesByPayloadBlockRoot: MapDef<RootHex, Set<PendingGossipsubMessage>>;
193
194
  private awaitingPayloadMessageCount = 0;
194
- private unknownBlocksBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
195
- private unknownEnvelopesBySlot = new MapDef<Slot, Set<RootHex>>(() => new Set());
195
+ private unknownBlocksBySlot = new MapDef<Slot, MapDef<RootHex, Set<PeerIdStr>>>(
196
+ () => new MapDef<RootHex, Set<PeerIdStr>>(() => new Set())
197
+ );
198
+ private unknownEnvelopesBySlot = new MapDef<Slot, MapDef<RootHex, Set<PeerIdStr>>>(
199
+ () => new MapDef<RootHex, Set<PeerIdStr>>(() => new Set())
200
+ );
196
201
 
197
202
  constructor(
198
203
  modules: NetworkProcessorModules,
@@ -271,35 +276,53 @@ export class NetworkProcessor {
271
276
  * Search block via `ChainEvent.unknownBlockRoot` event
272
277
  * Slot is the message slot, which is not necessarily the same as the block's slot, but it can be used for a good prune strategy.
273
278
  * In the rare case, if 2 messages on 2 slots search for the same root (for example beacon_attestation) we may emit the same root twice but BlockInputSync should handle it well.
279
+ * We forward every distinct peer that gossiped this root (not just the first) so BlockInputSync can
280
+ * prefer them as by-root download candidates.
274
281
  */
275
282
  searchUnknownBlock({slot, root}: SlotRootHex, source: BlockInputSource, peer?: PeerIdStr): void {
276
- if (
277
- this.chain.seenBlock(root) ||
278
- this.awaitingMessagesByBlockRoot.has(root) ||
279
- this.unknownBlocksBySlot.getOrDefault(slot).has(root)
280
- ) {
283
+ if (this.chain.seenBlock(root)) {
281
284
  return;
282
285
  }
283
- // Search for the unknown block
284
- this.unknownBlocksBySlot.getOrDefault(slot).add(root);
285
- this.chain.emitter.emit(ChainEvent.unknownBlockRoot, {rootHex: root, peer, source});
286
+ const peersForRoot = this.unknownBlocksBySlot.getOrDefault(slot);
287
+ const alreadySearching = peersForRoot.has(root) || this.awaitingMessagesByBlockRoot.has(root);
288
+ const forwardedPeers = peersForRoot.getOrDefault(root);
289
+
290
+ // brand-new search always emits (peer may be undefined)
291
+ let shouldEmit = !alreadySearching;
292
+ if (peer !== undefined && !forwardedPeers.has(peer) && forwardedPeers.size < MAX_PEERS_PER_ROOT) {
293
+ // First time we forward THIS peer for THIS root: emit so BlockInputSync accumulates it.
294
+ forwardedPeers.add(peer);
295
+ shouldEmit = true;
296
+ }
297
+ if (shouldEmit) {
298
+ this.chain.emitter.emit(ChainEvent.unknownBlockRoot, {rootHex: root, peer, source});
299
+ }
286
300
  }
287
301
 
288
302
  /**
289
303
  * Search envelope via `ChainEvent.unknownEnvelopeBlockRoot` event
290
304
  * Slot is the message slot, which is not necessarily the same as the envelope's slot, but it can be used for a good prune strategy.
291
305
  * In the rare case, if 2 messages on 2 slots search for the same root (for example beacon_attestation) we may emit the same root twice but BlockInputSync should handle it well.
306
+ * Same peer-forwarding + dedup behavior as searchUnknownBlock.
292
307
  */
293
308
  searchUnknownEnvelope({slot, root}: SlotRootHex, source: BlockInputSource, peer?: PeerIdStr): void {
294
- if (
295
- this.chain.seenPayloadEnvelope(root) ||
296
- this.awaitingMessagesByPayloadBlockRoot.has(root) ||
297
- this.unknownEnvelopesBySlot.getOrDefault(slot).has(root)
298
- ) {
309
+ if (this.chain.seenPayloadEnvelope(root)) {
299
310
  return;
300
311
  }
301
- this.unknownEnvelopesBySlot.getOrDefault(slot).add(root);
302
- this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, slot, peer, source});
312
+ const peersForRoot = this.unknownEnvelopesBySlot.getOrDefault(slot);
313
+ // capture "already searching" BEFORE getOrDefault(root) creates the entry
314
+ const alreadySearching = peersForRoot.has(root) || this.awaitingMessagesByPayloadBlockRoot.has(root);
315
+ const forwardedPeers = peersForRoot.getOrDefault(root);
316
+
317
+ let shouldEmit = !alreadySearching; // brand-new search always emits (peer may be undefined)
318
+ if (peer !== undefined && !forwardedPeers.has(peer) && forwardedPeers.size < MAX_PEERS_PER_ROOT) {
319
+ // First time we forward THIS peer for THIS root: emit so BlockInputSync accumulates it.
320
+ forwardedPeers.add(peer);
321
+ shouldEmit = true;
322
+ }
323
+ if (shouldEmit) {
324
+ this.chain.emitter.emit(ChainEvent.unknownEnvelopeBlockRoot, {rootHex: root, slot, peer, source});
325
+ }
303
326
  }
304
327
 
305
328
  private onPendingGossipsubMessage = (message: PendingGossipsubMessage): void => {
@@ -537,6 +560,7 @@ export class NetworkProcessor {
537
560
 
538
561
  private pushPendingGossipsubMessageToQueue(message: PendingGossipsubMessage): void {
539
562
  const topicType = message.topic.type;
563
+ message.queueAddedMs = Date.now();
540
564
  const droppedCount = this.gossipQueues[topicType].add(message);
541
565
  if (droppedCount) {
542
566
  // No need to report the dropped job to gossip. It will be eventually pruned from the mcache
@@ -616,7 +640,7 @@ export class NetworkProcessor {
616
640
 
617
641
  for (const [slot, roots] of this.unknownBlocksBySlot) {
618
642
  if (slot > minSlot) continue;
619
- for (const rootHex of roots) {
643
+ for (const rootHex of roots.keys()) {
620
644
  const gossipMessages = this.awaitingMessagesByBlockRoot.get(rootHex);
621
645
  if (gossipMessages !== undefined) {
622
646
  for (const message of gossipMessages) {
@@ -641,7 +665,7 @@ export class NetworkProcessor {
641
665
 
642
666
  for (const [slot, roots] of this.unknownEnvelopesBySlot) {
643
667
  if (slot > minSlot) continue;
644
- for (const rootHex of roots) {
668
+ for (const rootHex of roots.keys()) {
645
669
  const gossipMessages = this.awaitingMessagesByPayloadBlockRoot.get(rootHex);
646
670
  if (gossipMessages !== undefined) {
647
671
  for (const message of gossipMessages) {
@@ -723,12 +747,20 @@ export class NetworkProcessor {
723
747
  for (const msg of messageOrArray) {
724
748
  msg.startProcessUnixSec = nowSec;
725
749
  if (msg.queueAddedMs !== undefined) {
726
- this.metrics?.gossipValidationQueue.queueTime.observe(nowSec - msg.queueAddedMs / 1000);
750
+ this.metrics?.gossipValidationQueue.queueTime.observe(
751
+ {topic: msg.topic.type},
752
+ nowSec - msg.queueAddedMs / 1000
753
+ );
727
754
  }
728
755
  }
729
756
  } else {
730
- // indexed queue is not used here
731
757
  messageOrArray.startProcessUnixSec = nowSec;
758
+ if (messageOrArray.queueAddedMs !== undefined) {
759
+ this.metrics?.gossipValidationQueue.queueTime.observe(
760
+ {topic: messageOrArray.topic.type},
761
+ nowSec - messageOrArray.queueAddedMs / 1000
762
+ );
763
+ }
732
764
  }
733
765
 
734
766
  const acceptanceArr = Array.isArray(messageOrArray)
@@ -0,0 +1,54 @@
1
+ import {routes} from "@lodestar/api";
2
+ import {Logger} from "@lodestar/utils";
3
+ import {IBeaconChain} from "../chain/index.js";
4
+ import {INetwork} from "../network/index.js";
5
+ import {ClockEvent} from "../util/clock.js";
6
+
7
+ /**
8
+ * On every epoch, drain the deferred voluntary exit pool and publish the exits that have become
9
+ * processable. Lives at the node layer rather than inside `BeaconChain` because publishing needs
10
+ * `network`, which the chain does not (and should not) depend on.
11
+ */
12
+ export function startDeferredVoluntaryExitPublisher({
13
+ chain,
14
+ network,
15
+ logger,
16
+ signal,
17
+ }: {
18
+ chain: IBeaconChain;
19
+ network: INetwork;
20
+ logger: Logger;
21
+ signal: AbortSignal;
22
+ }): void {
23
+ const onEpoch = async (): Promise<void> => {
24
+ try {
25
+ // Use the cached head state directly rather than getHeadStateAtCurrentEpoch, which would regen
26
+ // the head forward to the current epoch. At init from a stale db-finalized state that regen can
27
+ // be many epoch transitions. The head state is fine here: this is a best-effort drain that
28
+ // re-runs every epoch, so a just-processable exit is republished next tick.
29
+ const state = chain.getHeadState();
30
+ const exits = chain.deferredVoluntaryExitPool.drainProcessableExits(state);
31
+ for (const exit of exits) {
32
+ try {
33
+ chain.opPool.insertVoluntaryExit(exit);
34
+ chain.emitter.emit(routes.events.EventType.voluntaryExit, exit);
35
+ await network.publishVoluntaryExit(exit);
36
+ logger.info("Voluntary exit successfully published for validator", {
37
+ validatorIndex: exit.message.validatorIndex,
38
+ });
39
+ } catch (e) {
40
+ logger.warn(
41
+ "Failed to publish deferred voluntary exit",
42
+ {validatorIndex: exit.message.validatorIndex},
43
+ e as Error
44
+ );
45
+ }
46
+ }
47
+ } catch (e) {
48
+ logger.warn("Failed to drain deferred voluntary exit pool", {}, e as Error);
49
+ }
50
+ };
51
+
52
+ chain.clock.addListener(ClockEvent.epoch, onEpoch);
53
+ signal.addEventListener("abort", () => chain.clock.removeListener(ClockEvent.epoch, onEpoch), {once: true});
54
+ }
@@ -22,6 +22,7 @@ import {Network, getReqRespHandlers} from "../network/index.js";
22
22
  import {BackfillSync} from "../sync/backfill/index.js";
23
23
  import {BeaconSync, IBeaconSync} from "../sync/index.js";
24
24
  import {Clock} from "../util/clock.js";
25
+ import {startDeferredVoluntaryExitPublisher} from "./deferredVoluntaryExitPublisher.js";
25
26
  import {runNodeNotifier} from "./notifier.js";
26
27
  import {IBeaconNodeOptions} from "./options.js";
27
28
 
@@ -259,6 +260,11 @@ export class BeaconNode {
259
260
  executionBuilder: opts.executionBuilder.enabled
260
261
  ? initializeExecutionBuilder(opts.executionBuilder, config, metrics, logger)
261
262
  : undefined,
263
+ builderApiClientOpts: {
264
+ timeout: opts.executionBuilder.timeout,
265
+ // Sent with all builder api requests, unless the node runs in private mode
266
+ userAgent: opts.executionBuilder.userAgent,
267
+ },
262
268
  });
263
269
 
264
270
  // Load persisted data from disk to in-memory caches
@@ -335,6 +341,8 @@ export class BeaconNode {
335
341
 
336
342
  void runNodeNotifier({network, chain, sync, config, logger, signal});
337
343
 
344
+ startDeferredVoluntaryExitPublisher({chain, network, logger, signal});
345
+
338
346
  return new BeaconNode({
339
347
  opts,
340
348
  config,
package/src/sync/types.ts CHANGED
@@ -36,6 +36,39 @@ export enum PendingBlockInputStatus {
36
36
  processing = "processing",
37
37
  }
38
38
 
39
+ /**
40
+ * Why a pending item was dropped from BlockInputSync WITHOUT completing.
41
+ * The happy path — a successfully imported/processed block or payload — is removed by the import
42
+ * flow (onPayloadImported, processReadyBlock/processPayload success) and is deliberately NOT tracked
43
+ * here
44
+ */
45
+ export enum DroppedItemReason {
46
+ /** prune: item's own slot is below the finalized slot (routine cleanup), or a downloaded block landed at/below finality */
47
+ belowFinalized = "below_finalized",
48
+ /** prune: slot-less, unresolvable payload evicted after PRUNE_UNRESOLVED_SLOT_EPOCHS */
49
+ agedOut = "aged_out",
50
+ /** pruneSetToMax evicted the oldest entry because the cache hit its capacity (DoS guard) */
51
+ capacity = "capacity",
52
+ /** descendant removed because an ancestor was invalid/removed */
53
+ invalidParent = "invalid_parent",
54
+ /** transient execution-engine error removal (EXECUTION_ENGINE_ERROR) */
55
+ elError = "el_error",
56
+ /** the execution engine declared the item invalid (EXECUTION_ENGINE_INVALID) */
57
+ elInvalid = "el_invalid",
58
+ /** a block failed our own checks (not correct w.r.t. our chain) */
59
+ invalidBlock = "invalid_block",
60
+ /** a block declares a parent payload hash that conflicts with the parent's actual payload (gloas) */
61
+ invalidParentPayload = "invalid_parent_payload",
62
+ /** a payload envelope failed gossip verification (ENVELOPE_VERIFICATION_ERROR) */
63
+ invalidEnvelope = "invalid_envelope",
64
+ /** a payload had an invalid signature (INVALID_SIGNATURE) */
65
+ invalidSignature = "invalid_signature",
66
+ /** a block root was given up on after exhausting download attempts (data could not be fetched) */
67
+ unavailable = "unavailable",
68
+ /** an unexpected / unhandled error code led to removal */
69
+ unknown = "unknown",
70
+ }
71
+
39
72
  export enum PendingPayloadInputStatus {
40
73
  pending = "pending",
41
74
  fetching = "fetching",