@lodestar/beacon-node 1.43.0 → 1.44.0-dev.1d0e0b9081

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 (83) hide show
  1. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/pool/index.js +46 -5
  3. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +26 -12
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/chain.d.ts +2 -1
  8. package/lib/chain/chain.d.ts.map +1 -1
  9. package/lib/chain/chain.js +3 -1
  10. package/lib/chain/chain.js.map +1 -1
  11. package/lib/chain/errors/executionPayloadBid.d.ts +19 -1
  12. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  13. package/lib/chain/errors/executionPayloadBid.js +3 -0
  14. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  15. package/lib/chain/interface.d.ts +2 -1
  16. package/lib/chain/interface.d.ts.map +1 -1
  17. package/lib/chain/interface.js.map +1 -1
  18. package/lib/chain/lightClient/index.d.ts.map +1 -1
  19. package/lib/chain/lightClient/index.js +1 -1
  20. package/lib/chain/lightClient/index.js.map +1 -1
  21. package/lib/chain/opPools/index.d.ts +1 -0
  22. package/lib/chain/opPools/index.d.ts.map +1 -1
  23. package/lib/chain/opPools/index.js +1 -0
  24. package/lib/chain/opPools/index.js.map +1 -1
  25. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  26. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  27. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  28. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  29. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  30. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  31. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  32. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  33. package/lib/chain/produceBlock/produceBlockBody.d.ts +4 -0
  34. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  35. package/lib/chain/produceBlock/produceBlockBody.js +36 -1
  36. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  37. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  38. package/lib/chain/validation/executionPayloadBid.js +65 -17
  39. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  40. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  41. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  42. package/lib/chain/validation/payloadAttestationMessage.js +5 -3
  43. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  44. package/lib/execution/engine/interface.d.ts +1 -0
  45. package/lib/execution/engine/interface.d.ts.map +1 -1
  46. package/lib/execution/engine/types.d.ts +2 -0
  47. package/lib/execution/engine/types.d.ts.map +1 -1
  48. package/lib/execution/engine/types.js +2 -0
  49. package/lib/execution/engine/types.js.map +1 -1
  50. package/lib/network/gossip/topic.d.ts +20 -767
  51. package/lib/network/gossip/topic.d.ts.map +1 -1
  52. package/lib/network/interface.d.ts +1 -0
  53. package/lib/network/interface.d.ts.map +1 -1
  54. package/lib/network/network.d.ts +1 -0
  55. package/lib/network/network.d.ts.map +1 -1
  56. package/lib/network/network.js +5 -0
  57. package/lib/network/network.js.map +1 -1
  58. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  59. package/lib/network/processor/gossipHandlers.js +8 -3
  60. package/lib/network/processor/gossipHandlers.js.map +1 -1
  61. package/lib/util/dependentRoot.d.ts +6 -2
  62. package/lib/util/dependentRoot.d.ts.map +1 -1
  63. package/lib/util/dependentRoot.js +20 -16
  64. package/lib/util/dependentRoot.js.map +1 -1
  65. package/package.json +14 -15
  66. package/src/api/impl/beacon/pool/index.ts +56 -3
  67. package/src/api/impl/validator/index.ts +28 -12
  68. package/src/chain/chain.ts +3 -0
  69. package/src/chain/errors/executionPayloadBid.ts +22 -1
  70. package/src/chain/interface.ts +2 -0
  71. package/src/chain/lightClient/index.ts +6 -6
  72. package/src/chain/opPools/index.ts +1 -0
  73. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  74. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  75. package/src/chain/produceBlock/produceBlockBody.ts +59 -0
  76. package/src/chain/validation/executionPayloadBid.ts +68 -18
  77. package/src/chain/validation/payloadAttestationMessage.ts +6 -4
  78. package/src/execution/engine/interface.ts +1 -0
  79. package/src/execution/engine/types.ts +4 -0
  80. package/src/network/interface.ts +1 -0
  81. package/src/network/network.ts +11 -0
  82. package/src/network/processor/gossipHandlers.ts +8 -2
  83. package/src/util/dependentRoot.ts +22 -18
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.43.0",
14
+ "version": "1.44.0-dev.1d0e0b9081",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -135,18 +135,17 @@
135
135
  "@libp2p/peer-id": "^6.0.4",
