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

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 (111) 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/blocks/importExecutionPayload.d.ts.map +1 -1
  8. package/lib/chain/blocks/importExecutionPayload.js +4 -2
  9. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  10. package/lib/chain/chain.d.ts +2 -1
  11. package/lib/chain/chain.d.ts.map +1 -1
  12. package/lib/chain/chain.js +3 -1
  13. package/lib/chain/chain.js.map +1 -1
  14. package/lib/chain/errors/executionPayloadBid.d.ts +24 -1
  15. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  16. package/lib/chain/errors/executionPayloadBid.js +4 -0
  17. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  18. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  19. package/lib/chain/forkChoice/index.js +14 -4
  20. package/lib/chain/forkChoice/index.js.map +1 -1
  21. package/lib/chain/interface.d.ts +2 -1
  22. package/lib/chain/interface.d.ts.map +1 -1
  23. package/lib/chain/interface.js.map +1 -1
  24. package/lib/chain/lightClient/index.d.ts.map +1 -1
  25. package/lib/chain/lightClient/index.js +1 -1
  26. package/lib/chain/lightClient/index.js.map +1 -1
  27. package/lib/chain/opPools/index.d.ts +1 -0
  28. package/lib/chain/opPools/index.d.ts.map +1 -1
  29. package/lib/chain/opPools/index.js +1 -0
  30. package/lib/chain/opPools/index.js.map +1 -1
  31. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  32. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  33. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  34. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  35. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  36. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  37. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  38. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  39. package/lib/chain/produceBlock/produceBlockBody.d.ts +4 -0
  40. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  41. package/lib/chain/produceBlock/produceBlockBody.js +48 -2
  42. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  43. package/lib/chain/validation/executionPayloadBid.d.ts +7 -3
  44. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  45. package/lib/chain/validation/executionPayloadBid.js +85 -21
  46. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  47. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  48. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  49. package/lib/chain/validation/payloadAttestationMessage.js +5 -3
  50. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  51. package/lib/chain/validatorMonitor.d.ts +1 -0
  52. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  53. package/lib/chain/validatorMonitor.js +16 -0
  54. package/lib/chain/validatorMonitor.js.map +1 -1
  55. package/lib/execution/builder/index.d.ts +1 -2
  56. package/lib/execution/builder/index.d.ts.map +1 -1
  57. package/lib/execution/builder/index.js +0 -1
  58. package/lib/execution/builder/index.js.map +1 -1
  59. package/lib/execution/engine/interface.d.ts +1 -0
  60. package/lib/execution/engine/interface.d.ts.map +1 -1
  61. package/lib/execution/engine/types.d.ts +2 -0
  62. package/lib/execution/engine/types.d.ts.map +1 -1
  63. package/lib/execution/engine/types.js +2 -0
  64. package/lib/execution/engine/types.js.map +1 -1
  65. package/lib/metrics/metrics/lodestar.d.ts +1 -1
  66. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  67. package/lib/metrics/metrics/lodestar.js +4 -3
  68. package/lib/metrics/metrics/lodestar.js.map +1 -1
  69. package/lib/network/gossip/topic.d.ts +1 -1
  70. package/lib/network/interface.d.ts +1 -0
  71. package/lib/network/interface.d.ts.map +1 -1
  72. package/lib/network/network.d.ts +1 -0
  73. package/lib/network/network.d.ts.map +1 -1
  74. package/lib/network/network.js +5 -0
  75. package/lib/network/network.js.map +1 -1
  76. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  77. package/lib/network/processor/gossipHandlers.js +18 -5
  78. package/lib/network/processor/gossipHandlers.js.map +1 -1
  79. package/lib/util/dependentRoot.d.ts +6 -2
  80. package/lib/util/dependentRoot.d.ts.map +1 -1
  81. package/lib/util/dependentRoot.js +20 -16
  82. package/lib/util/dependentRoot.js.map +1 -1
  83. package/package.json +14 -15
  84. package/src/api/impl/beacon/pool/index.ts +56 -3
  85. package/src/api/impl/validator/index.ts +28 -12
  86. package/src/chain/blocks/importExecutionPayload.ts +7 -1
  87. package/src/chain/chain.ts +3 -0
  88. package/src/chain/errors/executionPayloadBid.ts +25 -1
  89. package/src/chain/forkChoice/index.ts +14 -4
  90. package/src/chain/interface.ts +2 -0
  91. package/src/chain/lightClient/index.ts +6 -6
  92. package/src/chain/opPools/index.ts +1 -0
  93. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  94. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  95. package/src/chain/produceBlock/produceBlockBody.ts +75 -7
  96. package/src/chain/validation/executionPayloadBid.ts +94 -26
  97. package/src/chain/validation/payloadAttestationMessage.ts +6 -4
  98. package/src/chain/validatorMonitor.ts +18 -0
  99. package/src/execution/builder/index.ts +1 -4
  100. package/src/execution/engine/interface.ts +1 -0
  101. package/src/execution/engine/types.ts +4 -0
  102. package/src/metrics/metrics/lodestar.ts +4 -3
  103. package/src/network/interface.ts +1 -0
  104. package/src/network/network.ts +11 -0
  105. package/src/network/processor/gossipHandlers.ts +21 -4
  106. package/src/util/dependentRoot.ts +22 -18
  107. package/lib/execution/builder/utils.d.ts +0 -5
  108. package/lib/execution/builder/utils.d.ts.map +0 -1
  109. package/lib/execution/builder/utils.js +0 -17
  110. package/lib/execution/builder/utils.js.map +0 -1
  111. package/src/execution/builder/utils.ts +0 -19
