@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.
- package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/pool/index.js +46 -5
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +26 -12
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +4 -2
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/chain.d.ts +2 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +3 -1
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/executionPayloadBid.d.ts +24 -1
- package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadBid.js +4 -0
- package/lib/chain/errors/executionPayloadBid.js.map +1 -1
- package/lib/chain/forkChoice/index.d.ts.map +1 -1
- package/lib/chain/forkChoice/index.js +14 -4
- package/lib/chain/forkChoice/index.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -1
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/interface.js.map +1 -1
- package/lib/chain/lightClient/index.d.ts.map +1 -1
- package/lib/chain/lightClient/index.js +1 -1
- package/lib/chain/lightClient/index.js.map +1 -1
- package/lib/chain/opPools/index.d.ts +1 -0
- package/lib/chain/opPools/index.d.ts.map +1 -1
- package/lib/chain/opPools/index.js +1 -0
- package/lib/chain/opPools/index.js.map +1 -1
- package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
- package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
- package/lib/chain/opPools/payloadAttestationPool.js +30 -10
- package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
- package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
- package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
- package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts +4 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +48 -2
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.d.ts +7 -3
- package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +85 -21
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
- package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
- package/lib/chain/validation/payloadAttestationMessage.js +5 -3
- package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
- package/lib/chain/validatorMonitor.d.ts +1 -0
- package/lib/chain/validatorMonitor.d.ts.map +1 -1
- package/lib/chain/validatorMonitor.js +16 -0
- package/lib/chain/validatorMonitor.js.map +1 -1
- package/lib/execution/builder/index.d.ts +1 -2
- package/lib/execution/builder/index.d.ts.map +1 -1
- package/lib/execution/builder/index.js +0 -1
- package/lib/execution/builder/index.js.map +1 -1
- package/lib/execution/engine/interface.d.ts +1 -0
- package/lib/execution/engine/interface.d.ts.map +1 -1
- package/lib/execution/engine/types.d.ts +2 -0
- package/lib/execution/engine/types.d.ts.map +1 -1
- package/lib/execution/engine/types.js +2 -0
- package/lib/execution/engine/types.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +1 -1
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +4 -3
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +1 -1
- package/lib/network/interface.d.ts +1 -0
- package/lib/network/interface.d.ts.map +1 -1
- package/lib/network/network.d.ts +1 -0
- package/lib/network/network.d.ts.map +1 -1
- package/lib/network/network.js +5 -0
- package/lib/network/network.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +18 -5
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/util/dependentRoot.d.ts +6 -2
- package/lib/util/dependentRoot.d.ts.map +1 -1
- package/lib/util/dependentRoot.js +20 -16
- package/lib/util/dependentRoot.js.map +1 -1
- package/package.json +14 -15
- package/src/api/impl/beacon/pool/index.ts +56 -3
- package/src/api/impl/validator/index.ts +28 -12
- package/src/chain/blocks/importExecutionPayload.ts +7 -1
- package/src/chain/chain.ts +3 -0
- package/src/chain/errors/executionPayloadBid.ts +25 -1
- package/src/chain/forkChoice/index.ts +14 -4
- package/src/chain/interface.ts +2 -0
- package/src/chain/lightClient/index.ts +6 -6
- package/src/chain/opPools/index.ts +1 -0
- package/src/chain/opPools/payloadAttestationPool.ts +34 -10
- package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
- package/src/chain/produceBlock/produceBlockBody.ts +75 -7
- package/src/chain/validation/executionPayloadBid.ts +94 -26
- package/src/chain/validation/payloadAttestationMessage.ts +6 -4
- package/src/chain/validatorMonitor.ts +18 -0
- package/src/execution/builder/index.ts +1 -4
- package/src/execution/engine/interface.ts +1 -0
- package/src/execution/engine/types.ts +4 -0
- package/src/metrics/metrics/lodestar.ts +4 -3
- package/src/network/interface.ts +1 -0
- package/src/network/network.ts +11 -0
- package/src/network/processor/gossipHandlers.ts +21 -4
- package/src/util/dependentRoot.ts +22 -18
- package/lib/execution/builder/utils.d.ts +0 -5
- package/lib/execution/builder/utils.d.ts.map +0 -1
- package/lib/execution/builder/utils.js +0 -17
- package/lib/execution/builder/utils.js.map +0 -1
- package/src/execution/builder/utils.ts +0 -19
package/src/chain/chain.ts
CHANGED
|
@@ -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
|
|
144
|
-
// latestExecutionPayloadHeader). Using 0 as unavailable fallback
|
|
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
|
|
236
|
-
// latestExecutionPayloadHeader). Using 0 as unavailable fallback until
|
|
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}),
|
package/src/chain/interface.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
88
|
+
return aggregateMessageInto(message, validatorCommitteeIndices, aggregate);
|
|
89
89
|
}
|
|
90
90
|
// Create a new aggregate with data
|
|
91
|
-
aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message,
|
|
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(
|
|
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
|
|
170
|
+
aggregationBits,
|
|
156
171
|
data: message.data,
|
|
157
|
-
signature
|
|
172
|
+
signature,
|
|
158
173
|
};
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
function aggregateMessageInto(
|
|
162
177
|
message: gloas.PayloadAttestationMessage,
|
|
163
|
-
|
|
178
|
+
validatorCommitteeIndices: number[],
|
|
164
179
|
aggregate: AggregateFast
|
|
165
180
|
): InsertOutcome {
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
120
|
+
validatorCommitteeIndices,
|
|
119
121
|
};
|
|
120
122
|
}
|