136
136
  "@libp2p/prometheus-metrics": "^5.0.14",
137
137
  "@libp2p/tcp": "^11.0.13",
138
- "@lodestar/api": "^1.43.0",
139
- "@lodestar/config": "^1.43.0",
140
- "@lodestar/db": "^1.43.0",
141
- "@lodestar/fork-choice": "^1.43.0",
142
- "@lodestar/light-client": "^1.43.0",
143
- "@lodestar/logger": "^1.43.0",
144
- "@lodestar/params": "^1.43.0",
145
- "@lodestar/reqresp": "^1.43.0",
146
- "@lodestar/state-transition": "^1.43.0",
147
- "@lodestar/types": "^1.43.0",
148
- "@lodestar/utils": "^1.43.0",
149
- "@lodestar/validator": "^1.43.0",
138
+ "@lodestar/api": "^1.44.0-dev.1d0e0b9081",
139
+ "@lodestar/config": "^1.44.0-dev.1d0e0b9081",
140
+ "@lodestar/db": "^1.44.0-dev.1d0e0b9081",
141
+ "@lodestar/fork-choice": "^1.44.0-dev.1d0e0b9081",
142
+ "@lodestar/logger": "^1.44.0-dev.1d0e0b9081",
143
+ "@lodestar/params": "^1.44.0-dev.1d0e0b9081",
144
+ "@lodestar/reqresp": "^1.44.0-dev.1d0e0b9081",
145
+ "@lodestar/state-transition": "^1.44.0-dev.1d0e0b9081",
146
+ "@lodestar/types": "^1.44.0-dev.1d0e0b9081",
147
+ "@lodestar/utils": "^1.44.0-dev.1d0e0b9081",
148
+ "@lodestar/validator": "^1.44.0-dev.1d0e0b9081",
150
149
  "@multiformats/multiaddr": "^13.0.1",
151
150
  "datastore-core": "^11.0.2",
152
151
  "datastore-fs": "^11.0.2",
@@ -169,7 +168,7 @@
169
168
  "@libp2p/interface-internal": "^3.0.13",
170
169
  "@libp2p/logger": "^6.2.2",
171
170
  "@libp2p/utils": "^7.0.13",
172
- "@lodestar/spec-test-util": "^1.43.0",
171
+ "@lodestar/spec-test-util": "^1.44.0-dev.1d0e0b9081",
173
172
  "@types/js-yaml": "^4.0.5",
174
173
  "@types/qs": "^6.9.7",
175
174
  "@types/tmp": "^0.2.3",
@@ -187,5 +186,5 @@
187
186
  "beacon",
188
187
  "blockchain"
189
188
  ],
190
- "gitHead": "5cb87b7632ff76f9f93947147967618b26da2d0c"
189
+ "gitHead": "7bc209ff709ebfda1ad16a12aa7f0175c5767ae8"
191
190
  }
@@ -1,6 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ApplicationMethods} from "@lodestar/api/server";
3
3
  import {
4
+ ForkName,
4
5
  ForkPostElectra,
5
6
  ForkPreElectra,
6
7
  SYNC_COMMITTEE_SUBNET_SIZE,
@@ -16,12 +17,15 @@ import {
16
17
  GossipAction,
17
18
  PayloadAttestationError,
18
19
  PayloadAttestationErrorCode,
20
+ ProposerPreferencesError,
21
+ ProposerPreferencesErrorCode,
19
22
  SyncCommitteeError,
20
23
  } from "../../../../chain/errors/index.js";
21
24
  import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
22
25
  import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
23
26
  import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
24
27
  import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js";
28
+ import {validateGossipProposerPreferences} from "../../../../chain/validation/proposerPreferences.js";
25
29
  import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
26
30
  import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
27
31
  import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
@@ -81,6 +85,55 @@ export function getBeaconPoolApi({
81
85
  return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}};
82
86
  },
83
87
 
