@lodestar/beacon-node 1.43.0-dev.4451fec75a → 1.43.0-dev.4fe6b362c9

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 (56) hide show
  1. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/pool/index.js +43 -2
  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 +18 -11
  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/opPools/index.d.ts +1 -0
  19. package/lib/chain/opPools/index.d.ts.map +1 -1
  20. package/lib/chain/opPools/index.js +1 -0
  21. package/lib/chain/opPools/index.js.map +1 -1
  22. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  23. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  24. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  25. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  26. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  27. package/lib/chain/validation/executionPayloadBid.js +64 -16
  28. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  29. package/lib/network/gossip/topic.d.ts +19 -766
  30. package/lib/network/gossip/topic.d.ts.map +1 -1
  31. package/lib/network/interface.d.ts +1 -0
  32. package/lib/network/interface.d.ts.map +1 -1
  33. package/lib/network/network.d.ts +1 -0
  34. package/lib/network/network.d.ts.map +1 -1
  35. package/lib/network/network.js +5 -0
  36. package/lib/network/network.js.map +1 -1
  37. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  38. package/lib/network/processor/gossipHandlers.js +6 -1
  39. package/lib/network/processor/gossipHandlers.js.map +1 -1
  40. package/lib/util/dependentRoot.d.ts +6 -2
  41. package/lib/util/dependentRoot.d.ts.map +1 -1
  42. package/lib/util/dependentRoot.js +20 -16
  43. package/lib/util/dependentRoot.js.map +1 -1
  44. package/package.json +14 -14
  45. package/src/api/impl/beacon/pool/index.ts +53 -0
  46. package/src/api/impl/validator/index.ts +19 -11
  47. package/src/chain/chain.ts +3 -0
  48. package/src/chain/errors/executionPayloadBid.ts +22 -1
  49. package/src/chain/interface.ts +2 -0
  50. package/src/chain/opPools/index.ts +1 -0
  51. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  52. package/src/chain/validation/executionPayloadBid.ts +67 -17
  53. package/src/network/interface.ts +1 -0
  54. package/src/network/network.ts +11 -0
  55. package/src/network/processor/gossipHandlers.ts +6 -0
  56. package/src/util/dependentRoot.ts +22 -18
@@ -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
 
@@ -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,
@@ -1124,26 +1125,33 @@ export function getValidatorApi(
1124
1125
  async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
1125
1126
  notWhileSyncing();
1126
1127
 
1127
- // Early check that epoch is no more than current_epoch + 1, or allow for pre-genesis
1128
1128
  const currentEpoch = currentEpochWithDisparity();
1129
1129
  const nextEpoch = currentEpoch + 1;
1130
- if (currentEpoch >= 0 && epoch > nextEpoch) {
1130
+ const startSlot = computeStartSlotAtEpoch(epoch);
1131
+ const prepareNextSlotLookAheadMs =
1132
+ config.SLOT_DURATION_MS - config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS);
1133
+ const toNextEpochMs = msToNextEpoch();
1134
+ const nearNextEpoch = toNextEpochMs < prepareNextSlotLookAheadMs;
1135
+ // Post-Fulu the proposer lookahead is deterministic and known a full epoch ahead, so
1136
+ // close to the boundary `currentEpoch + 2` is serveable from the upcoming-epoch
1137
+ // checkpoint state (its `nextProposers`). Pre-Fulu / mid-epoch: `currentEpoch + 1` max.
1138
+ const isPostFulu = isForkPostFulu(config.getForkName(startSlot));
1139
+ const maxFutureEpoch = isPostFulu && nearNextEpoch && opts?.v2 ? nextEpoch + 1 : nextEpoch;
1140
+ if (currentEpoch >= 0 && epoch > maxFutureEpoch) {
1131
1141
  throw new ApiError(400, `Requested epoch ${epoch} must not be more than one epoch in the future`);
1132
1142
  }
1133
1143
 
1134
1144
  const head = chain.forkChoice.getHead();
1135
1145
  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
1146
  // 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) {
1147
+ // this is to avoid missed block proposal due to 0 epoch look ahead.
1148
+ // Post-Fulu, `nextEpoch + 1` is served from the same upcoming-epoch (`nextEpoch`)
1149
+ // checkpoint state via its `nextProposers` (deterministic proposer lookahead).
1150
+ if (nearNextEpoch && (epoch === nextEpoch || (isPostFulu && epoch === nextEpoch + 1))) {
1143
1151
  // wait for maximum 1 slot for cp state which is the timeout of validator api
1144
1152
  const cpState = await waitForCheckpointState({
1145
1153
  rootHex: head.blockRoot,
1146
- epoch,
1154
+ epoch: nextEpoch,
1147
1155
  });
1148
1156
  if (cpState) {
1149
1157
  state = cpState;
@@ -1218,7 +1226,7 @@ export function getValidatorApi(
1218
1226
  // It should be set to the latest block applied to `self` or the genesis block root.
1219
1227
  const dependentRoot =
1220
1228
  // In v2 the dependent root is different after fulu due to deterministic proposer lookahead
1221
- proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state) ||
1229
+ proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state, epoch) ||
1222
1230
  (await getGenesisBlockRoot(state));
1223
1231
 
1224
1232
  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
@@ -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";
@@ -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
+ }
@@ -7,7 +7,8 @@ import {
7
7
  isStatePostGloas,
8
8
  } from "@lodestar/state-transition";
9
9
  import {gloas} from "@lodestar/types";
10
- import {toRootHex} from "@lodestar/utils";
10
+ import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
11
+ import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
11
12
  import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
12
13
  import {IBeaconChain} from "../index.js";
13
14
  import {RegenCaller} from "../regen/index.js";
@@ -48,12 +49,55 @@ async function validateExecutionPayloadBid(
48
49
  });
