@lodestar/beacon-node 1.45.0-dev.4d9beacde9 → 1.45.0-dev.4ed65a371b

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 (118) hide show
  1. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/pool/index.js +2 -43
  3. package/lib/api/impl/beacon/pool/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 +46 -3
  6. package/lib/api/impl/beacon/state/index.js.map +1 -1
  7. package/lib/api/impl/beacon/state/utils.d.ts +11 -2
  8. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  9. package/lib/api/impl/beacon/state/utils.js +39 -3
  10. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  11. package/lib/api/impl/validator/index.d.ts.map +1 -1
  12. package/lib/api/impl/validator/index.js +46 -6
  13. package/lib/api/impl/validator/index.js.map +1 -1
  14. package/lib/chain/blocks/importBlock.js +1 -1
  15. package/lib/chain/blocks/importBlock.js.map +1 -1
  16. package/lib/chain/chain.d.ts +1 -2
  17. package/lib/chain/chain.d.ts.map +1 -1
  18. package/lib/chain/chain.js +1 -3
  19. package/lib/chain/chain.js.map +1 -1
  20. package/lib/chain/interface.d.ts +1 -2
  21. package/lib/chain/interface.d.ts.map +1 -1
  22. package/lib/chain/interface.js.map +1 -1
  23. package/lib/chain/opPools/proposerPreferencesPool.d.ts +2 -1
  24. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  25. package/lib/chain/opPools/proposerPreferencesPool.js +3 -0
  26. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  27. package/lib/chain/options.d.ts +1 -0
  28. package/lib/chain/options.d.ts.map +1 -1
  29. package/lib/chain/options.js +1 -0
  30. package/lib/chain/options.js.map +1 -1
  31. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  32. package/lib/chain/produceBlock/produceBlockBody.js +4 -12
  33. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  34. package/lib/chain/seenCache/index.d.ts +0 -1
  35. package/lib/chain/seenCache/index.d.ts.map +1 -1
  36. package/lib/chain/seenCache/index.js +0 -1
  37. package/lib/chain/seenCache/index.js.map +1 -1
  38. package/lib/chain/validation/executionPayloadBid.js +11 -10
  39. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  40. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  41. package/lib/chain/validation/proposerPreferences.js +11 -3
  42. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  43. package/lib/network/core/metrics.d.ts +65 -0
  44. package/lib/network/core/metrics.d.ts.map +1 -1
  45. package/lib/network/core/metrics.js +94 -0
  46. package/lib/network/core/metrics.js.map +1 -1
  47. package/lib/network/discv5/utils.d.ts.map +1 -1
  48. package/lib/network/discv5/utils.js +12 -3
  49. package/lib/network/discv5/utils.js.map +1 -1
  50. package/lib/network/gossip/encoding.d.ts.map +1 -1
  51. package/lib/network/gossip/encoding.js +7 -0
  52. package/lib/network/gossip/encoding.js.map +1 -1
  53. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  54. package/lib/network/gossip/gossipsub.js +2 -1
  55. package/lib/network/gossip/gossipsub.js.map +1 -1
  56. package/lib/network/gossip/topic.d.ts +6 -1
  57. package/lib/network/gossip/topic.d.ts.map +1 -1
  58. package/lib/network/gossip/topic.js +23 -3
  59. package/lib/network/gossip/topic.js.map +1 -1
  60. package/lib/network/peers/peerManager.d.ts.map +1 -1
  61. package/lib/network/peers/peerManager.js +10 -0
  62. package/lib/network/peers/peerManager.js.map +1 -1
  63. package/lib/network/peers/score/score.d.ts +3 -1
  64. package/lib/network/peers/score/score.d.ts.map +1 -1
  65. package/lib/network/peers/score/score.js +6 -1
  66. package/lib/network/peers/score/score.js.map +1 -1
  67. package/lib/network/peers/score/store.d.ts.map +1 -1
  68. package/lib/network/peers/score/store.js +6 -1
  69. package/lib/network/peers/score/store.js.map +1 -1
  70. package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
  71. package/lib/network/peers/utils/prioritizePeers.js +14 -0
  72. package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
  73. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  74. package/lib/network/processor/gossipHandlers.js +2 -3
  75. package/lib/network/processor/gossipHandlers.js.map +1 -1
  76. package/lib/sync/range/chain.d.ts +4 -1
  77. package/lib/sync/range/chain.d.ts.map +1 -1
  78. package/lib/sync/range/chain.js +3 -3
  79. package/lib/sync/range/chain.js.map +1 -1
  80. package/lib/sync/range/range.d.ts.map +1 -1
  81. package/lib/sync/range/range.js +10 -0
  82. package/lib/sync/range/range.js.map +1 -1
  83. package/lib/util/graffiti.d.ts +15 -0
  84. package/lib/util/graffiti.d.ts.map +1 -1
  85. package/lib/util/graffiti.js +55 -0
  86. package/lib/util/graffiti.js.map +1 -1
  87. package/package.json +14 -14
  88. package/src/api/impl/beacon/pool/index.ts +0 -53
  89. package/src/api/impl/beacon/state/index.ts +52 -1
  90. package/src/api/impl/beacon/state/utils.ts +57 -4
  91. package/src/api/impl/validator/index.ts +54 -5
  92. package/src/chain/blocks/importBlock.ts +1 -1
  93. package/src/chain/chain.ts +0 -3
  94. package/src/chain/interface.ts +0 -2
  95. package/src/chain/opPools/proposerPreferencesPool.ts +5 -1
  96. package/src/chain/options.ts +2 -0
  97. package/src/chain/produceBlock/produceBlockBody.ts +9 -17
  98. package/src/chain/seenCache/index.ts +0 -1
  99. package/src/chain/validation/executionPayloadBid.ts +14 -9
  100. package/src/chain/validation/proposerPreferences.ts +12 -3
  101. package/src/network/core/metrics.ts +94 -0
  102. package/src/network/discv5/utils.ts +11 -3
  103. package/src/network/gossip/encoding.ts +7 -0
  104. package/src/network/gossip/gossipsub.ts +2 -1
  105. package/src/network/gossip/topic.ts +28 -4
  106. package/src/network/peers/peerManager.ts +11 -0
  107. package/src/network/peers/score/score.ts +8 -1
  108. package/src/network/peers/score/store.ts +8 -1
  109. package/src/network/peers/utils/prioritizePeers.ts +15 -0
  110. package/src/network/processor/gossipHandlers.ts +1 -2
  111. package/src/sync/range/chain.ts +4 -5
  112. package/src/sync/range/range.ts +10 -0
  113. package/src/util/graffiti.ts +78 -0
  114. package/lib/chain/seenCache/seenProposerPreferences.d.ts +0 -16
  115. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +0 -1
  116. package/lib/chain/seenCache/seenProposerPreferences.js +0 -26
  117. package/lib/chain/seenCache/seenProposerPreferences.js.map +0 -1
  118. package/src/chain/seenCache/seenProposerPreferences.ts +0 -32