@@ -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
 
@@ -9,8 +9,12 @@ export enum ExecutionPayloadBidErrorCode {
9
9
  BID_TOO_HIGH = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_HIGH",
10
10
  TOO_MANY_KZG_COMMITMENTS = "EXECUTION_PAYLOAD_BID_ERROR_TOO_MANY_KZG_COMMITMENTS",
11
11
  UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
12
+ UNKNOWN_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_PARENT_BLOCK_HASH",
12
13
  INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT",
13
14
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
15
+ NO_MATCHING_PROPOSER_PREFERENCES = "EXECUTION_PAYLOAD_BID_ERROR_NO_MATCHING_PROPOSER_PREFERENCES",
16
+ PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH",
17
+ PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH",
14
18
  }
15
19
 
16
20
  export type ExecutionPayloadBidErrorType =
@@ -35,7 +39,27 @@ export type ExecutionPayloadBidErrorType =
35
39
  commitmentLimit: number;
36
40
  }
37
41
  | {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
42
+ | {code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH; parentBlockHash: RootHex}
38
43
  | {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
39
- | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot};
44
+ | {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}
45
+ | {
46
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES;
47
+ slot: Slot;
48
+ parentBlockRoot: RootHex;
49
+ dependentRoot: RootHex;
50
+ }
51
+ | {
52
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH;
53
+ builderIndex: BuilderIndex;
54
+ bidFeeRecipient: string;
55
+ expectedFeeRecipient: string;
56
+ }
57
+ | {
58
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH;
59
+ builderIndex: BuilderIndex;
60
+ bidGasLimit: number;
61
+ parentGasLimit: number;
62
+ targetGasLimit: number;
63
+ };
40
64
 
41
65
  export class ExecutionPayloadBidError extends GossipActionError<ExecutionPayloadBidErrorType> {}
@@ -140,9 +140,11 @@ export function initializeForkChoiceFromFinalizedState(
140
140
  executionPayloadBlockHash: isStatePostGloas(state)
141
141
  ? toRootHex(state.latestBlockHash)
142
142
  : toRootHex(state.latestExecutionPayloadHeader.blockHash),
143
- // TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
144
- // latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
143
+ // TODO GLOAS: executionPayloadNumber/GasLimit are not tracked in BeaconState post-gloas
144
+ // (EIP-7732 removed latestExecutionPayloadHeader). Using 0 as unavailable fallback
145
+ // see initializeForkChoiceFromUnfinalizedState for the same caveat on validation.
145
146
  executionPayloadNumber: isStatePostGloas(state) ? 0 : state.payloadBlockNumber,
147
+ executionPayloadGasLimit: isStatePostGloas(state) ? 0 : state.latestExecutionPayloadHeader.gasLimit,
146
148
  executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing,
147
149
  }
148
150
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
@@ -232,9 +234,17 @@ export function initializeForkChoiceFromUnfinalizedState(
232
234
  executionPayloadBlockHash: isStatePostGloas(unfinalizedState)
233
235
  ? toRootHex(unfinalizedState.latestBlockHash)
234
236
  : toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash),
235
- // TODO GLOAS: executionPayloadNumber is not tracked in BeaconState post-gloas (EIP-7732 removed
236
- // latestExecutionPayloadHeader). Using 0 as unavailable fallback until a solution is found.
237
+ // TODO GLOAS: executionPayloadNumber/GasLimit are not tracked in BeaconState post-gloas
238
+ // (EIP-7732 removed latestExecutionPayloadHeader). Using 0 as unavailable fallback until
239
+ // a solution is found. The 0 doesn't gate validation in practice: at boot the head's
240
+ // PENDING variant's `executionPayloadBlockHash` is the *parent's* payload hash (per the
241
+ // PENDING/EMPTY convention), so gossip bids that reference the head's *own* payload
242
+ // hash won't match this variant anyway and will IGNORE until `onExecutionPayload`
243
+ // upgrades the head to FULL with real values.
237
244
  executionPayloadNumber: isStatePostGloas(unfinalizedState) ? 0 : unfinalizedState.payloadBlockNumber,
245
+ executionPayloadGasLimit: isStatePostGloas(unfinalizedState)
246
+ ? 0
247
+ : unfinalizedState.latestExecutionPayloadHeader.gasLimit,
238
248
  executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing,
239
249
  }