49
50
  }
50
51
 
52
+ // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
53
+ // Moved earlier than the spec ordering so we can derive the proposer dependent root for the
54
+ // proposer-preferences lookup below from a known fork-choice block.
55
+ const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
56
+ if (parentBlock === null) {
57
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
58
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
59
+ parentBlockRoot: parentBlockRootHex,
60
+ });
61
+ }
62
+
51
63
  // [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
52
64
  // 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.
65
+ // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`.
66
+ const bidEpoch = computeEpochAtSlot(bid.slot);
67
+ // gloas is always post-Fulu, so `get_proposer_dependent_root` is the post-Fulu (deterministic
68
+ // proposer lookahead) form `block_root_at(start_slot(epoch - MIN_SEED_LOOKAHEAD) - 1)` with
69
+ // `MIN_SEED_LOOKAHEAD == 1` — identical to the attester-shuffling dependent root for the same
70
+ // epoch (both 1-epoch lookahead), hence `getShufflingDependentRoot`. `null` on a
71
+ // unknown/finalized-pruned ancestor or genesis edge → degrade to IGNORE below instead of
72
+ // letting a raw `ForkChoiceError` escape the `GossipActionError` contract.
73
+ const dependentRootHex = (() => {
74
+ try {
75
+ return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
76
+ } catch {
77
+ return null;
78
+ }
79
+ })();
80
+
81
+ if (dependentRootHex === null) {
82
+ // Could not derive the dependent root for this branch (unknown/finalized-pruned ancestor,
83
+ // genesis edge, etc.) → definitionally no matching `SignedProposerPreferences`.
84
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
85
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
86
+ slot: bid.slot,
87
+ parentBlockRoot: parentBlockRootHex,
88
+ dependentRoot: "unknown",
89
+ });
90
+ }
91
+
92
+ const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
93
+ if (proposerPreferences === null) {
94
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
95
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
96
+ slot: bid.slot,
97
+ parentBlockRoot: parentBlockRootHex,
98
+ dependentRoot: dependentRootHex,
99
+ });
100
+ }
57
101
 
58
102
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
59
103
  // `is_active_builder(state, bid.builder_index)` returns `True`.
@@ -75,10 +119,25 @@ async function validateExecutionPayloadBid(
75
119
  }
76
120
 
77
121
  // [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`.
122
+ if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
123
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
124
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH,
125
+ builderIndex: bid.builderIndex,
126
+ bidFeeRecipient: toHex(bid.feeRecipient),
127
+ expectedFeeRecipient: toHex(proposerPreferences.message.feeRecipient),
128
+ });
129
+ }
130
+
78
131
  // [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.
132
+ const bidGasLimit = Number(bid.gasLimit);
133
+ if (bidGasLimit !== proposerPreferences.message.gasLimit) {
134
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
135
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
136
+ builderIndex: bid.builderIndex,
137
+ bidGasLimit,
138
+ expectedGasLimit: proposerPreferences.message.gasLimit,
139
+ });
140
+ }
82
141
 
83
142
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
84
143
  // consensus layer -- i.e. validate that
@@ -128,15 +187,6 @@ async function validateExecutionPayloadBid(
128
187
  // payload in fork choice.
129
188
  // TODO GLOAS: implement this
130
189
 
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
190
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
141
191
  const signatureSet = createSingleSignatureSetFromComponents(
142
192
  PublicKey.fromBytes(builder.pubkey),
@@ -114,6 +114,7 @@ export interface INetwork extends INetworkCorePublic {
114
114
  publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number>;
115
115
  publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
116
116
  publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
117
+ publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
117
118
 
118
119
  // Debug
119
120
  dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]>;
@@ -526,6 +526,17 @@ export class Network implements INetwork {
526
526
  );
527
527
  }
528
528
 
529
+ async publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number> {
530
+ const epoch = computeEpochAtSlot(signedProposerPreferences.message.proposalSlot);
531
+ const boundary = this.config.getForkBoundaryAtEpoch(epoch);
532
+
533
+ return this.publishGossip<GossipType.proposer_preferences>(
534
+ {type: GossipType.proposer_preferences, boundary},
535
+ signedProposerPreferences,
536
+ {ignoreDuplicatePublishError: true}
537
+ );
538
+ }
539
+
529
540
  private async publishGossip<K extends GossipType>(
530
541
  topic: GossipTopicMap[K],
531
542
  object: GossipTypeMap[K],
@@ -1237,6 +1237,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1237
1237
  const {serializedData} = gossipData;
1238
1238
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
1239
1239
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
1240
+
1241
+ chain.proposerPreferencesPool.add(signedProposerPreferences);
1242
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1243
+ version: ForkName.gloas,
1244
+ data: signedProposerPreferences,
1245
+ });
1240
1246
  },
