@lodestar/beacon-node 1.45.0-dev.3100972235 → 1.45.0-dev.33dbc1859a

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 (106) 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/blocks/payloadEnvelopeProcessor.d.ts +5 -2
  17. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts.map +1 -1
  18. package/lib/chain/blocks/payloadEnvelopeProcessor.js +17 -17
  19. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
  20. package/lib/chain/chain.d.ts +1 -2
  21. package/lib/chain/chain.d.ts.map +1 -1
  22. package/lib/chain/chain.js +1 -3
  23. package/lib/chain/chain.js.map +1 -1
  24. package/lib/chain/interface.d.ts +1 -2
  25. package/lib/chain/interface.d.ts.map +1 -1
  26. package/lib/chain/interface.js.map +1 -1
  27. package/lib/chain/opPools/proposerPreferencesPool.d.ts +2 -1
  28. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  29. package/lib/chain/opPools/proposerPreferencesPool.js +3 -0
  30. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  31. package/lib/chain/options.d.ts +1 -0
  32. package/lib/chain/options.d.ts.map +1 -1
  33. package/lib/chain/options.js +1 -0
  34. package/lib/chain/options.js.map +1 -1
  35. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  36. package/lib/chain/produceBlock/produceBlockBody.js +4 -12
  37. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  38. package/lib/chain/seenCache/index.d.ts +0 -1
  39. package/lib/chain/seenCache/index.d.ts.map +1 -1
  40. package/lib/chain/seenCache/index.js +0 -1
  41. package/lib/chain/seenCache/index.js.map +1 -1
  42. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +1 -0
  43. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  44. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +7 -0
  45. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  46. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  47. package/lib/chain/stateCache/persistentCheckpointsCache.js +3 -2
  48. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  49. package/lib/chain/validation/executionPayloadBid.js +8 -7
  50. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  51. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  52. package/lib/chain/validation/proposerPreferences.js +11 -3
  53. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  54. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  55. package/lib/network/gossip/gossipsub.js +2 -1
  56. package/lib/network/gossip/gossipsub.js.map +1 -1
  57. package/lib/network/gossip/topic.d.ts +25 -777
  58. package/lib/network/gossip/topic.d.ts.map +1 -1
  59. package/lib/network/gossip/topic.js +23 -3
  60. package/lib/network/gossip/topic.js.map +1 -1
  61. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  62. package/lib/network/processor/gossipHandlers.js +28 -16
  63. package/lib/network/processor/gossipHandlers.js.map +1 -1
  64. package/lib/sync/range/chain.d.ts +4 -1
  65. package/lib/sync/range/chain.d.ts.map +1 -1
  66. package/lib/sync/range/chain.js +3 -3
  67. package/lib/sync/range/chain.js.map +1 -1
  68. package/lib/sync/range/range.d.ts.map +1 -1
  69. package/lib/sync/range/range.js +10 -0
  70. package/lib/sync/range/range.js.map +1 -1
  71. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  72. package/lib/sync/utils/downloadByRange.js +25 -19
  73. package/lib/sync/utils/downloadByRange.js.map +1 -1
  74. package/lib/util/graffiti.d.ts +15 -0
  75. package/lib/util/graffiti.d.ts.map +1 -1
  76. package/lib/util/graffiti.js +55 -0
  77. package/lib/util/graffiti.js.map +1 -1
  78. package/package.json +14 -14
  79. package/src/api/impl/beacon/pool/index.ts +0 -53
  80. package/src/api/impl/beacon/state/index.ts +52 -1
  81. package/src/api/impl/beacon/state/utils.ts +57 -4
  82. package/src/api/impl/validator/index.ts +54 -5
  83. package/src/chain/blocks/importBlock.ts +1 -1
  84. package/src/chain/blocks/payloadEnvelopeProcessor.ts +17 -17
  85. package/src/chain/chain.ts +0 -3
  86. package/src/chain/interface.ts +0 -2
  87. package/src/chain/opPools/proposerPreferencesPool.ts +5 -1
  88. package/src/chain/options.ts +2 -0
  89. package/src/chain/produceBlock/produceBlockBody.ts +9 -17
  90. package/src/chain/seenCache/index.ts +0 -1
  91. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +8 -0
  92. package/src/chain/stateCache/persistentCheckpointsCache.ts +3 -2
  93. package/src/chain/validation/executionPayloadBid.ts +9 -6
  94. package/src/chain/validation/proposerPreferences.ts +12 -3
  95. package/src/network/gossip/gossipsub.ts +2 -1
  96. package/src/network/gossip/topic.ts +28 -4
  97. package/src/network/processor/gossipHandlers.ts +30 -16
  98. package/src/sync/range/chain.ts +4 -5
  99. package/src/sync/range/range.ts +10 -0
  100. package/src/sync/utils/downloadByRange.ts +26 -19
  101. package/src/util/graffiti.ts +78 -0
  102. package/lib/chain/seenCache/seenProposerPreferences.d.ts +0 -16
  103. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +0 -1
  104. package/lib/chain/seenCache/seenProposerPreferences.js +0 -26
  105. package/lib/chain/seenCache/seenProposerPreferences.js.map +0 -1
  106. package/src/chain/seenCache/seenProposerPreferences.ts +0 -32