240
250
  : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
@@ -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,7 +18,9 @@ import {
18
18
  G2_POINT_AT_INFINITY,
19
19
  IBeaconStateView,
20
20
  type IBeaconStateViewBellatrix,
21
+ computeEpochAtSlot,
21
22
  computeTimeAtSlot,
23
+ getExpectedGasLimit,
22
24
  isStatePostBellatrix,
23
25
  isStatePostCapella,
24
26
  isStatePostGloas,
@@ -51,17 +53,13 @@ import {
51
53
  import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
52
54
  import {ZERO_HASH_HEX} from "../../constants/index.js";
53
55
  import {numToQuantity} from "../../execution/engine/utils.js";
54
- import {
55
- IExecutionBuilder,
56
- IExecutionEngine,
57
- PayloadAttributes,
58
- PayloadId,
59
- getExpectedGasLimit,
60
- } from "../../execution/index.js";
56
+ import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js";
57
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
61
58
  import {fromGraffitiBytes} from "../../util/graffiti.js";
62
59
  import {kzg} from "../../util/kzg.js";
63
60
  import type {BeaconChain} from "../chain.js";
64
61
  import {CommonBlockBody} from "../interface.js";
62
+ import {ProposerPreferencesPool} from "../opPools/index.js";
65
63
  import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
66
64
 
67
65
  // Time to provide the EL to generate a payload from new payload id
@@ -204,6 +202,9 @@ export async function produceBlockBody<T extends BlockType>(
204
202
  // this into a completely separate function and have pre/post gloas more separated
205
203
  const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
206
204
  const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
205
+ // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
206
+ // ProposerPreferencesPool. Consider using this unified cache instead
207
+ // see https://github.com/ChainSafe/lodestar/issues/9379
207
208
  const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
208
209
 
209
210
  const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
@@ -324,6 +325,7 @@ export async function produceBlockBody<T extends BlockType>(
324
325
  fetchedTime,
325
326
  executionBlockHash: toRootHex(executionPayload.blockHash),
326
327
  blobs: blobsBundle.commitments.length,
328
+ gasLimit: executionPayload.gasLimit,
327
329
  });
328
330
 
329
331
  Object.assign(logMeta, {
@@ -633,6 +635,8 @@ export async function prepareExecutionPayload(
633
635
  chain: {
634
636
  executionEngine: IExecutionEngine;
635
637
  config: ChainForkConfig;
638
+ forkChoice: IForkChoice;
639
+ proposerPreferencesPool: ProposerPreferencesPool;
636
640
  },
637
641
  logger: Logger,
638
642
  fork: ForkPostBellatrix,
@@ -733,6 +737,7 @@ export function getPayloadAttributesForSSE(
733
737
  chain: {
734
738
  config: ChainForkConfig;
735
739
  forkChoice: IForkChoice;
740
+ proposerPreferencesPool: ProposerPreferencesPool;
736
741
  },
737
742
  {
738
743
  prepareState,
@@ -789,6 +794,8 @@ function preparePayloadAttributes(
789
794
  fork: ForkPostBellatrix,
790
795
  chain: {
791
796
  config: ChainForkConfig;
797
+ forkChoice: IForkChoice;
798
+ proposerPreferencesPool: ProposerPreferencesPool;
792
799
  },
793
800
  {
794
801
  prepareState,
@@ -851,12 +858,73 @@ function preparePayloadAttributes(
851
858
  }
852
859
 
853
860
  if (ForkSeq[fork] >= ForkSeq.gloas) {
861
+ if (!isStatePostGloas(prepareState)) {
862
+ throw new Error("Expected Gloas state for Gloas payload attributes");
863
+ }
854
864
  (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
865
+ (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit(
866
+ chain,
867
+ prepareSlot,
868
+ parentBlockRoot,
869
+ parentBlockHash
870
+ );
855
871
  }
856
872
 
857
873
  return payloadAttributes;
858
874
  }
859
875
 
876
+ /**
877
+ * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4`
878
+ * `targetGasLimit` field (consensus-specs#5235, execution-apis#796).
879
+ *
880
+ * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool
881
+ * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching
882
+ * preferences are pooled, target the parent payload's gas limit so the gas limit stays
883
+ * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`).
884
+ *
885
+ * The parent payload's gas_limit is read from fork choice — the variant matching
886
+ * `(parentBlockRoot, parentBlockHash)` carries the correct value for both FULL parents
887
+ * (FULL.executionPayloadGasLimit = delivered payload's gas_limit) and EMPTY parents
888
+ * (EMPTY.executionPayloadGasLimit = inherited grandparent's gas_limit).
889
+ */
890
+ function getProposerTargetGasLimit(
891
+ chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool},
892
+ prepareSlot: Slot,
893
+ parentBlockRoot: Root,
894
+ parentBlockHash: Bytes32
895
+ ): number {
896
+ const parentBlockRootHex = toRootHex(parentBlockRoot);
897
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
898
+ const dependentRootHex = (() => {
899
+ if (parentBlock === null) {
900
+ return null;
901
+ }
902
+ try {
903
+ return getShufflingDependentRoot(
904
+ chain.forkChoice,
905
+ computeEpochAtSlot(prepareSlot),
906
+ computeEpochAtSlot(parentBlock.slot),
907
+ parentBlock
908
+ );
909
+ } catch {
910
+ return null;
911
+ }
912
+ })();
913
+
914
+ const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null;
915
+ if (pref !== null) {
916
+ return pref.message.targetGasLimit;
917
+ }
918
+
919
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, toRootHex(parentBlockHash));
920
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
921
+ throw new Error(
922
+ `Cannot resolve parent payload gas_limit for proposer targetGasLimit fallback parentBlockRoot=${parentBlockRootHex} parentBlockHash=${toRootHex(parentBlockHash)}`
923
+ );
924
+ }
925
+ return parentPayloadVariant.executionPayloadGasLimit;
926
+ }
927
+
860
928
  export async function produceCommonBlockBody<T extends BlockType>(
861
929
  this: BeaconChain,
862
930
  blockType: T,
@@ -4,10 +4,12 @@ import {
4
4
  createSingleSignatureSetFromComponents,
5
5
  getExecutionPayloadBidSigningRoot,
6
6
  isActiveBuilder,
7
+ isGasLimitTargetCompatible,
7
8
  isStatePostGloas,
8
9
  } from "@lodestar/state-transition";
9
- import {gloas} from "@lodestar/types";
10
- import {toRootHex} from "@lodestar/utils";
10
+ import {ValidatorIndex, gloas} from "@lodestar/types";
11
+ import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
12
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
11
13
  import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
12
14
  import {IBeaconChain} from "../index.js";
13
15
  import {RegenCaller} from "../regen/index.js";
@@ -15,21 +17,21 @@ import {RegenCaller} from "../regen/index.js";
15
17
  export async function validateApiExecutionPayloadBid(
16
18
  chain: IBeaconChain,
17
19
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
18
- ): Promise<void> {
20
+ ): Promise<{proposerIndex: ValidatorIndex}> {
19
21
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
20
22
  }
21
23
 
22
24
  export async function validateGossipExecutionPayloadBid(
23
25
  chain: IBeaconChain,
24
26
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
25
- ): Promise<void> {
27
+ ): Promise<{proposerIndex: ValidatorIndex}> {
26
28
  return validateExecutionPayloadBid(chain, signedExecutionPayloadBid);
27
29
  }
28
30
 
29
31
  async function validateExecutionPayloadBid(
30
32
  chain: IBeaconChain,
31
33
  signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
32
- ): Promise<void> {
34
+ ): Promise<{proposerIndex: ValidatorIndex}> {
33
35
  const bid = signedExecutionPayloadBid.message;
34
36
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
35
37
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
@@ -48,12 +50,55 @@ async function validateExecutionPayloadBid(
48
50
  });
49
51
  }
50
52
 
53
+ // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
54
+ // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
55
+ // proposer-preferences lookup below from a known fork-choice block.
56
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
57
+ if (parentBlock === null) {
58
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
59
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
60
+ parentBlockRoot: parentBlockRootHex,
61
+ });
62
+ }
63
+
51
64
  // [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
52
65
  // seen — i.e. `proposal_slot == bid.slot` AND `dependent_root ==
53
- // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`,
54
- // where `parent_state` is the post-state of `bid.parent_block_root`.
55
- // This is the message referenced as `proposer_preferences` in the following REJECT rules.
56
- // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
66
+ // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`.
67
+ const bidEpoch = computeEpochAtSlot(bid.slot);
68
+ // gloas is always post-Fulu, so `get_proposer_dependent_root` is the post-Fulu (deterministic
69
+ // proposer lookahead) form `block_root_at(start_slot(epoch - MIN_SEED_LOOKAHEAD) - 1)` with
70
+ // `MIN_SEED_LOOKAHEAD == 1` — identical to the attester-shuffling dependent root for the same
71
+ // epoch (both 1-epoch lookahead), hence `getShufflingDependentRoot`. `null` on a
72
+ // unknown/finalized-pruned ancestor or genesis edge → degrade to IGNORE below instead of
73
+ // letting a raw `ForkChoiceError` escape the `GossipActionError` contract.
74
+ const dependentRootHex = (() => {
75
+ try {
76
+ return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
77
+ } catch {
78
+ return null;
79
+ }
80
+ })();
81
+
82
+ if (dependentRootHex === null) {
83
+ // Could not derive the dependent root for this branch (unknown/finalized-pruned ancestor,
84
+ // genesis edge, etc.) → definitionally no matching `SignedProposerPreferences`.
85
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
86
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
87
+ slot: bid.slot,
88
+ parentBlockRoot: parentBlockRootHex,
89
+ dependentRoot: "unknown",
90
+ });
91
+ }
92
+
93
+ const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
94
+ if (proposerPreferences === null) {
95
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
96
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
97
+ slot: bid.slot,
98
+ parentBlockRoot: parentBlockRootHex,
99
+ dependentRoot: dependentRootHex,
100
+ });
101
+ }
57
102
 
58
103
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
59
104
  // `is_active_builder(state, bid.builder_index)` returns `True`.
@@ -75,10 +120,44 @@ async function validateExecutionPayloadBid(
75
120
  }
76
121
 
77
122
  // [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`.
78
- // [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`.
79
- // Both compared against the matching `proposer_preferences` defined above (same branch
80
- // via dependent_root, same proposal_slot).
81
- // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
123
+ if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
124
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
125
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH,
126
+ builderIndex: bid.builderIndex,
127
+ bidFeeRecipient: toHex(bid.feeRecipient),
128
+ expectedFeeRecipient: toHex(proposerPreferences.message.feeRecipient),
129
+ });
130
+ }
131
+
132
+ // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution payload in fork
133
+ // choice. Looks up the variant of `bid.parent_block_root` whose payload hash matches
134
+ // `bid.parent_block_hash` — works for both FULL parents (FULL variant carries the delivered
135
+ // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent
136
+ // payload's hash, since the new block doesn't have its own payload). Variant carries the
137
+ // executed payload's gas_limit, which we use as `parent_gas_limit` below.
138
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex);
139
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
140
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
141
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
142
+ parentBlockHash: parentBlockHashHex,
143
+ });
144
+ }
145
+
146
+ // [IGNORE] `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, target_gas_limit)`,
147
+ // where `parent_gas_limit` is the `gas_limit` of the parent execution payload and
148
+ // `target_gas_limit` is `proposer_preferences.target_gas_limit`.
149
+ const bidGasLimit = Number(bid.gasLimit);
150
+ const parentGasLimit = parentPayloadVariant.executionPayloadGasLimit;
151
+ const targetGasLimit = proposerPreferences.message.targetGasLimit;
152
+ if (!isGasLimitTargetCompatible(parentGasLimit, bidGasLimit, targetGasLimit)) {
153
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
154
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
155
+ builderIndex: bid.builderIndex,
156
+ bidGasLimit,
157
+ parentGasLimit,
158
+ targetGasLimit,
159
+ });
160
+ }
82
161
 
