@lodestar/beacon-node 1.43.0 → 1.44.0-dev.08d94fd9e2

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 (134) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +30 -0
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
  5. package/lib/api/impl/beacon/pool/index.js +46 -5
  6. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  7. package/lib/api/impl/validator/index.d.ts.map +1 -1
  8. package/lib/api/impl/validator/index.js +105 -53
  9. package/lib/api/impl/validator/index.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  11. package/lib/chain/blocks/importBlock.js +5 -2
  12. package/lib/chain/blocks/importBlock.js.map +1 -1
  13. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.js +5 -3
  15. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  16. package/lib/chain/chain.d.ts +3 -2
  17. package/lib/chain/chain.d.ts.map +1 -1
  18. package/lib/chain/chain.js +5 -2
  19. package/lib/chain/chain.js.map +1 -1
  20. package/lib/chain/errors/executionPayloadBid.d.ts +29 -1
  21. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  22. package/lib/chain/errors/executionPayloadBid.js +5 -0
  23. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  24. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  25. package/lib/chain/forkChoice/index.js +14 -4
  26. package/lib/chain/forkChoice/index.js.map +1 -1
  27. package/lib/chain/interface.d.ts +2 -1
  28. package/lib/chain/interface.d.ts.map +1 -1
  29. package/lib/chain/interface.js.map +1 -1
  30. package/lib/chain/lightClient/index.d.ts.map +1 -1
  31. package/lib/chain/lightClient/index.js +1 -1
  32. package/lib/chain/lightClient/index.js.map +1 -1
  33. package/lib/chain/opPools/executionPayloadBidPool.d.ts +4 -4
  34. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  35. package/lib/chain/opPools/executionPayloadBidPool.js +6 -4
  36. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  37. package/lib/chain/opPools/index.d.ts +1 -0
  38. package/lib/chain/opPools/index.d.ts.map +1 -1
  39. package/lib/chain/opPools/index.js +1 -0
  40. package/lib/chain/opPools/index.js.map +1 -1
  41. package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
  42. package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
  43. package/lib/chain/opPools/payloadAttestationPool.js +30 -10
  44. package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
  45. package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
  46. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
  47. package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
  48. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
  49. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  50. package/lib/chain/prepareNextSlot.js +2 -1
  51. package/lib/chain/prepareNextSlot.js.map +1 -1
  52. package/lib/chain/produceBlock/produceBlockBody.d.ts +7 -1
  53. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  54. package/lib/chain/produceBlock/produceBlockBody.js +107 -18
  55. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  56. package/lib/chain/regen/interface.d.ts +2 -0
  57. package/lib/chain/regen/interface.d.ts.map +1 -1
  58. package/lib/chain/regen/interface.js +2 -0
  59. package/lib/chain/regen/interface.js.map +1 -1
  60. package/lib/chain/validation/executionPayloadBid.d.ts +7 -3
  61. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  62. package/lib/chain/validation/executionPayloadBid.js +97 -23
  63. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  64. package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
  65. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  66. package/lib/chain/validation/payloadAttestationMessage.js +20 -8
  67. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  68. package/lib/chain/validatorMonitor.d.ts +1 -0
  69. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  70. package/lib/chain/validatorMonitor.js +16 -0
  71. package/lib/chain/validatorMonitor.js.map +1 -1
  72. package/lib/execution/builder/index.d.ts +1 -2
  73. package/lib/execution/builder/index.d.ts.map +1 -1
  74. package/lib/execution/builder/index.js +0 -1
  75. package/lib/execution/builder/index.js.map +1 -1
  76. package/lib/execution/engine/interface.d.ts +1 -0
  77. package/lib/execution/engine/interface.d.ts.map +1 -1
  78. package/lib/execution/engine/types.d.ts +2 -0
  79. package/lib/execution/engine/types.d.ts.map +1 -1
  80. package/lib/execution/engine/types.js +2 -0
  81. package/lib/execution/engine/types.js.map +1 -1
  82. package/lib/metrics/metrics/lodestar.d.ts +1 -1
  83. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  84. package/lib/metrics/metrics/lodestar.js +4 -3
  85. package/lib/metrics/metrics/lodestar.js.map +1 -1
  86. package/lib/network/gossip/topic.d.ts +20 -767
  87. package/lib/network/gossip/topic.d.ts.map +1 -1
  88. package/lib/network/interface.d.ts +2 -0
  89. package/lib/network/interface.d.ts.map +1 -1
  90. package/lib/network/network.d.ts +2 -0
  91. package/lib/network/network.d.ts.map +1 -1
  92. package/lib/network/network.js +10 -0
  93. package/lib/network/network.js.map +1 -1
  94. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  95. package/lib/network/processor/gossipHandlers.js +19 -6
  96. package/lib/network/processor/gossipHandlers.js.map +1 -1
  97. package/lib/util/dependentRoot.d.ts +6 -2
  98. package/lib/util/dependentRoot.d.ts.map +1 -1
  99. package/lib/util/dependentRoot.js +20 -16
  100. package/lib/util/dependentRoot.js.map +1 -1
  101. package/package.json +14 -15
  102. package/src/api/impl/beacon/blocks/index.ts +36 -0
  103. package/src/api/impl/beacon/pool/index.ts +59 -4
  104. package/src/api/impl/validator/index.ts +120 -54
  105. package/src/chain/blocks/importBlock.ts +10 -2
  106. package/src/chain/blocks/importExecutionPayload.ts +8 -2
  107. package/src/chain/chain.ts +5 -0
  108. package/src/chain/errors/executionPayloadBid.ts +27 -1
  109. package/src/chain/forkChoice/index.ts +14 -4
  110. package/src/chain/interface.ts +2 -0
  111. package/src/chain/lightClient/index.ts +6 -6
  112. package/src/chain/opPools/executionPayloadBidPool.ts +10 -9
  113. package/src/chain/opPools/index.ts +1 -0
  114. package/src/chain/opPools/payloadAttestationPool.ts +34 -10
  115. package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
  116. package/src/chain/prepareNextSlot.ts +2 -1
  117. package/src/chain/produceBlock/produceBlockBody.ts +158 -25
  118. package/src/chain/regen/interface.ts +2 -0
  119. package/src/chain/validation/executionPayloadBid.ts +107 -28
  120. package/src/chain/validation/payloadAttestationMessage.ts +23 -10
  121. package/src/chain/validatorMonitor.ts +18 -0
  122. package/src/execution/builder/index.ts +1 -4
  123. package/src/execution/engine/interface.ts +1 -0
  124. package/src/execution/engine/types.ts +4 -0
  125. package/src/metrics/metrics/lodestar.ts +4 -3
  126. package/src/network/interface.ts +2 -0
  127. package/src/network/network.ts +22 -0
  128. package/src/network/processor/gossipHandlers.ts +25 -6
  129. package/src/util/dependentRoot.ts +22 -18
  130. package/lib/execution/builder/utils.d.ts +0 -5
  131. package/lib/execution/builder/utils.d.ts.map +0 -1
  132. package/lib/execution/builder/utils.js +0 -17
  133. package/lib/execution/builder/utils.js.map +0 -1
  134. package/src/execution/builder/utils.ts +0 -19
