@lodestar/beacon-node 1.43.0 → 1.44.0-dev.552cdce8d0
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 +18 -11
- package/lib/api/impl/validator/index.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 +19 -1
- package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadBid.js +3 -0
- package/lib/chain/errors/executionPayloadBid.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/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +64 -16
- 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/network/gossip/topic.d.ts +19 -766
- package/lib/network/gossip/topic.d.ts.map +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 +8 -3
- 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 +19 -11
- package/src/chain/chain.ts +3 -0
- package/src/chain/errors/executionPayloadBid.ts +22 -1
- 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/validation/executionPayloadBid.ts +67 -17
- package/src/chain/validation/payloadAttestationMessage.ts +6 -4
- package/src/network/interface.ts +1 -0
- package/src/network/network.ts +11 -0
- package/src/network/processor/gossipHandlers.ts +8 -2
- package/src/util/dependentRoot.ts +22 -18
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ChainSafe/lodestar/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.44.0-dev.552cdce8d0",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
@@ -135,18 +135,17 @@
|
|
|
135
135
|
"@libp2p/peer-id": "^6.0.4",
|
|
136
136
|
"@libp2p/prometheus-metrics": "^5.0.14",
|
|
137
137
|
"@libp2p/tcp": "^11.0.13",
|
|
138
|
-
"@lodestar/api": "^1.
|
|
139
|
-
"@lodestar/config": "^1.
|
|
140
|
-
"@lodestar/db": "^1.
|
|
141
|
-
"@lodestar/fork-choice": "^1.
|
|
142
|
-
"@lodestar/
|
|
143
|
-
"@lodestar/
|
|
144
|
-
"@lodestar/
|
|
145
|
-
"@lodestar/
|
|
146
|
-
"@lodestar/
|
|
147
|
-
"@lodestar/
|
|
148
|
-
"@lodestar/
|
|
149
|
-
"@lodestar/validator": "^1.43.0",
|
|
138
|
+
"@lodestar/api": "^1.44.0-dev.552cdce8d0",
|
|
139
|
+
"@lodestar/config": "^1.44.0-dev.552cdce8d0",
|
|
140
|
+
"@lodestar/db": "^1.44.0-dev.552cdce8d0",
|
|
141
|
+
"@lodestar/fork-choice": "^1.44.0-dev.552cdce8d0",
|
|
142
|
+
"@lodestar/logger": "^1.44.0-dev.552cdce8d0",
|
|
143
|
+
"@lodestar/params": "^1.44.0-dev.552cdce8d0",
|
|
144
|
+
"@lodestar/reqresp": "^1.44.0-dev.552cdce8d0",
|
|
145
|
+
"@lodestar/state-transition": "^1.44.0-dev.552cdce8d0",
|
|
146
|
+
"@lodestar/types": "^1.44.0-dev.552cdce8d0",
|
|
147
|
+
"@lodestar/utils": "^1.44.0-dev.552cdce8d0",
|
|
148
|
+
"@lodestar/validator": "^1.44.0-dev.552cdce8d0",
|
|
150
149
|
"@multiformats/multiaddr": "^13.0.1",
|
|
151
150
|
"datastore-core": "^11.0.2",
|
|
152
151
|
"datastore-fs": "^11.0.2",
|
|
@@ -169,7 +168,7 @@
|
|
|
169
168
|
"@libp2p/interface-internal": "^3.0.13",
|
|
170
169
|
"@libp2p/logger": "^6.2.2",
|
|
171
170
|
"@libp2p/utils": "^7.0.13",
|
|
172
|
-
"@lodestar/spec-test-util": "^1.
|
|
171
|
+
"@lodestar/spec-test-util": "^1.44.0-dev.552cdce8d0",
|
|
173
172
|
"@types/js-yaml": "^4.0.5",
|
|
174
173
|
"@types/qs": "^6.9.7",
|
|
175
174
|
"@types/tmp": "^0.2.3",
|
|
@@ -187,5 +186,5 @@
|
|
|
187
186
|
"beacon",
|
|
188
187
|
"blockchain"
|
|
189
188
|
],
|
|
190
|
-
"gitHead": "
|
|
189
|
+
"gitHead": "fed08b6217ae9e76ec2ce7b28016620c41faba4f"
|
|
191
190
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {routes} from "@lodestar/api";
|
|
2
2
|
import {ApplicationMethods} from "@lodestar/api/server";
|
|
3
3
|
import {
|
|
4
|
+
ForkName,
|
|
4
5
|
ForkPostElectra,
|
|
5
6
|
ForkPreElectra,
|
|
6
7
|
SYNC_COMMITTEE_SUBNET_SIZE,
|
|
@@ -16,12 +17,15 @@ import {
|
|
|
16
17
|
GossipAction,
|
|
17
18
|
PayloadAttestationError,
|
|
18
19
|
PayloadAttestationErrorCode,
|
|
20
|
+
ProposerPreferencesError,
|
|
21
|
+
ProposerPreferencesErrorCode,
|
|
19
22
|
SyncCommitteeError,
|
|
20
23
|
} from "../../../../chain/errors/index.js";
|
|
21
24
|
import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
|
|
22
25
|
import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
|
|
23
26
|
import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
|
|
24
27
|
import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js";
|
|
28
|
+
import {validateGossipProposerPreferences} from "../../../../chain/validation/proposerPreferences.js";
|
|
25
29
|
import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
|
|
26
30
|
import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
|
|
27
31
|
import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
|
|
@@ -81,6 +85,55 @@ export function getBeaconPoolApi({
|
|
|
81
85
|
return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}};
|
|
82
86
|
},
|
|
83
87
|
|
|
88
|
+
async getPoolProposerPreferences({slot}) {
|
|
89
|
+
const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot);
|
|
90
|
+
if (!isForkPostGloas(fork)) {
|
|
91
|
+
throw new ApiError(400, `Proposer preferences pool is not supported before Gloas fork=${fork}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async submitSignedProposerPreferences({signedProposerPreferences}) {
|
|
98
|
+
const failures: FailureList = [];
|
|
99
|
+
|
|
100
|
+
await Promise.all(
|
|
101
|
+
signedProposerPreferences.map(async (signed, i) => {
|
|
102
|
+
try {
|
|
103
|
+
await validateGossipProposerPreferences(chain, signed);
|
|
104
|
+
|
|
105
|
+
chain.proposerPreferencesPool.add(signed);
|
|
106
|
+
await network.publishProposerPreferences(signed);
|
|
107
|
+
chain.emitter.emit(routes.events.EventType.proposerPreferences, {
|
|
108
|
+
version: ForkName.gloas,
|
|
109
|
+
data: signed,
|
|
110
|
+
});
|
|
111
|
+
} catch (e) {
|
|
112
|
+
const logCtx = {
|
|
113
|
+
slot: signed.message.proposalSlot,
|
|
114
|
+
validatorIndex: signed.message.validatorIndex,
|
|
115
|
+
dependentRoot: toRootHex(signed.message.dependentRoot),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) {
|
|
119
|
+
logger.debug("Ignoring known signed proposer preferences", logCtx);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
failures.push({index: i, message: (e as Error).message});
|
|
124
|
+
logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, e as Error);
|
|
125
|
+
if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) {
|
|
126
|
+
chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (failures.length > 0) {
|
|
133
|
+
throw new IndexedError("Error processing signed proposer preferences", failures);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
84
137
|
async getPoolAttesterSlashings() {
|
|
85
138
|
const fork = chain.config.getForkName(chain.clock.currentSlot);
|
|
86
139
|
|
|
@@ -258,7 +311,7 @@ export function getBeaconPoolApi({
|
|
|
258
311
|
try {
|
|
259
312
|
const validateFn = () => validateApiPayloadAttestationMessage(chain, payloadAttestationMessage);
|
|
260
313
|
const {slot, beaconBlockRoot} = payloadAttestationMessage.data;
|
|
261
|
-
const {attDataRootHex,
|
|
314
|
+
const {attDataRootHex, validatorCommitteeIndices} = await validateGossipFnRetryUnknownRoot(
|
|
262
315
|
validateFn,
|
|
263
316
|
network,
|
|
264
317
|
chain,
|
|
@@ -269,13 +322,13 @@ export function getBeaconPoolApi({
|
|
|
269
322
|
const insertOutcome = chain.payloadAttestationPool.add(
|
|
270
323
|
payloadAttestationMessage,
|
|
271
324
|
attDataRootHex,
|
|
272
|
-
|
|
325
|
+
validatorCommitteeIndices
|
|
273
326
|
);
|
|
274
327
|
metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome});
|
|
275
328
|
|
|
276
329
|
chain.forkChoice.notifyPtcMessages(
|
|
277
330
|
toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
|
|
278
|
-
|
|
331
|
+
validatorCommitteeIndices,
|
|
279
332
|
payloadAttestationMessage.data.payloadPresent
|
|
280
333
|
);
|
|
281
334
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isForkPostBellatrix,
|
|
15
15
|
isForkPostDeneb,
|
|
16
16
|
isForkPostElectra,
|
|
17
|
+
isForkPostFulu,
|
|
17
18
|
isForkPostGloas,
|
|
18
19
|
} from "@lodestar/params";
|
|
19
20
|
import {
|
|
@@ -925,7 +926,7 @@ export function getValidatorApi(
|
|
|
925
926
|
metrics?.blockProductionRequests.inc({source});
|
|
926
927
|
|
|
927
928
|
const graffitiBytes = toGraffitiBytes(
|
|
928
|
-
graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion,
|
|
929
|
+
graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts)
|
|
929
930
|
);
|
|
930
931
|
const commonBlockBodyPromise = chain.produceCommonBlockBody({
|
|
931
932
|
slot,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
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
|
|
|
@@ -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> {}
|
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
|
+
}
|
|
@@ -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
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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),
|
|
@@ -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
|
}
|
package/src/network/interface.ts
CHANGED
|
@@ -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[]>;
|
package/src/network/network.ts
CHANGED
|
@@ -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],
|