@@ -1,7 +1,6 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ApplicationMethods} from "@lodestar/api/server";
3
3
  import {
4
- ForkName,
5
4
  ForkPostElectra,
6
5
  ForkPreElectra,
7
6
  SYNC_COMMITTEE_SUBNET_SIZE,
@@ -17,15 +16,12 @@ import {
17
16
  GossipAction,
18
17
  PayloadAttestationError,
19
18
  PayloadAttestationErrorCode,
20
- ProposerPreferencesError,
21
- ProposerPreferencesErrorCode,
22
19
  SyncCommitteeError,
23
20
  } from "../../../../chain/errors/index.js";
24
21
  import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
25
22
  import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
26
23
  import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
27
24
  import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js";
28
- import {validateGossipProposerPreferences} from "../../../../chain/validation/proposerPreferences.js";
29
25
  import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
30
26
  import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
31
27
  import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
@@ -66,55 +62,6 @@ export function getBeaconPoolApi({
66
62
  return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}};
67
63
  },
68
64
 
69
- async getPoolProposerPreferences({slot}) {
70
- const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot);
71
- if (!isForkPostGloas(fork)) {
72
- throw new ApiError(400, `Proposer preferences pool is not supported before Gloas fork=${fork}`);
73
- }
74
-
75
- return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}};
76
- },
77
-
78
- async submitSignedProposerPreferences({signedProposerPreferences}) {
79
- const failures: FailureList = [];
80
-
81
- await Promise.all(
82
- signedProposerPreferences.map(async (signed, i) => {
83
- try {
84
- await validateGossipProposerPreferences(chain, signed);
85
-
86
- chain.proposerPreferencesPool.add(signed);
87
- await network.publishProposerPreferences(signed);
88
- chain.emitter.emit(routes.events.EventType.proposerPreferences, {
89
- version: ForkName.gloas,
90
- data: signed,
91
- });
92
- } catch (e) {
93
- const logCtx = {
94
- slot: signed.message.proposalSlot,
95
- validatorIndex: signed.message.validatorIndex,
96
- dependentRoot: toRootHex(signed.message.dependentRoot),
97
- };
98
-
99
- if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
100
- logger.debug("Ignoring known signed proposer preferences", logCtx);
101
- return;
102
- }
103
-
104
- failures.push({index: i, message: (e as Error).message});
105
- logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, e as Error);
106
- if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
107
- chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
108
- }
109
- }
110
- })
111
- );
112
-
113
- if (failures.length > 0) {
114
- throw new IndexedError("Error processing signed proposer preferences", failures);
115
- }
116
- },
117
-
118
65
  async getPoolAttesterSlashingsV2() {
119
66
  const fork = chain.config.getForkName(chain.clock.currentSlot);
120
67
  return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: fork}};