@@ -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,66 @@ 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
+
64
+ // [REJECT] The bid is for a higher slot than its parent block -- i.e.
65
+ // validate that `bid.slot` is greater than the slot of the block with root
66
+ // `bid.parent_block_root`.
67
+ if (bid.slot <= parentBlock.slot) {
68
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
69
+ code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
70
+ parentSlot: parentBlock.slot,
71
+ slot: bid.slot,
72
+ });
73
+ }
74
+
51
75
  // [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
52
76
  // 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.
77
+ // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`.
78
+ const bidEpoch = computeEpochAtSlot(bid.slot);
79
+ // gloas is always post-Fulu, so `get_proposer_dependent_root` is the post-Fulu (deterministic
80
+ // proposer lookahead) form `block_root_at(start_slot(epoch - MIN_SEED_LOOKAHEAD) - 1)` with
81
+ // `MIN_SEED_LOOKAHEAD == 1` — identical to the attester-shuffling dependent root for the same
82
+ // epoch (both 1-epoch lookahead), hence `getShufflingDependentRoot`. `null` on a
83
+ // unknown/finalized-pruned ancestor or genesis edge → degrade to IGNORE below instead of
84
+ // letting a raw `ForkChoiceError` escape the `GossipActionError` contract.
85
+ const dependentRootHex = (() => {
86
+ try {
87
+ return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
88
+ } catch {
89
+ return null;
90
+ }
91
+ })();
92
+
93
+ if (dependentRootHex === null) {
94
+ // Could not derive the dependent root for this branch (unknown/finalized-pruned ancestor,
95
+ // genesis edge, etc.) → definitionally no matching `SignedProposerPreferences`.
96
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
97
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
98
+ slot: bid.slot,
99
+ parentBlockRoot: parentBlockRootHex,
100
+ dependentRoot: "unknown",
101
+ });
102
+ }
103
+
104
+ const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
105
+ if (proposerPreferences === null) {
106
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
107
+ code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
108
+ slot: bid.slot,
109
+ parentBlockRoot: parentBlockRootHex,
110
+ dependentRoot: dependentRootHex,
111
+ });
112
+ }
57
113
 