@@ -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
  }
@@ -8,28 +8,24 @@ import {ImportPayloadOpts} from "./types.js";
8
8
  // TODO GLOAS: Set to be equal to DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES for now
9
9
  const QUEUE_MAX_LENGTH = 16;
10
10
 
11
- enum PayloadEnvelopeImportStatus {
12
- queued = "queued",
13
- importing = "importing",
14
- imported = "imported",
15
- }
16
-
17
11
  /**
18
12
  * PayloadEnvelopeProcessor processes payload envelope jobs in a queued fashion, one after the other.
19
13
  *
20
14
  * Jobs are enqueued only on envelope arrival (gossip or API). The envelope may reach us before
21
15
  * the sampled data columns; importExecutionPayload awaits `verifyPayloadsDataAvailability`
22
16
  * internally, so a queued job can pend for up to `PAYLOAD_DATA_AVAILABILITY_TIMEOUT` while
23
- * waiting for columns. Duplicate triggers for the same payloadInput are deduped via `importStatus`.
17
+ * waiting for columns. Duplicate triggers for the same payloadInput are deduped by sharing the
18
+ * in-flight import promise: every caller observes the real import outcome. Resolving duplicates
19
+ * early instead would let BlockInputSync mark a still-pending payload as processed, re-queue it,
20
+ * and spin re-processing the same root until OOM while the import is blocked (e.g. EL syncing).
24
21
  */