@@ -9,13 +9,15 @@ import {
9
9
  isStatePostAltair,
10
10
  isStatePostElectra,
11
11
  isStatePostFulu,
12
+ isStatePostGloas,
12
13
  } from "@lodestar/state-transition";
13
- import {ValidatorIndex, getValidatorStatus, ssz} from "@lodestar/types";
14
+ import {ValidatorIndex, getBuilderStatus, getValidatorStatus, ssz} from "@lodestar/types";
14
15
  import {ApiError} from "../../errors.js";
15
16
  import {ApiModules} from "../../types.js";
16
17
  import {assertUniqueItems} from "../../utils.js";
17
18
  import {
18
19
  filterStateValidatorsByStatus,
20
+ getStateBuilderIndex,
19
21
  getStateResponseWithRegen,
20
22
  getStateValidatorIndex,
21
23
  toValidatorResponse,
@@ -143,6 +145,55 @@ export function getBeaconStateApi({
143
145
  return this.getStateValidators(args, context);
144
146
  },
145
147
 
148
+ async getStateBuilders({stateId, builderIds = [], statuses = []}) {
149
+ const {state, executionOptimistic, finalized} = await getState(stateId);
150
+ if (!isStatePostGloas(state)) {
151
+ throw new ApiError(400, `Builders are not supported for pre-gloas state fork=${state.forkName}`);
152
+ }
153
+ const finalizedEpoch = state.finalizedCheckpoint.epoch;
154
+
155
+ const builderResponses: routes.beacon.BuilderResponse[] = [];
156
+ if (builderIds.length) {
157
+ assertUniqueItems(builderIds, "Duplicate builder IDs provided");
158
+
159
+ for (const id of builderIds) {
160
+ const resp = getStateBuilderIndex(id, state);
161
+ if (resp.valid) {
162
+ const builderIndex = resp.builderIndex;
163
+ const builder = state.getBuilder(builderIndex);
164
+ const status = getBuilderStatus(builder, finalizedEpoch);
165
+ if (statuses.length && !statuses.includes(status)) {
166
+ continue;
167
+ }
168
+ builderResponses.push({index: builderIndex, status, builder});
169
+ }
170
+ }
171
+ return {
172
+ data: builderResponses,
173
+ meta: {executionOptimistic, finalized},
174
+ };
175
+ }
176
+
177
+ if (statuses.length) {
178
+ assertUniqueItems(statuses, "Duplicate statuses provided");
179
+ }
180
+
181
+ const buildersLength = state.getBuildersLength();
182
+ for (let builderIndex = 0; builderIndex < buildersLength; builderIndex++) {
183
+ const builder = state.getBuilder(builderIndex);
184
+ const status = getBuilderStatus(builder, finalizedEpoch);
185
+ if (statuses.length && !statuses.includes(status)) {
186
+ continue;
187
+ }
188
+ builderResponses.push({index: builderIndex, status, builder});
189
+ }
190
+
191
+ return {
192
+ data: builderResponses,
193
+ meta: {executionOptimistic, finalized},
194
+ };
195
+ },
196
+
146
197
  async postStateValidatorIdentities({stateId, validatorIds = []}) {
147
198
  const {state, executionOptimistic, finalized} = await getState(stateId);
148
199
  const {pubkeyCache} = chain;
@@ -1,9 +1,18 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice";
3
3
  import {GENESIS_SLOT} from "@lodestar/params";
4
- import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition";
5
- import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types";
6
- import {fromHex} from "@lodestar/utils";
4
+ import {IBeaconStateView, IBeaconStateViewGloas, PubkeyCache} from "@lodestar/state-transition";
5
+ import {
6
+ BLSPubkey,
7
+ BuilderIndex,
8
+ Epoch,
9
+ RootHex,
10
+ Slot,
11
+ ValidatorIndex,
12
+ getValidatorStatus,
13
+ phase0,
14
+ } from "@lodestar/types";
15
+ import {byteArrayEquals, fromHex} from "@lodestar/utils";
7
16
  import {IBeaconChain} from "../../../../chain/index.js";
8
17
  import {ApiError, ValidationError} from "../../errors.js";
9
18
 
@@ -78,6 +87,50 @@ export function toValidatorResponse(
78
87
  };
79
88
  }
80
89
 
90
+ type StateBuilderIndexResponse =
91
+ | {valid: true; builderIndex: BuilderIndex}
92
+ | {valid: false; code: number; reason: string};
93
+
94
+ export function getStateBuilderIndex(
95
+ id: routes.beacon.BuilderId | BLSPubkey,
96
+ state: IBeaconStateViewGloas
97
+ ): StateBuilderIndexResponse {
98
+ if (typeof id === "string") {
99
+ // mutate `id` and fallthrough to below
100
+ if (id.startsWith("0x")) {
101
+ try {
102
+ id = fromHex(id);
103
+ } catch (_e) {
104
+ return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"};
105
+ }
106
+ } else {
107
+ id = Number(id);
108
+ }
109
+ }
110
+
111
+ if (typeof id === "number") {
112
+ const builderIndex = id;
113
+ // builder is invalid or added later than given stateId
114
+ if (!Number.isSafeInteger(builderIndex) || builderIndex < 0) {
115
+ return {valid: false, code: 400, reason: "Invalid builder index"};
116
+ }
117
+ if (builderIndex >= state.getBuildersLength()) {
118
+ return {valid: false, code: 404, reason: "Builder index from future state"};
119
+ }
120
+ return {valid: true, builderIndex};
121
+ }
122
+
123
+ // typeof id === Uint8Array
124
+ // There is no builder pubkey cache, linear scan over the registry
125
+ const buildersLength = state.getBuildersLength();
126
+ for (let builderIndex = 0; builderIndex < buildersLength; builderIndex++) {
127
+ if (byteArrayEquals(state.getBuilder(builderIndex).pubkey, id)) {
128
+ return {valid: true, builderIndex};
129
+ }
130
+ }
131
+ return {valid: false, code: 404, reason: "Builder pubkey not found in state"};
132
+ }
133
+
81
134
  export function filterStateValidatorsByStatus(
82
135
  statuses: string[],
83
136
  state: IBeaconStateView,
@@ -122,7 +175,7 @@ export function getStateValidatorIndex(
122
175
  if (typeof id === "number") {
123
176
  const validatorIndex = id;
124
177
  // validator is invalid or added later than given stateId
125
- if (!Number.isSafeInteger(validatorIndex)) {
178
+ if (!Number.isSafeInteger(validatorIndex) || validatorIndex < 0) {
126
179
  return {valid: false, code: 400, reason: "Invalid validator index"};
127
180
  }
128
181
  if (validatorIndex >= state.validatorCount) {
@@ -65,6 +65,8 @@ import {
65
65
  AttestationError,
66
66
  AttestationErrorCode,
67
67
  GossipAction,
68
+ ProposerPreferencesError,
69
+ ProposerPreferencesErrorCode,
68
70
  SyncCommitteeError,
69
71
  SyncCommitteeErrorCode,
70
72
  } from "../../../chain/errors/index.js";
@@ -74,6 +76,7 @@ import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/prod
74
76
  import {RegenCaller} from "../../../chain/regen/index.js";
75
77
  import {CheckpointHex} from "../../../chain/stateCache/types.js";
76
78
  import {validateApiAggregateAndProof} from "../../../chain/validation/index.js";
79
+ import {validateGossipProposerPreferences} from "../../../chain/validation/proposerPreferences.js";
77
80
  import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js";
78
81
  import {ZERO_HASH} from "../../../constants/index.js";
79
82
  import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js";
@@ -82,7 +85,7 @@ import {CommitteeSubscription} from "../../../network/subnets/index.js";
82
85
  import {SyncState} from "../../../sync/index.js";
83
86
  import {callInNextEventLoop} from "../../../util/eventLoop.js";
84
87
  import {isOptimisticBlock} from "../../../util/forkChoice.js";
85
- import {getDefaultGraffiti, toGraffitiBytes} from "../../../util/graffiti.js";
88
+ import {getBlockGraffiti, toGraffitiBytes} from "../../../util/graffiti.js";
86
89
  import {getLodestarClientVersion} from "../../../util/metadata.js";
87
90
  import {ApiOptions} from "../../options.js";
88
91
  import {getStateResponseWithRegen} from "../beacon/state/utils.js";
@@ -611,7 +614,10 @@ export function getValidatorApi(
611
614
  }
612
615
 
613
616
  const graffitiBytes = toGraffitiBytes(
614
- graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts)
617
+ getBlockGraffiti(graffiti, getLodestarClientVersion(opts), chain.executionEngine.clientVersion, {
618
+ private: opts.private,
619
+ graffitiAppend: chain.opts.graffitiAppend,
620
+ })
615
621
  );
616
622
 
617
623
  const loggerContext = {
@@ -921,7 +927,10 @@ export function getValidatorApi(
921
927
  metrics?.blockProductionSlotDelta.set(slot - parentSlot);
922
928
 
923
929
  const graffitiBytes = toGraffitiBytes(
924
- graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts)
930
+ getBlockGraffiti(graffiti, getLodestarClientVersion(opts), chain.executionEngine.clientVersion, {
931
+ private: opts.private,
932
+ graffitiAppend: chain.opts.graffitiAppend,
933
+ })
925
934
  );
926
935
 
927
936
  // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
@@ -1112,8 +1121,8 @@ export function getValidatorApi(
1112
1121
 
1113
1122
  const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
1114
1123
  if (!block) {
1115
- // No block is seen at slot. Return 404 so vc can skip casting payload attestation.
1116
- throw new ApiError(404, `No canonical block found at slot=${slot}`);
1124
+ // No canonical block is seen at slot. Return 204 so vc can skip casting payload attestation.
1125
+ return {data: undefined, meta: {version: fork}, status: 204};
1117
1126
  }
1118
1127
 
1119
1128
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
@@ -1765,6 +1774,46 @@ export function getValidatorApi(
1765
1774
  });
1766
1775
  },
1767
1776
 
1777
+ async submitProposerPreferences({signedProposerPreferences}) {
1778
+ const failures: FailureList = [];
1779
+
1780
+ await Promise.all(
1781
+ signedProposerPreferences.map(async (signed, i) => {
1782
+ try {
1783
+ await validateGossipProposerPreferences(chain, signed);
1784
+
1785
+ chain.proposerPreferencesPool.add(signed);
1786
+ await network.publishProposerPreferences(signed);
1787
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1788
+ version: config.getForkName(signed.message.proposalSlot),
1789
+ data: signed,
1790
+ });
1791
+ } catch (e) {
1792
+ const logCtx = {
1793
+ slot: signed.message.proposalSlot,
1794
+ validatorIndex: signed.message.validatorIndex,
1795
+ dependentRoot: toRootHex(signed.message.dependentRoot),
1796
+ };
1797
+
1798
+ if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
1799
+ logger.debug("Ignoring known signed proposer preferences", logCtx);
1800
+ return;
1801
+ }
1802
+
1803
+ failures.push({index: i, message: (e as Error).message});
1804
+ logger.verbose(`Error on submitProposerPreferences [${i}]`, logCtx, e as Error);
1805
+ if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
1806
+ chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
1807
+ }
1808
+ }
1809
+ })
1810
+ );
1811
+
1812
+ if (failures.length > 0) {
1813
+ throw new IndexedError("Error processing signed proposer preferences", failures);
1814
+ }
1815
+ },
1816
+
1768
1817
  async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) {
1769
1818
  const fork = config.getForkName(slot);
1770
1819
 
@@ -426,7 +426,7 @@ export async function importBlock(
426
426
  }
427
427
  } catch (e) {
428
428
  if (isStartSlotOfEpoch(proposalSlot)) {
429
- notOverrideFcuReason = NotReorgedReason.NotShufflingStable;
429
+ notOverrideFcuReason = NotReorgedReason.AtEpochBoundary;
430
430
  } else {
431
431
  this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
432
432
  }
@@ -107,7 +107,6 @@ import {
107
107
  SeenExecutionPayloadBids,
108
108
  SeenPayloadAttesters,
109
109
  SeenPayloadEnvelopeInput,
110
- SeenProposerPreferences,
111
110
  SeenSyncCommitteeMessages,
112
111
  } from "./seenCache/index.js";
113
112
  import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js";
@@ -189,7 +188,6 @@ export class BeaconChain implements IBeaconChain {
189
188
  readonly seenPayloadAttesters = new SeenPayloadAttesters();
190
189
  readonly seenAggregatedAttestations: SeenAggregatedAttestations;
191
190
  readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids();
192
- readonly seenProposerPreferences = new SeenProposerPreferences();
193
191
  readonly seenBlockProposers = new SeenBlockProposers();
194
192
  readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages();
195
193
  readonly seenContributionAndProof: SeenContributionAndProof;
@@ -1473,7 +1471,6 @@ export class BeaconChain implements IBeaconChain {
1473
1471
  this.payloadAttestationPool.prune(slot);
1474
1472
  this.executionPayloadBidPool.prune(slot);
1475
1473
  this.seenExecutionPayloadBids.prune(slot);
1476
- this.seenProposerPreferences.prune(slot);
1477
1474
  this.proposerPreferencesPool.prune(slot);
1478
1475
  this.seenAttestationDatas.onSlot(slot);
1479
1476
  this.reprocessController.onSlot(slot);
@@ -61,7 +61,6 @@ import {
61
61
  SeenContributionAndProof,
62
62
  SeenExecutionPayloadBids,
63
63
  SeenPayloadAttesters,
64
- SeenProposerPreferences,
65
64
  SeenSyncCommitteeMessages,
66
65
  } from "./seenCache/index.js";
67
66
  import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js";
@@ -133,7 +132,6 @@ export interface IBeaconChain {
133
132
  readonly seenPayloadAttesters: SeenPayloadAttesters;
134
133
  readonly seenAggregatedAttestations: SeenAggregatedAttestations;
135
134
  readonly seenExecutionPayloadBids: SeenExecutionPayloadBids;
136
- readonly seenProposerPreferences: SeenProposerPreferences;
137
135
  readonly seenBlockProposers: SeenBlockProposers;
138
136
  readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages;
139
137
  readonly seenContributionAndProof: SeenContributionAndProof;
@@ -1,4 +1,4 @@
1
- import {RootHex, Slot, gloas} from "@lodestar/types";
1
+ import {RootHex, Slot, ValidatorIndex, gloas} from "@lodestar/types";
2
2
  import {toRootHex} from "@lodestar/utils";
3
3
 
4
4
  /**
@@ -22,6 +22,10 @@ export class ProposerPreferencesPool {
22
22
  return this.bySlot.get(slot)?.get(dependentRootHex) ?? null;
23
23
  }
24
24
 
25
+ isKnown(proposalSlot: Slot, dependentRoot: RootHex, validatorIndex: ValidatorIndex): boolean {
26
+ return this.get(proposalSlot, dependentRoot)?.message.validatorIndex === validatorIndex;
27
+ }
28
+
25
29
  add(signed: gloas.SignedProposerPreferences): void {
26
30
  const {proposalSlot, dependentRoot} = signed.message;
27
31
  const rootHex = toRootHex(dependentRoot);
@@ -35,6 +35,7 @@ export type IChainOptions = BlockProcessOpts &
35
35
  persistOrphanedBlocksDir?: string;
36
36
  skipCreateStateCacheIfAvailable?: boolean;
37
37
  suggestedFeeRecipient: string;
38
+ graffitiAppend?: boolean;
38
39
  maxSkipSlots?: number;
39
40
  /** Ensure blobs returned by the execution engine are valid */
40
41
  sanityCheckExecutionEngineBlobs?: boolean;
@@ -107,6 +108,7 @@ export const defaultChainOptions: IChainOptions = {
107
108
  computeUnrealized: true,
108
109
  fastConfirmation: false,
109
110
  suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
111
+ graffitiAppend: true,
110
112
  serveHistoricalState: false,
111
113
  assertCorrectProgressiveBalances: false,
112
114
  archiveStateEpochFrequency: 1024,
@@ -43,6 +43,7 @@ import {
43
43
  ValidatorIndex,
44
44
  Wei,
45
45
  altair,
46
+ bellatrix,
46
47
  capella,
47
48
  deneb,
48
49
  electra,
@@ -830,28 +831,19 @@ export function getPayloadAttributesForSSE(
830
831
  feeRecipient,
831
832
  });
832
833
 
833
- let parentBlockNumber: number;
834
- if (isForkPostGloas(fork)) {
835
- const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(
836
- toRootHex(parentBlockRoot),
837
- toRootHex(parentBlockHash)
838
- );
839
- if (parentBlock?.executionPayloadBlockHash == null) {
840
- throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`);
841
- }
842
- parentBlockNumber = parentBlock.executionPayloadNumber;
843
- } else {
844
- parentBlockNumber = prepareState.payloadBlockNumber;
845
- }
846
-
847
- const ssePayloadAttributes: SSEPayloadAttributes = {
834
+ const ssePayloadAttributes = {
848
835
  proposerIndex: prepareState.getBeaconProposer(prepareSlot),
849
836
  proposalSlot: prepareSlot,
850
- parentBlockNumber,
851
837
  parentBlockRoot,
852
838
  parentBlockHash,
853
839
  payloadAttributes,
854
- };
840
+ } as SSEPayloadAttributes;
841
+
842
+ if (!isForkPostGloas(fork)) {
843
+ // Removed in Gloas, builders can get the block number from the EL via the block hash if required
844
+ (ssePayloadAttributes as bellatrix.SSEPayloadAttributes).parentBlockNumber = prepareState.payloadBlockNumber;
845
+ }
846
+
855
847
  return ssePayloadAttributes;
856
848
  }
857
849
 
@@ -5,4 +5,3 @@ export {SeenContributionAndProof} from "./seenCommitteeContribution.js";
5
5
  export {SeenExecutionPayloadBids} from "./seenExecutionPayloadBids.js";
6
6
  export {SeenBlockInput} from "./seenGossipBlockInput.js";
7
7
  export {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenPayloadEnvelopeInput.js";
8
- export {SeenProposerPreferences} from "./seenProposerPreferences.js";
@@ -37,9 +37,11 @@ async function validateExecutionPayloadBid(
37
37
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
38
38
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
39
39
 
40
- // [IGNORE] `bid.slot` is the current slot or the next slot.
41
- const currentSlot = chain.clock.currentSlot;
42
- if (bid.slot !== currentSlot && bid.slot !== currentSlot + 1) {
40
+ // [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
41
+ if (
42
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
43
+ !chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
44
+ ) {
43
45
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
44
46
  code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
45
47
  builderIndex: bid.builderIndex,
@@ -122,17 +124,20 @@ async function validateExecutionPayloadBid(
122
124
  throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
123
125
  }
124
126
 
125
- // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
126
- // `is_active_builder(state, bid.builder_index)` returns `True`.
127
- let builder: gloas.Builder;
128
- try {
129
- builder = state.getBuilder(bid.builderIndex);
130
- } catch {
127
+ // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
128
+ // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
129
+ // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
130
+ // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
131
+ if (bid.builderIndex >= state.getBuildersLength()) {
131
132
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
132
133
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
133
134
  builderIndex: bid.builderIndex,
134
135
  });
135
136
  }
137
+
138
+ // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
139
+ // `is_active_builder(state, bid.builder_index)` returns `True`.
140
+ const builder = state.getBuilder(bid.builderIndex);
136
141
  if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
137
142
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
138
143
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
@@ -81,7 +81,7 @@ export async function validateGossipProposerPreferences(
81
81
  }
82
82
 
83
83
  // [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index).
84
- if (chain.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) {
84
+ if (chain.proposerPreferencesPool.isKnown(proposalSlot, dependentRootHex, validatorIndex)) {
85
85
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
86
86
  code: ProposerPreferencesErrorCode.ALREADY_KNOWN,
87
87
  proposalSlot,
@@ -105,6 +105,15 @@ export async function validateGossipProposerPreferences(
105
105
  });
106
106
  }
107
107
 
108
- // Valid
109
- chain.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex);
108
+ // Repeated check - deals with race-condition between preferences submissions
109
+ if (chain.proposerPreferencesPool.isKnown(proposalSlot, dependentRootHex, validatorIndex)) {
110
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
111
+ code: ProposerPreferencesErrorCode.ALREADY_KNOWN,
112
+ proposalSlot,
113
+ validatorIndex,
114
+ dependentRoot: dependentRootHex,
115
+ });
116
+ }
117
+
118
+ chain.proposerPreferencesPool.add(signedProposerPreferences);
110
119
  }
@@ -127,6 +127,100 @@ export function createNetworkCoreMetrics(register: RegistryMetricCreator) {
127
127
  name: "lodestar_peer_manager_starved_bool",
128
128
  help: "Whether lodestar is starved of data while syncing",
129
129
  }),
130
+ /**
131
+ * Core selection/pruning phase of the heartbeat, split out of heartbeatDuration so it
132
+ * can be attributed on its own. Divide by peersEvaluated for time-per-peer.
133
+ */
134
+ prioritizePeersDuration: register.histogram({
135
+ name: "lodestar_peer_manager_prioritize_peers_seconds",
136
+ help: "prioritizePeers function duration in seconds, the core peer selection/pruning algorithm",
137
+ buckets: [0.0005, 0.001, 0.005, 0.01, 0.05, 0.1],
138
+ }),
139
+ /**
140
+ * Score decay + map prune over every tracked peer, split out of heartbeatDuration.
141
+ * This loop iterates the whole score store, so divide by scoreMapSize (not
142
+ * peersEvaluated) for time-per-peer.
143
+ */
144
+ scoreUpdateDuration: register.histogram({
145
+ name: "lodestar_peer_score_update_seconds",
146
+ help: "Peer score store update (decay + prune over all peers) duration in seconds",
147
+ buckets: [0.0005, 0.001, 0.005, 0.01, 0.05, 0.1],
148
+ }),
149
+ /**
150
+ * Denominator for prioritizePeersDuration: it scales with the number of connected
151
+ * healthy peers, so time-per-peer (prioritizePeersDuration / this) is the comparable
152
+ * figure. For scoreUpdateDuration use scoreMapSize instead.
153
+ */
154
+ peersEvaluated: register.histogram({
155
+ name: "lodestar_peer_manager_peers_evaluated_count",
156
+ help: "Number of connected healthy peers evaluated by prioritizePeers per heartbeat, denominator for prioritize_peers_seconds",
157
+ buckets: [0, 25, 50, 75, 100, 150, 200],
158
+ }),
159
+ /**
160
+ * Every peer the manager intends to disconnect. Covers both the top-of-heartbeat
161
+ * bad-score disconnects ("banned"/"score_too_low") and the prioritization reasons;
162
+ * the older peersRequestedToDisconnect gauge only recorded the latter. Counts intent,
163
+ * not completion (goodbye is fire-and-forget).
164
+ */
165
+ peersPruned: register.counter<{reason: string}>({
166
+ name: "lodestar_peer_manager_peers_pruned_total",
167
+ help: "Total peers the peer manager intends to disconnect, labeled by reason (incl. bad-score and prioritization reasons)",
168
+ labelNames: ["reason"],
169
+ }),
170
+ /**
171
+ * Actual peer count per active subnet, to check the min-peers-per-subnet invariant.
172
+ * Buckets straddle the target (TARGET_SUBNET_PEERS = 6).
173
+ */
174
+ peersPerActiveSubnet: register.histogram<{type: SubnetType}>({
175
+ name: "lodestar_peer_manager_peers_per_active_subnet",
176
+ help: "Histogram of connected peer count per active subnet, labeled by subnet type",
177
+ labelNames: ["type"],
178
+ buckets: [0, 2, 4, 6, 8, 12],
179
+ }),
180
+ /**
181
+ * Live outbound ratio, to check the OUTBOUND_PEERS_RATIO (10%) invariant (issue #2215).
182
+ * Denominator is connected healthy peers (banned/disconnected already removed).
183
+ */
184
+ outboundPeersRatio: register.gauge({
185
+ name: "lodestar_peer_manager_outbound_peers_ratio",
186
+ help: "Ratio of outbound peers to total connected healthy peers, verifies the outbound peers invariant",
187
+ }),
188
+ /**
189
+ * Score state crossings (Healthy/Disconnected/Banned). Sensitive to the decay formula,
190
+ * thresholds, and gossip-score weighting.
191
+ */
192
+ scoreStateTransitions: register.counter<{from: string; to: string}>({
193
+ name: "lodestar_peer_score_state_transitions_total",
194
+ help: "Total peer score state transitions, labeled by from and to state (Healthy/Disconnected/Banned)",
195
+ labelNames: ["from", "to"],
196
+ }),
197
+ /**
198
+ * Entry count of the score store (not bytes). Reflects the prune-to-MAX_ENTRIES /
199
+ * SCORE_THRESHOLD retention logic. Also the denominator for scoreUpdateDuration
200
+ * time-per-peer (that loop iterates the whole store, not just connected peers).
201
+ */
202
+ scoreMapSize: register.gauge({
203
+ name: "lodestar_peer_manager_score_map_size",
204
+ help: "Current number of entries in the peer score store",
205
+ }),
206
+ /**
207
+ * Entry count of the connectedPeers map (not bytes). Should track libp2p_peers; a
208
+ * persistent gap indicates the connection leak guarded by leakedConnectionsCount.
209
+ */
210
+ connectedPeersMapSize: register.gauge({
211
+ name: "lodestar_peer_manager_connected_peers_map_size",
212
+ help: "Current number of entries in the peer manager connectedPeers map",
213
+ }),
214
+ /**
215
+ * assertPeerRelevance outcome per Status exchange: "relevant", an irrelevant reason
216
+ * code, or "error". Fires on every status evaluation (inbound requests and our own
217
+ * requestStatus responses both route through onStatus).
218
+ */
219
+ relevanceCheck: register.counter<{result: string}>({
220
+ name: "lodestar_peer_relevance_check_total",
221
+ help: "Total peer relevance checks on Status, labeled by result (relevant or irrelevant reason code)",
222
+ labelNames: ["result"],
223
+ }),
130
224
  },
131
225
  leakedConnectionsCount: register.gauge({
132
226
  name: "lodestar_peer_manager_leaked_connections_count",