58
114
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
59
115
  // `is_active_builder(state, bid.builder_index)` returns `True`.
@@ -75,10 +131,44 @@ async function validateExecutionPayloadBid(
75
131
  }
76
132
 
77
133
  // [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.
134
+ if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
135
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
136
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH,
137
+ builderIndex: bid.builderIndex,
138
+ bidFeeRecipient: toHex(bid.feeRecipient),
139
+ expectedFeeRecipient: toHex(proposerPreferences.message.feeRecipient),
140
+ });
141
+ }
142
+
143
+ // [IGNORE] `bid.parent_block_hash` is the block hash of a known execution payload in fork
144
+ // choice. Looks up the variant of `bid.parent_block_root` whose payload hash matches
145
+ // `bid.parent_block_hash` — works for both FULL parents (FULL variant carries the delivered
146
+ // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent
147
+ // payload's hash, since the new block doesn't have its own payload). Variant carries the
148
+ // executed payload's gas_limit, which we use as `parent_gas_limit` below.
149
+ const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex);
150
+ if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) {
151
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
152
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
153
+ parentBlockHash: parentBlockHashHex,
154
+ });
155
+ }
156
+
157
+ // [IGNORE] `is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, target_gas_limit)`,
158
+ // where `parent_gas_limit` is the `gas_limit` of the parent execution payload and
159
+ // `target_gas_limit` is `proposer_preferences.target_gas_limit`.
160
+ const bidGasLimit = Number(bid.gasLimit);
161
+ const parentGasLimit = parentPayloadVariant.executionPayloadGasLimit;
162
+ const targetGasLimit = proposerPreferences.message.targetGasLimit;
163
+ if (!isGasLimitTargetCompatible(parentGasLimit, bidGasLimit, targetGasLimit)) {
164
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
165
+ code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
166
+ builderIndex: bid.builderIndex,
167
+ bidGasLimit,
168
+ parentGasLimit,
169
+ targetGasLimit,
170
+ });
171
+ }
82
172
 
83
173
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
84
174
  // consensus layer -- i.e. validate that
@@ -107,11 +197,11 @@ async function validateExecutionPayloadBid(
107
197
  // [IGNORE] this bid is the highest value bid seen for the tuple
108
198
  // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`.
109
199
  const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex);
110
- if (bestBid !== null && bestBid.value >= bid.value) {
200
+ if (bestBid !== null && bestBid.message.value >= bid.value) {
111
201
  throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
112
202
  code: ExecutionPayloadBidErrorCode.BID_TOO_LOW,
113
203
  bidValue: bid.value,
114
- currentHighestBid: bestBid.value,
204
+ currentHighestBid: bestBid.message.value,
115
205
  });
116
206
  }
117
207
  // [IGNORE] `bid.value` is less or equal than the builder's excess balance --
@@ -124,19 +214,6 @@ async function validateExecutionPayloadBid(
124
214
  });
125
215
  }
126
216
 
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
217
  // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