88
+ async getPoolProposerPreferences({slot}) {
89
+ const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot);
90
+ if (!isForkPostGloas(fork)) {
91
+ throw new ApiError(400, `Proposer preferences pool is not supported before Gloas fork=${fork}`);
92
+ }
93
+
94
+ return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}};
95
+ },
96
+
97
+ async submitSignedProposerPreferences({signedProposerPreferences}) {
98
+ const failures: FailureList = [];
99
+
100
+ await Promise.all(
101
+ signedProposerPreferences.map(async (signed, i) => {
102
+ try {
103
+ await validateGossipProposerPreferences(chain, signed);
104
+
105
+ chain.proposerPreferencesPool.add(signed);
106
+ await network.publishProposerPreferences(signed);
107
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
108
+ version: ForkName.gloas,
109
+ data: signed,
110
+ });
111
+ } catch (e) {
112
+ const logCtx = {
113
+ slot: signed.message.proposalSlot,
114
+ validatorIndex: signed.message.validatorIndex,
115
+ dependentRoot: toRootHex(signed.message.dependentRoot),
116
+ };
117
+
118
+ if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
119
+ logger.debug("Ignoring known signed proposer preferences", logCtx);
120
+ return;
121
+ }
122
+
123
+ failures.push({index: i, message: (e as Error).message});
124
+ logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, e as Error);
125
+ if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
126
+ chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
127
+ }
128
+ }
129
+ })
130
+ );
131
+
132
+ if (failures.length > 0) {
133
+ throw new IndexedError("Error processing signed proposer preferences", failures);
134
+ }
135
+ },
136
+
84
137
  async getPoolAttesterSlashings() {
85
138
  const fork = chain.config.getForkName(chain.clock.currentSlot);
86
139
 
@@ -258,7 +311,7 @@ export function getBeaconPoolApi({
258
311
  try {
259
312
  const validateFn = () => validateApiPayloadAttestationMessage(chain, payloadAttestationMessage);
260
313
  const {slot, beaconBlockRoot} = payloadAttestationMessage.data;
261
- const {attDataRootHex, validatorCommitteeIndex} = await validateGossipFnRetryUnknownRoot(
314
+ const {attDataRootHex, validatorCommitteeIndices} = await validateGossipFnRetryUnknownRoot(
262
315
  validateFn,
263
316
  network,
264
317
  chain,
@@ -269,13 +322,13 @@ export function getBeaconPoolApi({
269
322
  const insertOutcome = chain.payloadAttestationPool.add(
270
323
  payloadAttestationMessage,
271
324
  attDataRootHex,
272
- validatorCommitteeIndex
325
+ validatorCommitteeIndices
273
326
  );
274
327
  metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome});
275
328
 
276
329
  chain.forkChoice.notifyPtcMessages(
277
330
  toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
278
- [validatorCommitteeIndex],
331
+ validatorCommitteeIndices,
279
332
  payloadAttestationMessage.data.payloadPresent
280
333
  );
281
334
 
@@ -14,6 +14,7 @@ import {
14
14
  isForkPostBellatrix,
15
15
  isForkPostDeneb,
16
16
  isForkPostElectra,
17
+ isForkPostFulu,
17
18
  isForkPostGloas,
18
19
  } from "@lodestar/params";
19
20
  import {
@@ -925,7 +926,7 @@ export function getValidatorApi(
925
926
  metrics?.blockProductionRequests.inc({source});
926
927
 
927
928
  const graffitiBytes = toGraffitiBytes(
928
- graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {})
929
+ graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts)
929
930
  );
930
931
  const commonBlockBodyPromise = chain.produceCommonBlockBody({
931
932
  slot,
@@ -1066,7 +1067,15 @@ export function getValidatorApi(
1066
1067
 
1067
1068
  const blockIsForSlot = block.slot === slot;
1068
1069
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
1069
- const payloadPresent = blockIsForSlot && (payloadInput?.hasPayloadEnvelope() ?? false);
1070
+ // Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
1071
+ // into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not
1072
+ // the input's creation time.
1073
+ const payloadDueSec = config.getPayloadDueMs() / 1000;
1074
+ const payloadPresent =
1075
+ blockIsForSlot &&
1076
+ payloadInput !== undefined &&
1077
+ payloadInput.hasPayloadEnvelope() &&
1078
+ chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec;
1070
1079
  const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false);
1071
1080
 
1072
1081
  return {
@@ -1124,26 +1133,33 @@ export function getValidatorApi(
1124
1133
  async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
1125
1134
  notWhileSyncing();
1126
1135
 
1127
- // Early check that epoch is no more than current_epoch + 1, or allow for pre-genesis
1128
1136
  const currentEpoch = currentEpochWithDisparity();
1129
1137
  const nextEpoch = currentEpoch + 1;
1130
- if (currentEpoch >= 0 && epoch > nextEpoch) {
1138
+ const startSlot = computeStartSlotAtEpoch(epoch);
1139
+ const prepareNextSlotLookAheadMs =
1140
+ config.SLOT_DURATION_MS - config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS);
1141
+ const toNextEpochMs = msToNextEpoch();
1142
+ const nearNextEpoch = toNextEpochMs < prepareNextSlotLookAheadMs;
1143
+ // Post-Fulu the proposer lookahead is deterministic and known a full epoch ahead, so
1144
+ // close to the boundary `currentEpoch + 2` is serveable from the upcoming-epoch
1145
+ // checkpoint state (its `nextProposers`). Pre-Fulu / mid-epoch: `currentEpoch + 1` max.
1146
+ const isPostFulu = isForkPostFulu(config.getForkName(startSlot));
1147
+ const maxFutureEpoch = isPostFulu && nearNextEpoch && opts?.v2 ? nextEpoch + 1 : nextEpoch;
1148
+ if (currentEpoch >= 0 && epoch > maxFutureEpoch) {
1131
1149
  throw new ApiError(400, `Requested epoch ${epoch} must not be more than one epoch in the future`);
1132
1150
  }
1133
1151
 
1134
1152
  const head = chain.forkChoice.getHead();
1135
1153
  let state: IBeaconStateView | undefined = undefined;
1136
- const startSlot = computeStartSlotAtEpoch(epoch);
1137
- const prepareNextSlotLookAheadMs =
1138
- config.SLOT_DURATION_MS - config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS);
1139
- const toNextEpochMs = msToNextEpoch();
1140
1154
  // validators may request next epoch's duties when it's close to next epoch
1141
- // this is to avoid missed block proposal due to 0 epoch look ahead
1142
- if (epoch === nextEpoch && toNextEpochMs < prepareNextSlotLookAheadMs) {
1155
+ // this is to avoid missed block proposal due to 0 epoch look ahead.
1156
+ // Post-Fulu, `nextEpoch + 1` is served from the same upcoming-epoch (`nextEpoch`)
1157
+ // checkpoint state via its `nextProposers` (deterministic proposer lookahead).
1158
+ if (nearNextEpoch && (epoch === nextEpoch || (isPostFulu && epoch === nextEpoch + 1))) {
1143
1159
  // wait for maximum 1 slot for cp state which is the timeout of validator api
1144
1160
  const cpState = await waitForCheckpointState({
1145
1161
  rootHex: head.blockRoot,
1146
- epoch,
1162
+ epoch: nextEpoch,
1147
1163
  });
1148
1164
  if (cpState) {
1149
1165
  state = cpState;
@@ -1218,7 +1234,7 @@ export function getValidatorApi(
1218
1234
  // It should be set to the latest block applied to `self` or the genesis block root.
1219
1235
  const dependentRoot =
1220
1236
  // In v2 the dependent root is different after fulu due to deterministic proposer lookahead
1221
- proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state) ||
1237
+ proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state, epoch) ||
1222
1238
  (await getGenesisBlockRoot(state));
1223
1239
 
1224
1240
  return {
@@ -88,6 +88,7 @@ import {
88
88
  ExecutionPayloadBidPool,
89
89
  OpPool,
90
90
  PayloadAttestationPool,
91
+ ProposerPreferencesPool,
91
92
  SyncCommitteeMessagePool,
92
93
  SyncContributionAndProofPool,
93
94
  } from "./opPools/index.js";
@@ -180,6 +181,7 @@ export class BeaconChain implements IBeaconChain {
180
181
  readonly syncContributionAndProofPool;
181
182
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;
182
183
  readonly payloadAttestationPool: PayloadAttestationPool;
184
+ readonly proposerPreferencesPool = new ProposerPreferencesPool();
183
185
  readonly opPool: OpPool;
184
186
 
185
187
  // Gossip seen cache
@@ -1462,6 +1464,7 @@ export class BeaconChain implements IBeaconChain {
1462
1464
  this.executionPayloadBidPool.prune(slot);
1463
1465
  this.seenExecutionPayloadBids.prune(slot);
1464
1466
  this.seenProposerPreferences.prune(slot);
1467
+ this.proposerPreferencesPool.prune(slot);
1465
1468
  this.seenAttestationDatas.onSlot(slot);
1466
1469
  this.reprocessController.onSlot(slot);
1467
1470
 
@@ -11,6 +11,9 @@ export enum ExecutionPayloadBidErrorCode {
11
11
  UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
12
12
  INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT",
13
13
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
14
+ NO_MATCHING_PROPOSER_PREFERENCES = "EXECUTION_PAYLOAD_BID_ERROR_NO_MATCHING_PROPOSER_PREFERENCES",
15
+ PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH",
16
+ PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH",
14
17
  }
15
18
 
16
19
  export type ExecutionPayloadBidErrorType =
@@ -36,6 +39,24 @@ export type ExecutionPayloadBidErrorType =
36
39
  }
37
40
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
38
41
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
39
- | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot};
42
+ | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}
43
+ | {
44
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES;
45
+ slot: Slot;
46
+ parentBlockRoot: RootHex;
47
+ dependentRoot: RootHex;
48
+ }
49
+ | {
50
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH;
51
+ builderIndex: BuilderIndex;
52
+ bidFeeRecipient: string;
53
+ expectedFeeRecipient: string;
54
+ }
55
+ | {
56
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH;
57
+ builderIndex: BuilderIndex;
58
+ bidGasLimit: number;
59
+ expectedGasLimit: number;
60
+ };
40
61
 
41
62
  export class ExecutionPayloadBidError extends GossipActionError<ExecutionPayloadBidErrorType> {}
@@ -47,6 +47,7 @@ import {
47
47
  ExecutionPayloadBidPool,
48
48
  OpPool,
49
49
  PayloadAttestationPool,
50
+ ProposerPreferencesPool,
50
51
  SyncCommitteeMessagePool,
51
52
  SyncContributionAndProofPool,
52
53
  } from "./opPools/index.js";
@@ -124,6 +125,7 @@ export interface IBeaconChain {
124
125
  readonly syncContributionAndProofPool: SyncContributionAndProofPool;
125
126
  readonly executionPayloadBidPool: ExecutionPayloadBidPool;
126
127
  readonly payloadAttestationPool: PayloadAttestationPool;
128
+ readonly proposerPreferencesPool: ProposerPreferencesPool;
127
129
  readonly opPool: OpPool;
128
130
 
129
131
  // Gossip seen cache
@@ -1,12 +1,6 @@
1
1
  import {BitArray} from "@chainsafe/ssz";
2
2
  import {routes} from "@lodestar/api";
3
3
  import {ChainForkConfig} from "@lodestar/config";
4
- import {
5
- LightClientUpdateSummary,
6
- isBetterUpdate,
7
- toLightClientUpdateSummary,
8
- upgradeLightClientHeader,
9
- } from "@lodestar/light-client/spec";
10
4
  import {
11
5
  ForkName,
12
6
  ForkPostAltair,
@@ -27,6 +21,12 @@ import {
27
21
  computeSyncPeriodAtSlot,
28
22
  executionPayloadToPayloadHeader,
29
23
  } from "@lodestar/state-transition";
24
+ import {
25
+ LightClientUpdateSummary,
26
+ isBetterUpdate,
27
+ toLightClientUpdateSummary,
28
+ upgradeLightClientHeader,
29
+ } from "@lodestar/state-transition/light-client";
30
30
  import {
31
31
  BeaconBlock,
32
32
  BeaconBlockBody,
@@ -3,5 +3,6 @@ export {AttestationPool} from "./attestationPool.js";
3
3
  export {ExecutionPayloadBidPool} from "./executionPayloadBidPool.js";
4
4
  export {OpPool} from "./opPool.js";
5
5
  export {PayloadAttestationPool} from "./payloadAttestationPool.js";
6
+ export {ProposerPreferencesPool} from "./proposerPreferencesPool.js";
6
7
  export {SyncCommitteeMessagePool} from "./syncCommitteeMessagePool.js";
7
8
  export {SyncContributionAndProofPool} from "./syncContributionAndProofPool.js";
@@ -57,7 +57,7 @@ export class PayloadAttestationPool {
57
57
  add(
58
58
  message: gloas.PayloadAttestationMessage,
59
59
  payloadAttDataRootHex: RootHex,
60
- validatorCommitteeIndex: number
60
+ validatorCommitteeIndices: number[]
61
61
  ): InsertOutcome {
62
62
  const slot = message.data.slot;
63
63
  const lowestPermissibleSlot = this.lowestPermissibleSlot;
@@ -85,10 +85,10 @@ export class PayloadAttestationPool {
85
85
  const aggregate = aggregateByDataRoot.get(payloadAttDataRootHex);
86
86
  if (aggregate) {
87
87
  // Aggregate msg into aggregate
88
- return aggregateMessageInto(message, validatorCommitteeIndex, aggregate);
88
+ return aggregateMessageInto(message, validatorCommitteeIndices, aggregate);
89
89
  }
90
90
  // Create a new aggregate with data
91
- aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndex));
91
+ aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndices));
92
92
 
93
93
  return InsertOutcome.NewData;
94
94
  }
@@ -150,25 +150,49 @@ export class PayloadAttestationPool {
150
150
  }
151
151
  }
152
152
 
153
- function messageToAggregate(message: gloas.PayloadAttestationMessage, validatorCommitteeIndex: number): AggregateFast {
153
+ function messageToAggregate(
154
+ message: gloas.PayloadAttestationMessage,
155
+ validatorCommitteeIndices: number[]
156
+ ): AggregateFast {
157
+ const aggregationBits = BitArray.fromBitLen(PTC_SIZE);
158
+ for (const index of validatorCommitteeIndices) {
159
+ aggregationBits.set(index, true);
160
+ }
161
+ const sig = signatureFromBytesNoCheck(message.signature);
162
+ // The validator signed once but occupies `validatorCommitteeIndices.length` PTC positions.
163
+ // Verification aggregates the pubkey once per set bit, so the signature must be aggregated
164
+ // the same number of times for the BLS check to balance — same pattern as sync committee.
165
+ const signature =
166
+ validatorCommitteeIndices.length === 1
167
+ ? sig
168
+ : aggregateSignatures(new Array(validatorCommitteeIndices.length).fill(sig));
154
169
  return {
155
- aggregationBits: BitArray.fromSingleBit(PTC_SIZE, validatorCommitteeIndex),
170
+ aggregationBits,
156
171
  data: message.data,
157
- signature: signatureFromBytesNoCheck(message.signature),
172
+ signature,
158
173
  };
159
174
  }
160
175
 
161
176
  function aggregateMessageInto(
162
177
  message: gloas.PayloadAttestationMessage,
163
- validatorCommitteeIndex: number,
178
+ validatorCommitteeIndices: number[],
164
179
  aggregate: AggregateFast
165
180
  ): InsertOutcome {
166
- if (aggregate.aggregationBits.get(validatorCommitteeIndex) === true) {
181
+ // Gossip dedup via `seenPayloadAttesters` is keyed by (epoch, validatorIndex), so the same
182
+ // validator's message is never processed twice — all of its bits are set together or none.
183
+ // Checking the first index is sufficient.
184
+ if (aggregate.aggregationBits.get(validatorCommitteeIndices[0]) === true) {
167
185
  return InsertOutcome.AlreadyKnown;
168
186
  }
169
187
 
170
- aggregate.aggregationBits.set(validatorCommitteeIndex, true);
171
- aggregate.signature = aggregateSignatures([aggregate.signature, signatureFromBytesNoCheck(message.signature)]);
188
+ for (const index of validatorCommitteeIndices) {
189
+ aggregate.aggregationBits.set(index, true);
190
+ }
191
+ const sig = signatureFromBytesNoCheck(message.signature);
192
+ aggregate.signature = aggregateSignatures([
193
+ aggregate.signature,
194
+ ...new Array(validatorCommitteeIndices.length).fill(sig),
195
+ ]);
172
196
 
173
197
  return InsertOutcome.Aggregated;
174
198
  }
@@ -0,0 +1,59 @@
1
+ import {RootHex, Slot, gloas} from "@lodestar/types";
2
+ import {toRootHex} from "@lodestar/utils";
3
+
4
+ /**
5
+ * Pool of validated `SignedProposerPreferences` indexed by `(slot, dependent_root)`.
6
+ *
7
+ * The primary consumer is `validateExecutionPayloadBid`, which looks up the matching
8
+ * preferences via `get(bid.slot, dependent_root)` to enforce the IGNORE-existence and
9
+ * REJECT-equality rules from the gloas spec. The beacon API `/pool/proposer_preferences`
10
+ * GET endpoint reads from the same pool via `getAll`.
11
+ *
12
+ * `validator_index` is intentionally not part of the key: gossip validation enforces
13
+ * `proposers[proposalSlot % SLOTS_PER_EPOCH] === validatorIndex` against the shuffling
14
+ * implied by `dependent_root`, so once a preference has been validated `(slot, dependent_root)`
15
+ * already pins down the validator.
16
+ */
17
+ export class ProposerPreferencesPool {
18
+ private readonly bySlot = new Map<Slot, Map<RootHex, gloas.SignedProposerPreferences>>();
19
+
20
+ /** Lookup for bid validation: matches `(bid.slot, get_proposer_dependent_root(parent_state, ...))`. */
21
+ get(slot: Slot, dependentRootHex: RootHex): gloas.SignedProposerPreferences | null {
22
+ return this.bySlot.get(slot)?.get(dependentRootHex) ?? null;
23
+ }
24
+
25
+ add(signed: gloas.SignedProposerPreferences): void {
26
+ const {proposalSlot, dependentRoot} = signed.message;
27
+ const rootHex = toRootHex(dependentRoot);
28
+ let byRoot = this.bySlot.get(proposalSlot);
29
+ if (!byRoot) {
30
+ byRoot = new Map();
31
+ this.bySlot.set(proposalSlot, byRoot);
32
+ }
33
+ byRoot.set(rootHex, signed);
34
+ }
35
+
36
+ /** API read-out: flatten across branches, optionally filtered by slot. */
37
+ getAll(slot?: Slot): gloas.SignedProposerPreferences[] {
38
+ if (slot !== undefined) {
39
+ const byRoot = this.bySlot.get(slot);
40
+ return byRoot ? Array.from(byRoot.values()) : [];
41
+ }
42
+ const out: gloas.SignedProposerPreferences[] = [];
43
+ for (const byRoot of this.bySlot.values()) {
44
+ for (const v of byRoot.values()) out.push(v);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Entries are only load-bearing while `proposal_slot >= current_slot`. Once the slot has
51
+ * passed the `[IGNORE] proposal_slot > current_slot` gossip rule takes over, so drop them
52
+ * on each slot tick.
53
+ */
54
+ prune(currentSlot: Slot): void {
55
+ for (const slot of this.bySlot.keys()) {
56
+ if (slot < currentSlot) this.bySlot.delete(slot);
57
+ }
58
+ }
59
+ }
@@ -18,6 +18,8 @@ import {
18
18
  G2_POINT_AT_INFINITY,
19
19
  IBeaconStateView,
20
20
  type IBeaconStateViewBellatrix,
21
+ type IBeaconStateViewGloas,
22
+ computeEpochAtSlot,
21
23
  computeTimeAtSlot,
22
24
  isStatePostBellatrix,
23
25
  isStatePostCapella,
@@ -58,10 +60,12 @@ import {
58
60
  PayloadId,
59
61
  getExpectedGasLimit,
60
62
  } from "../../execution/index.js";
63
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
61
64
  import {fromGraffitiBytes} from "../../util/graffiti.js";
62
65
  import {kzg} from "../../util/kzg.js";
63
66
  import type {BeaconChain} from "../chain.js";
64
67
  import {CommonBlockBody} from "../interface.js";
68
+ import {ProposerPreferencesPool} from "../opPools/index.js";
65
69
  import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
66
70
 
67
71
  // Time to provide the EL to generate a payload from new payload id
@@ -204,6 +208,9 @@ export async function produceBlockBody<T extends BlockType>(
204
208
  // this into a completely separate function and have pre/post gloas more separated
205
209
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
206
210
  const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
211
+ // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
212
+ // ProposerPreferencesPool. Consider using this unified cache instead
213
+ // see https://github.com/ChainSafe/lodestar/issues/9379
207
214
  const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
208
215
 
209
216
  const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
@@ -633,6 +640,8 @@ export async function prepareExecutionPayload(
633
640
  chain: {
634
641
  executionEngine: IExecutionEngine;
635
642
  config: ChainForkConfig;
643
+ forkChoice: IForkChoice;
644
+ proposerPreferencesPool: ProposerPreferencesPool;
636
645
  },
637
646
  logger: Logger,
638
647
  fork: ForkPostBellatrix,
@@ -733,6 +742,7 @@ export function getPayloadAttributesForSSE(
733
742
  chain: {
734
743
  config: ChainForkConfig;
735
744
  forkChoice: IForkChoice;
745
+ proposerPreferencesPool: ProposerPreferencesPool;
736
746
  },
737
747
  {
738
748
  prepareState,
@@ -789,6 +799,8 @@ function preparePayloadAttributes(
789
799
  fork: ForkPostBellatrix,
790
800
  chain: {
791
801
  config: ChainForkConfig;
802
+ forkChoice: IForkChoice;
803
+ proposerPreferencesPool: ProposerPreferencesPool;
792
804
  },
793
805
  {
794
806
  prepareState,
@@ -851,12 +863,59 @@ function preparePayloadAttributes(
851
863
  }
852
864
 
853
865
  if (ForkSeq[fork] >= ForkSeq.gloas) {
866
+ if (!isStatePostGloas(prepareState)) {
867
+ throw new Error("Expected Gloas state for Gloas payload attributes");
868
+ }
854
869
  (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
870
+ (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit(
871
+ chain,
872
+ prepareState,
873
+ prepareSlot,
874
+ parentBlockRoot
875
+ );
855
876
  }
856
877
 
857
878
  return payloadAttributes;
858
879
  }
859
880
 
881
+ /**
882
+ * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4`
883
+ * `targetGasLimit` field (consensus-specs#5235, execution-apis#796).
884
+ *
885
+ * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool
886
+ * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching
887
+ * preferences are pooled, target the parent payload's gas limit so the gas limit stays
888
+ * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`).
889
+ */
890
+ function getProposerTargetGasLimit(
891
+ chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool},
892
+ state: IBeaconStateViewGloas,
893
+ prepareSlot: Slot,
894
+ parentBlockRoot: Root
895
+ ): number {
896
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(parentBlockRoot));
897
+ const dependentRootHex = (() => {
898
+ if (parentBlock === null) {
899
+ return null;
900
+ }
901
+ try {
902
+ return getShufflingDependentRoot(
903
+ chain.forkChoice,
904
+ computeEpochAtSlot(prepareSlot),
905
+ computeEpochAtSlot(parentBlock.slot),
906
+ parentBlock
907
+ );
908
+ } catch {
909
+ return null;
910
+ }
911
+ })();
912
+
913
+ const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null;
914
+ // TODO GLOAS: state.latestExecutionPayloadBid is the latest *bid*, not the latest *executed*
915
+ // payload — for EMPTY parents this drifts. Consider having a default value like Prysm's DefaultBuilderGasLimit.
916
+ return Number(pref ? pref.message.targetGasLimit : state.latestExecutionPayloadBid.gasLimit);
917
+ }
918
+
860
919
  export async function produceCommonBlockBody<T extends BlockType>(
861
920
  this: BeaconChain,
862
921
  blockType: T,