83
162
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
84
163
  // consensus layer -- i.e. validate that
@@ -124,19 +203,6 @@ async function validateExecutionPayloadBid(
124
203
  });
125
204
  }
126
205
 
127
- // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution
128
- // payload in fork choice.
129
- // TODO GLOAS: implement this
130
-
131
- // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon
132
- // block in fork choice.
133
- if (!chain.forkChoice.hasBlock(bid.parentBlockRoot)) {
134
- throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
135
- code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
136
- parentBlockRoot: parentBlockRootHex,
137
- });
138
- }
139
-
140
206
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
141
207
  const signatureSet = createSingleSignatureSetFromComponents(
142
208
  PublicKey.fromBytes(builder.pubkey),
@@ -154,4 +220,6 @@ async function validateExecutionPayloadBid(
154
220
 
155
221
  // Valid
156
222
  chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex);
223
+
224
+ return {proposerIndex: proposerPreferences.message.validatorIndex};
157
225
  }
@@ -11,7 +11,7 @@ import {IBeaconChain} from "../index.js";
11
11
 
12
12
  export type PayloadAttestationValidationResult = {
13
13
  attDataRootHex: RootHex;
14
- validatorCommitteeIndex: number;
14
+ validatorCommitteeIndices: number[];
15
15
  };