141
218
  const signatureSet = createSingleSignatureSetFromComponents(
142
219
  PublicKey.fromBytes(builder.pubkey),
@@ -154,4 +231,6 @@ async function validateExecutionPayloadBid(
154
231
 
155
232
  // Valid
156
233
  chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex);
234
+
235
+ return {proposerIndex: proposerPreferences.message.validatorIndex};
157
236
  }
@@ -8,10 +8,11 @@ import {RootHex, gloas, ssz} from "@lodestar/types";
8
8
  import {toRootHex} from "@lodestar/utils";
9
9
  import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
+ import {RegenCaller} from "../regen/index.js";
11
12
 
12
13
  export type PayloadAttestationValidationResult = {
13
14
  attDataRootHex: RootHex;
14
- validatorCommitteeIndex: number;
15
+ validatorCommitteeIndices: number[];
15
16
  };
16
17
 
17
18
  export async function validateApiPayloadAttestationMessage(
@@ -61,28 +62,40 @@ async function validatePayloadAttestationMessage(
61
62
  // [IGNORE] The message's block `data.beacon_block_root` has been seen (via
62
63
  // gossip or non-gossip sources) (a client MAY queue attestation for processing
63
64
  // once the block is retrieved. Note a client might want to request payload after).
64
- if (!chain.forkChoice.hasBlock(data.beaconBlockRoot)) {
65
+ const block = chain.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot);
66
+ if (!block) {
65
67
  throw new PayloadAttestationError(GossipAction.IGNORE, {
66
68
  code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
67
69
  blockRoot: toRootHex(data.beaconBlockRoot),
68
70
  });
69
71
  }
70
72
 
71
- const state = chain.getHeadState();
72
- if (!isStatePostGloas(state)) {
73
- throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`);
74
- }
75
-
76
73
  // [REJECT] The message's block `data.beacon_block_root` passes validation.
77
74
  // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice,
78
75
  // it is possible that the block didn't pass the validation
79
76
 
77
+ // Use the referenced block's branch state for the PTC committee check
78
+ const state = await chain.regen
79
+ .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage)
80
+ .catch(() => {
81
+ throw new PayloadAttestationError(GossipAction.IGNORE, {
82
+ code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
83
+ blockRoot: toRootHex(data.beaconBlockRoot),
84
+ });
85
+ });
86
+
87
+ if (!isStatePostGloas(state)) {
88
+ throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`);
89
+ }
90
+
80
91
  // [REJECT] The message's validator index is within the payload committee in
81
92
  // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to
82
93
  // processing the block up to the current slot as determined by the fork choice.
83
- const validatorCommitteeIndex = state.getIndexInPayloadTimelinessCommittee(validatorIndex, data.slot);
94
+ // The validator may occupy multiple PTC positions because `compute_ptc` samples
95
+ // by effective balance — collect all of them so duplicate votes are counted.
96
+ const validatorCommitteeIndices = state.getIndicesInPayloadTimelinessCommittee(validatorIndex, data.slot);
84
97
 
85
- if (validatorCommitteeIndex === -1) {
98
+ if (validatorCommitteeIndices.length === 0) {
86
99
  throw new PayloadAttestationError(GossipAction.REJECT, {
87
100
  code: PayloadAttestationErrorCode.INVALID_ATTESTER,
88
101
  attesterIndex: validatorIndex,
@@ -115,6 +128,6 @@ async function validatePayloadAttestationMessage(
115
128
 
116
129
  return {
117
130
  attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)),
118
- validatorCommitteeIndex,
131
+ validatorCommitteeIndices,
119
132
  };
120
133
  }
@@ -66,6 +66,7 @@ export type ValidatorMonitor = {
66
66
  delaySec: Seconds,
67
67
  envelope: gloas.SignedExecutionPayloadEnvelope
68
68
  ): void;
69
+ registerExecutionPayloadBid(src: OpSource, proposerIndex: ValidatorIndex, bid: gloas.ExecutionPayloadBid): void;
69
70
  registerImportedBlock(block: BeaconBlock, data: {proposerBalanceDelta: number}): void;
70
71
  onPoolSubmitUnaggregatedAttestation(
71
72
  seenTimestampSec: number,
@@ -459,6 +460,23 @@ export function createValidatorMonitor(
459
460
  // TODO GLOAS: implement execution payload envelope monitoring
460
461
  },
461
462
 
463
+ registerExecutionPayloadBid(src, proposerIndex, bid) {
464
+ if (!validators.has(proposerIndex)) {
465
+ return;
466
+ }
467
+ log("Received an execution payload bid for monitored proposer", {
468
+ slot: bid.slot,
469
+ proposerIndex,
470
+ src,
471
+ builderIndex: bid.builderIndex,
472
+ gasLimit: bid.gasLimit,
473
+ value: bid.value.toString(),
474
+ parentBlockRoot: toRootHex(bid.parentBlockRoot),
475
+ parentBlockHash: toRootHex(bid.parentBlockHash),
476
+ blockHash: toRootHex(bid.blockHash),
477
+ });
478
+ },
479
+
462
480
  registerImportedBlock(block, {proposerBalanceDelta}) {
463
481
  const validator = validators.get(block.proposerIndex);
464
482
  if (validator) {
@@ -1,11 +1,8 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {Logger} from "@lodestar/logger";
3
3
  import {Metrics} from "../../metrics/metrics.js";
4
- import {IExecutionBuilder} from "./interface.js";
5
-
6
- export {getExpectedGasLimit} from "./utils.js";
7
-
8
4
  import {ExecutionBuilderHttp, ExecutionBuilderHttpOpts, defaultExecutionBuilderHttpOpts} from "./http.js";
5
+ import {IExecutionBuilder} from "./interface.js";
9
6
 
10
7
  export {ExecutionBuilderHttp, defaultExecutionBuilderHttpOpts};
11
8
 
@@ -88,6 +88,7 @@ export type PayloadAttributes = {
88
88
  withdrawals?: capella.Withdrawal[];
89
89
  parentBeaconBlockRoot?: Uint8Array;
90
90
  slotNumber?: number; // EIP-7843
91
+ targetGasLimit?: number; // GLOAS (PayloadAttributesV4, execution-apis#796)
91
92
  };
92
93
 
93
94
  export type VersionedHashes = Uint8Array[];
@@ -245,6 +245,8 @@ export type PayloadAttributesRpc = {
245
245
  parentBeaconBlockRoot?: DATA;
246
246
  /** QUANTITY, 64 Bits - value for the slot number field of the new payload (EIP-7843) */
247
247
  slotNumber?: QUANTITY;
248
+ /** QUANTITY, 64 Bits - target value for the gasLimit field of the new payload (GLOAS, execution-apis#796) */
249
+ targetGasLimit?: QUANTITY;
248
250
  };
249
251
 
250
252
  export type ClientVersionRpc = {
@@ -425,6 +427,7 @@ export function serializePayloadAttributes(data: PayloadAttributes): PayloadAttr
425
427
  withdrawals: data.withdrawals?.map(serializeWithdrawal),
426
428
  parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined,
427
429
  slotNumber: data.slotNumber !== undefined ? numToQuantity(data.slotNumber) : undefined,
430
+ targetGasLimit: data.targetGasLimit !== undefined ? numToQuantity(data.targetGasLimit) : undefined,
428
431
  };
429
432
  }
430
433
 
@@ -442,6 +445,7 @@ export function deserializePayloadAttributes(data: PayloadAttributesRpc): Payloa
442
445
  withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)),
443
446
  parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined,
444
447
  slotNumber: data.slotNumber !== undefined ? quantityToNum(data.slotNumber) : undefined,
448
+ targetGasLimit: data.targetGasLimit !== undefined ? quantityToNum(data.targetGasLimit) : undefined,
445
449
  };
446
450
  }
447
451
 
@@ -987,10 +987,11 @@ export function createLodestarMetrics(
987
987
  }),