25
22
  export class PayloadEnvelopeProcessor {
26
23
  readonly jobQueue: JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>;
27
- private readonly importStatus = new WeakMap<PayloadEnvelopeInput, PayloadEnvelopeImportStatus>();
24
+ private readonly imports = new WeakMap<PayloadEnvelopeInput, Promise<void>>();
28
25
 
29
26
  constructor(chain: BeaconChain, metrics: Metrics | null, signal: AbortSignal) {
30
27
  this.jobQueue = new JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>(
31
28
  (payloadInput, opts) => {
32
- this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.importing);
33
29
  return processExecutionPayload.call(chain, payloadInput, signal, opts);
34
30
  },
35
31
  {maxLength: QUEUE_MAX_LENGTH, noYieldIfOneItem: true, signal},
@@ -38,24 +34,28 @@ export class PayloadEnvelopeProcessor {
38
34
  }
39
35
 
40
36
  async processPayloadEnvelopeJob(payloadInput: PayloadEnvelopeInput, opts: ImportPayloadOpts = {}): Promise<void> {
41
- if (this.importStatus.get(payloadInput) !== undefined) {
42
- return;
37
+ const existing = this.imports.get(payloadInput);
38
+ if (existing) {
39
+ return existing;
43
40
  }
44
41
 
45
42
  await this.jobQueue.waitForSpace();
46
43
 
47
44
  // Re-check after await, as another call may have queued this payload.
48
- if (this.importStatus.get(payloadInput) !== undefined) {
49
- return;
45
+ const queued = this.imports.get(payloadInput);
46
+ if (queued) {
47
+ return queued;
50
48
  }
51
49
 
52
- this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.queued);
50
+ const importPromise = this.jobQueue.push(payloadInput, opts);
51
+ this.imports.set(payloadInput, importPromise);
53
52
 
54
53
  try {
55
- await this.jobQueue.push(payloadInput, opts);
56
- this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.imported);
54
+ await importPromise;
57
55
  } catch (e) {
58
- this.importStatus.delete(payloadInput);
56
+ // Drop the failed import so a later attempt can retry once the failure cause is resolved
57
+ // (e.g. BLOCK_NOT_IN_FORK_CHOICE after the block lands in fork choice)
58
+ this.imports.delete(payloadInput);
59
59
  throw e;
60
60
  }
61
61
  }
@@ -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";
@@ -152,6 +152,14 @@ export class SeenPayloadEnvelopeInput {
152
152
  return this.payloadInputs.size;
153
153
  }
154
154
 
155
+ prune(blockRootHex: RootHex): void {
156
+ const input = this.payloadInputs.get(blockRootHex);
157
+ if (input) {
158
+ this.evictPayloadInput(input);
159
+ this.logger?.verbose("SeenPayloadEnvelopeInput.prune deleted", {slot: input.slot, root: blockRootHex});
160
+ }
161
+ }
162
+
155
163
  pruneBelowParent(parentBlock: ProtoBlock): void {
156
164
  for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
157
165
  // Only evict once the payload is FULL (revealed/imported) — on an EMPTY/PENDING branch we may
@@ -331,8 +331,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
331
331
  const key = toCacheKey(cpHex);
332
332
  const cacheItem = this.cache.get(key);
333
333
  this.metrics?.cpStateCache.adds.inc();
334
- if (cacheItem !== undefined && isPersistedCacheItem(cacheItem)) {
335
- const persistedKey = cacheItem.value;
334
+ // keep an existing persistedKey (persisted or reloaded-in-memory); dropping it orphans the on-disk file
335
+ const persistedKey = cacheItem && (isPersistedCacheItem(cacheItem) ? cacheItem.value : cacheItem.persistedKey);
336
+ if (persistedKey !== undefined) {
336
337
  // was persisted to disk, set back to memory
337
338
  this.cache.set(key, {type: CacheItemType.inMemory, state, persistedKey});
338
339
  this.logger.verbose("Added checkpoint state to memory but a persisted key existed", {
@@ -122,17 +122,20 @@ async function validateExecutionPayloadBid(
122
122
  throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
123
123
  }
124
124
 
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 {
125
+ // [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
126
+ // `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
127
+ // out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
128
+ // `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
129
+ if (bid.builderIndex >= state.getBuildersLength()) {
131
130
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
132
131
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
133
132
  builderIndex: bid.builderIndex,
134
133
  });
135
134
  }
135
+
136
+ // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
137
+ // `is_active_builder(state, bid.builder_index)` returns `True`.
138
+ const builder = state.getBuilder(bid.builderIndex);
136
139
  if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
137
140
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
138
141
  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
  }
@@ -35,7 +35,7 @@ import {
35
35
  computeGossipPeerScoreParams,
36
36
  gossipScoreThresholds,
37
37
  } from "./scoringParameters.js";
38
- import {GossipTopicCache, getCoreTopicsAtFork, stringifyGossipTopic} from "./topic.js";
38
+ import {GossipTopicCache, getAllowedTopics, getCoreTopicsAtFork, stringifyGossipTopic} from "./topic.js";
39
39
 
40
40
  /** As specified in https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md */
41
41
  const GOSSIPSUB_HEARTBEAT_INTERVAL = 0.7 * 1000;
@@ -140,6 +140,7 @@ export class Eth2Gossipsub {
140
140
  const gossipsubInstance = gossipsub({
141
141
  globalSignaturePolicy: StrictNoSign,
142
142
  allowPublishToZeroTopicPeers: allowPublishToZeroPeers,
143
+ allowedTopics: getAllowedTopics(networkConfig),
143
144
  D: gossipsubD ?? GOSSIP_D,
144
145
  Dlo: gossipsubDLow ?? GOSSIP_D_LOW,
145
146
  Dhi: gossipsubDHigh ?? GOSSIP_D_HIGH,
@@ -246,7 +246,7 @@ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr:
246
246
  export function getCoreTopicsAtFork(
247
247
  networkConfig: NetworkConfig,
248
248
  fork: ForkName,
249
- opts: {subscribeAllSubnets?: boolean; disableLightClientServer?: boolean}
249
+ opts: {subscribeAllSubnets?: boolean; subscribeAllColumnSubnets?: boolean; disableLightClientServer?: boolean}
250
250
  ): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
251
251
  // Common topics for all forks
252
252
  const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [
@@ -266,7 +266,7 @@ export function getCoreTopicsAtFork(
266
266
 
267
267
  // After fulu also track data_column_sidecar_{index}
268
268
  if (ForkSeq[fork] >= ForkSeq.fulu) {
269
- topics.push(...getDataColumnSidecarTopics(networkConfig));
269
+ topics.push(...getDataColumnSidecarTopics(networkConfig, opts.subscribeAllColumnSubnets));
270
270
  }
271
271
 
272
272
  // After Deneb and before Fulu also track blob_sidecar_{subnet_id}
@@ -309,15 +309,39 @@ export function getCoreTopicsAtFork(
309
309
  return topics;
310
310
  }
311
311
 
312
+ /**
313
+ * Build the complete set of valid gossip topic strings across every fork boundary.
314
+ */
315
+ export function getAllowedTopics(networkConfig: NetworkConfig): Set<string> {
316
+ const {config} = networkConfig;
317
+ const allowedTopics = new Set<string>();
318
+
319
+ for (const boundary of config.forkBoundariesAscendingEpochOrder) {
320
+ const topics = getCoreTopicsAtFork(networkConfig, boundary.fork, {
321
+ subscribeAllSubnets: true,
322
+ subscribeAllColumnSubnets: true,
323
+ disableLightClientServer: false,
324
+ });
325
+ for (const topic of topics) {
326
+ allowedTopics.add(stringifyGossipTopic(config, {...topic, boundary}));
327
+ }
328
+ }
329
+
330
+ return allowedTopics;
331
+ }
332
+
312
333
  /**
313
334
  * Pick data column subnets to subscribe to post-fulu.
314
335
  */
315
336
  export function getDataColumnSidecarTopics(
316
- networkConfig: NetworkConfig
337
+ networkConfig: NetworkConfig,
338
+ subscribeAllColumnSubnets = false
317
339
  ): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
318
340
  const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [];
319
341
 
320
- const subnets = networkConfig.custodyConfig.sampledSubnets;
342
+ const subnets = subscribeAllColumnSubnets
343
+ ? Array.from({length: networkConfig.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT}, (_, i) => i)
344
+ : networkConfig.custodyConfig.sampledSubnets;
321
345
  for (const subnet of subnets) {
322
346
  topics.push({type: GossipType.data_column_sidecar, subnet});
323
347
  }
@@ -165,6 +165,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
165
165
  // tracked in https://github.com/ChainSafe/lodestar/issues/7957
166
166
 
167
167
  const logCtx = {
168
+ slot,
168
169
  currentSlot: chain.clock.currentSlot,
169
170
  peerId: peerIdStr,
170
171
  delaySec,
@@ -184,20 +185,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
184
185
  seenTimestampSec,
185
186
  peerIdStr,
186
187
  });
188
+
189
+ // Optimistically seed the payload-envelope cache too, mirroring seenBlockInputCache above.
190
+ // This ensures we have PayloadEnvelopeInput, even through "PARENT_UNKNOWN" error
191
+ // see https://github.com/ChainSafe/lodestar/issues/9475
192
+ if (isForkPostGloas(fork)) {
193
+ chain.seenPayloadEnvelopeInputCache.add({
194
+ blockRootHex,
195
+ block: signedBlock as SignedBeaconBlock<ForkPostGloas>,
196
+ forkName: fork,
197
+ sampledColumns: chain.custodyConfig.sampledColumns,
198
+ custodyColumns: chain.custodyConfig.custodyColumns,
199
+ timeCreatedSec: seenTimestampSec,
200
+ });
201
+ }
202
+
187
203
  try {
188
204
  const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork);
189
205
 
190
- if (isForkPostGloas(fork)) {
191
- chain.seenPayloadEnvelopeInputCache.add({
192
- blockRootHex,
193
- block: signedBlock as SignedBeaconBlock<ForkPostGloas>,
194
- forkName: fork,
195
- sampledColumns: chain.custodyConfig.sampledColumns,
196
- custodyColumns: chain.custodyConfig.custodyColumns,
197
- timeCreatedSec: seenTimestampSec,
198
- });
199
- }
200
-
201
206
  const blockInputMeta = blockInput.getLogMeta();
202
207
 
203
208
  const recvToValidation = Date.now() / 1000 - seenTimestampSec;
@@ -234,12 +239,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
234
239
  throw e;
235
240
  }
236
241
 
237
- if (e.action === GossipAction.REJECT) {
238
- chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`);
242
+ // IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated.
243
+ // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT
244
+ // (provably invalid) and unexpected errors prune below.
245
+ if (e.action === GossipAction.IGNORE) {
246
+ throw e;
239
247
  }
248
+
249
+ chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`);
240
250
  }
241
251
 
252
+ // REJECT or unexpected (non-BlockGossipError) error: drop the optimistically-added entries from
253
+ // both caches, keeping them consistent.
242
254
  chain.seenBlockInputCache.prune(blockRootHex);
255
+ if (isForkPostGloas(fork)) {
256
+ chain.seenPayloadEnvelopeInputCache.prune(blockRootHex);
257
+ }
243
258
  throw e;
244
259
  }
245
260
  }
@@ -1127,9 +1142,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1127
1142
  const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
1128
1143
 
1129
1144
  logger.debug("Received gossip payload envelope", {
1145
+ slot,
1130
1146
  currentSlot: chain.clock.currentSlot,
1131
1147
  peerId: peerIdStr,
1132
- slot,
1133
1148
  blockRoot: toRootHex(envelope.beaconBlockRoot),
1134
1149
  delaySec,
1135
1150
  });
@@ -1258,9 +1273,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1258
1273
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
1259
1274
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
1260
1275
 
1261
- chain.proposerPreferencesPool.add(signedProposerPreferences);
1262
1276
  chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1263
- version: ForkName.gloas,
1277
+ version: config.getForkName(signedProposerPreferences.message.proposalSlot),
1264
1278
  data: signedProposerPreferences,
1265
1279
  });
1266
1280
  },