16
16
 
17
17
  export async function validateApiPayloadAttestationMessage(
@@ -80,9 +80,11 @@ async function validatePayloadAttestationMessage(
80
80
  // [REJECT] The message's validator index is within the payload committee in
81
81
  // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to
82
82
  // processing the block up to the current slot as determined by the fork choice.
83
- const validatorCommitteeIndex = state.getIndexInPayloadTimelinessCommittee(validatorIndex, data.slot);
83
+ // The validator may occupy multiple PTC positions because `compute_ptc` samples
84
+ // by effective balance — collect all of them so duplicate votes are counted.
85
+ const validatorCommitteeIndices = state.getIndicesInPayloadTimelinessCommittee(validatorIndex, data.slot);
84
86
 
85
- if (validatorCommitteeIndex === -1) {
87
+ if (validatorCommitteeIndices.length === 0) {
86
88
  throw new PayloadAttestationError(GossipAction.REJECT, {
87
89
  code: PayloadAttestationErrorCode.INVALID_ATTESTER,
88
90
  attesterIndex: validatorIndex,
@@ -115,6 +117,6 @@ async function validatePayloadAttestationMessage(
115
117
 
116
118
  return {
117
119
  attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)),
118
- validatorCommitteeIndex,
120
+ validatorCommitteeIndices,
119
121
  };
120
122
  }