988
988
  },
989
989
  importPayload: {
990
- bySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
991
- name: "lodestar_import_payload_by_source_total",
992
- help: "Total number of imported execution payload envelopes by source",
990
+ elapsedTimeTillImported: register.histogram<{source: PayloadEnvelopeInputSource}>({
991
+ name: "lodestar_import_payload_elapsed_time_till_imported_seconds",
992
+ help: "Time elapsed between slot time and the time execution payload envelope is imported (added to fork choice)",
993
993
  labelNames: ["source"],
994
+ buckets: [1, 2, 3, 6, 9, 12],
994
995
  }),
995
996
  columnsBySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
996
997
  name: "lodestar_import_payload_columns_by_source_total",
@@ -113,7 +113,9 @@ export interface INetwork extends INetworkCorePublic {
113
113
  publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise<number>;
114
114
  publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number>;
115
115
  publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
116
+ publishSignedExecutionPayloadBid(signedBid: gloas.SignedExecutionPayloadBid): Promise<number>;
116
117
  publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
118
+ publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
117
119
 
118
120
  // Debug
119
121
  dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]>;
@@ -515,6 +515,17 @@ export class Network implements INetwork {
515
515
  );
516
516
  }
517
517
 
518
+ async publishSignedExecutionPayloadBid(signedBid: gloas.SignedExecutionPayloadBid): Promise<number> {
519
+ const epoch = computeEpochAtSlot(signedBid.message.slot);
520
+ const boundary = this.config.getForkBoundaryAtEpoch(epoch);
521
+
522
+ return this.publishGossip<GossipType.execution_payload_bid>(
523
+ {type: GossipType.execution_payload_bid, boundary},
524
+ signedBid,
525
+ {ignoreDuplicatePublishError: true}
526
+ );
527
+ }
528
+
518
529
  async publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number> {
519
530
  const epoch = computeEpochAtSlot(payloadAttestationMessage.data.slot);
520
531
  const boundary = this.config.getForkBoundaryAtEpoch(epoch);
@@ -526,6 +537,17 @@ export class Network implements INetwork {
526
537
  );
527
538
  }