1241
1247
  };
1242
1248
  }
@@ -2,45 +2,49 @@ import {EpochDifference, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
2
  import {Epoch, RootHex} from "@lodestar/types";
3
3
 
4
4
  /**
5
- * Get dependent root of a shuffling given attestation epoch and head block.
5
+ * Get dependent root of a shuffling given a message epoch and a proto block.
6
+ *
7
+ * Pre-gloas, this is used for attestation validation
8
+ * Post-gloas, this is also used for execution_payload_bid validation because post-fulu,
9
+ * a dependent root of a proposal duties is 1-epoch look ahead (instead of 0 as of pre-fulu)
6
10
  */
7
11
  export function getShufflingDependentRoot(
8
12
  forkChoice: IForkChoice,
9
- attEpoch: Epoch,
10
- blockEpoch: Epoch,
11
- attHeadBlock: ProtoBlock
13
+ msgEpoch: Epoch,
14
+ protoBlockEpoch: Epoch,
15
+ protoBlock: ProtoBlock
12
16
  ): RootHex {
13
17
  let shufflingDependentRoot: RootHex;
14
- if (blockEpoch === attEpoch) {
18
+ if (protoBlockEpoch === msgEpoch) {
15
19
  // current shuffling, this is equivalent to `headState.currentShuffling`
16
- // given blockEpoch = attEpoch = n
20
+ // given protoBlockEpoch = msgEpoch = n
17
21
  // epoch: (n-2) (n-1) n (n+1)
18
22
  // |-------|-------|-------|-------|
19
- // attHeadBlock ------------------------^
23
+ // protoBlock ------------------------^
20
24
  // shufflingDependentRoot ------^
21
- shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.previous);
22
- } else if (blockEpoch === attEpoch - 1) {
25
+ shufflingDependentRoot = forkChoice.getDependentRoot(protoBlock, EpochDifference.previous);
26
+ } else if (protoBlockEpoch === msgEpoch - 1) {
23
27
  // next shuffling, this is equivalent to `headState.nextShuffling`
24
- // given blockEpoch = n-1, attEpoch = n
28
+ // given protoBlockEpoch = n-1, msgEpoch = n
25
29
  // epoch: (n-2) (n-1) n (n+1)
26
30
  // |-------|-------|-------|-------|
27
- // attHeadBlock -------------------^
31
+ // protoBlock -------------------^
28
32
  // shufflingDependentRoot ------^
29
- shufflingDependentRoot = forkChoice.getDependentRoot(attHeadBlock, EpochDifference.current);
30
- } else if (blockEpoch < attEpoch - 1) {
33
+ shufflingDependentRoot = forkChoice.getDependentRoot(protoBlock, EpochDifference.current);
34
+ } else if (protoBlockEpoch < msgEpoch - 1) {
31
35
  // this never happens with default chain option of maxSkipSlots = 32, however we still need to handle it
32
36
  // check the verifyHeadBlockAndTargetRoot() function above
33
- // given blockEpoch = n-2, attEpoch = n
37
+ // given protoBlockEpoch = n-2, msgEpoch = n
34
38
  // epoch: (n-2) (n-1) n (n+1)
35
39
  // |-------|-------|-------|-------|
36
- // attHeadBlock -----------^
40
+ // protoBlock -----------^
37
41
  // shufflingDependentRoot -----^
38
- shufflingDependentRoot = attHeadBlock.blockRoot;
42
+ shufflingDependentRoot = protoBlock.blockRoot;
39
43
  // use lodestar_gossip_attestation_head_slot_to_attestation_slot metric to track this case
40
44
  } else {
41
- // blockEpoch > attEpoch
45
+ // protoBlockEpoch > msgEpoch
42
46
  // should not happen, handled in verifyAttestationTargetRoot
43
- throw Error(`attestation epoch ${attEpoch} is before head block epoch ${blockEpoch}`);
47
+ throw Error(`message epoch ${msgEpoch} is before proto block epoch ${protoBlockEpoch}`);
44
48
  }
45
49
 
46
50
  return shufflingDependentRoot;