528
539
 
540
+ async publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number> {
541
+ const epoch = computeEpochAtSlot(signedProposerPreferences.message.proposalSlot);
542
+ const boundary = this.config.getForkBoundaryAtEpoch(epoch);
543
+
544
+ return this.publishGossip<GossipType.proposer_preferences>(
545
+ {type: GossipType.proposer_preferences, boundary},
546
+ signedProposerPreferences,
547
+ {ignoreDuplicatePublishError: true}
548
+ );
549
+ }
550
+
529
551
  private async publishGossip<K extends GossipType>(
530
552
  topic: GossipTopicMap[K],
531
553
  object: GossipTypeMap[K],
@@ -1117,7 +1117,16 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1117
1117
  }
1118
1118
 
1119
1119
  const slot = envelope.payload.slotNumber;
1120
- const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
1120
+ const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
1121
+
1122
+ logger.debug("Received gossip payload envelope", {
1123
+ currentSlot: chain.clock.currentSlot,
1124
+ peerId: peerIdStr,
1125
+ slot,
1126
+ blockRoot: toRootHex(envelope.beaconBlockRoot),
1127
+ delaySec,
1128
+ });
1129
+
1121
1130
  metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
1122
1131
  chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope);
1123
1132
 
@@ -1197,7 +1206,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1197
1206
  const insertOutcome = chain.payloadAttestationPool.add(
1198
1207
  payloadAttestationMessage,
1199
1208
  validationResult.attDataRootHex,
1200
- validationResult.validatorCommitteeIndex
1209
+ validationResult.validatorCommitteeIndices
1201
1210
  );
1202
1211
  metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
1203
1212
  } catch (e) {
@@ -1205,8 +1214,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1205
1214
  }
1206
1215
  chain.forkChoice.notifyPtcMessages(
1207
1216
  toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
1208
- [validationResult.validatorCommitteeIndex],
1209
- payloadAttestationMessage.data.payloadPresent
1217
+ payloadAttestationMessage.data.slot,
1218
+ validationResult.validatorCommitteeIndices,
1219
+ payloadAttestationMessage.data.payloadPresent,
1220
+ payloadAttestationMessage.data.blobDataAvailable
1210
1221
  );
1211
1222
  },
1212
1223
  [GossipType.execution_payload_bid]: async ({
@@ -1215,16 +1226,18 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1215
1226
  }: GossipHandlerParamGeneric<GossipType.execution_payload_bid>) => {
1216
1227
  const {serializedData} = gossipData;
1217
1228
  const executionPayloadBid = sszDeserialize(topic, serializedData);
1218
- await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
1229
+ const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
1219
1230
 
1220
1231
  // Handle valid payload bid by storing in a bid pool
1221
1232
  try {
1222
- const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid.message);
1233
+ const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid);
1223
1234
  metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome});
1224
1235
  } catch (e) {
1225
1236
  logger.error("Error adding to executionPayloadBid pool", {}, e as Error);
1226
1237
  }
1227
1238
 
1239
+ chain.validatorMonitor?.registerExecutionPayloadBid(OpSource.gossip, proposerIndex, executionPayloadBid.message);
1240
+
1228
1241
  chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
1229
1242
  version: config.getForkName(executionPayloadBid.message.slot),
1230
1243
  data: executionPayloadBid,
@@ -1237,6 +1250,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
1237
1250
  const {serializedData} = gossipData;
1238
1251
  const signedProposerPreferences = sszDeserialize(topic, serializedData);
1239
1252
  await validateGossipProposerPreferences(chain, signedProposerPreferences);
1253
+
1254
+ chain.proposerPreferencesPool.add(signedProposerPreferences);
1255
+ chain.emitter.emit(routes.events.EventType.proposerPreferences, {
1256
+ version: ForkName.gloas,
1257
+ data: signedProposerPreferences,
1258
+ });
1240
1259
  },
1241
1260
  };
1242
1261
  }
@@ -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;
@@ -1,5 +0,0 @@
1
- /**
2
- * Calculates expected gas limit based on parent gas limit and target gas limit
3
- */
4
- export declare function getExpectedGasLimit(parentGasLimit: number, targetGasLimit: number): number;
5
- //# sourceMappingURL=utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/execution/builder/utils.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAU1F"}
@@ -1,17 +0,0 @@
1
- /**
2
- * From https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md
3
- */
4
- const gasLimitAdjustmentFactor = 1024;
5
- /**
6
- * Calculates expected gas limit based on parent gas limit and target gas limit
7
- */
8
- export function getExpectedGasLimit(parentGasLimit, targetGasLimit) {
9
- const maxGasLimitDifference = Math.max(Math.floor(parentGasLimit / gasLimitAdjustmentFactor) - 1, 0);
10
- if (targetGasLimit > parentGasLimit) {
11
- const gasDiff = targetGasLimit - parentGasLimit;
12
- return parentGasLimit + Math.min(gasDiff, maxGasLimitDifference);
13
- }
14
- const gasDiff = parentGasLimit - targetGasLimit;
15
- return parentGasLimit - Math.min(gasDiff, maxGasLimitDifference);
16
- }
17
- //# sourceMappingURL=utils.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/execution/builder/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAEtC;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,cAAsB,EAAE,cAAsB,EAAU;IAC1F,MAAM,qBAAqB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,wBAAwB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAErG,IAAI,cAAc,GAAG,cAAc,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;QAChD,OAAO,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,GAAG,cAAc,CAAC;IAChD,OAAO,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;AAAA,CAClE"}
@@ -1,19 +0,0 @@
1
- /**
2
- * From https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md
3
- */
4
- const gasLimitAdjustmentFactor = 1024;
5
-
6
- /**
7
- * Calculates expected gas limit based on parent gas limit and target gas limit
8
- */
9
- export function getExpectedGasLimit(parentGasLimit: number, targetGasLimit: number): number {
10
- const maxGasLimitDifference = Math.max(Math.floor(parentGasLimit / gasLimitAdjustmentFactor) - 1, 0);
11
-
12
- if (targetGasLimit > parentGasLimit) {
13
- const gasDiff = targetGasLimit - parentGasLimit;
14
- return parentGasLimit + Math.min(gasDiff, maxGasLimitDifference);
15
- }
16
-
17
- const gasDiff = parentGasLimit - targetGasLimit;
18
- return parentGasLimit - Math.min(gasDiff, maxGasLimitDifference);
19
- }