diff --git a/packages/api/src/beacon/routes/beacon/pool.ts b/packages/api/src/beacon/routes/beacon/pool.ts index b0ad328786fb..5d0a031771f5 100644 --- a/packages/api/src/beacon/routes/beacon/pool.ts +++ b/packages/api/src/beacon/routes/beacon/pool.ts @@ -6,7 +6,6 @@ import { ForkPreElectra, MAX_PAYLOAD_ATTESTATIONS, PTC_SIZE, - SLOTS_PER_EPOCH, isForkPostElectra, } from "@lodestar/params"; import { @@ -47,12 +46,6 @@ const SignedBLSToExecutionChangeListType = ArrayOf(ssz.capella.SignedBLSToExecut const SyncCommitteeMessageListType = ArrayOf(ssz.altair.SyncCommitteeMessage); const PayloadAttestationListType = ArrayOf(ssz.gloas.PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS); const PayloadAttestationMessageListType = ArrayOf(ssz.gloas.PayloadAttestationMessage, PTC_SIZE); -// 2 * SLOTS_PER_EPOCH max normally; getAll() may return up to 3 * SLOTS_PER_EPOCH during non-finality -const MAX_PROPOSER_PREFERENCES_PER_REQUEST = 3 * SLOTS_PER_EPOCH; -const SignedProposerPreferencesListType = ArrayOf( - ssz.gloas.SignedProposerPreferences, - MAX_PROPOSER_PREFERENCES_PER_REQUEST -); type AttestationListPhase0 = ValueOf; type AttestationListElectra = ValueOf; @@ -68,7 +61,6 @@ type SignedBLSToExecutionChangeList = ValueOf; type PayloadAttestationList = ValueOf; type PayloadAttestationMessageList = ValueOf; -type SignedProposerPreferencesList = ValueOf; export type Endpoints = { /** @@ -95,18 +87,6 @@ export type Endpoints = { VersionMeta >; - /** - * Get signed proposer preferences from operations pool - * Retrieves proposer preferences known by the node but not necessarily incorporated into any block. - */ - getPoolProposerPreferences: Endpoint< - "GET", - {slot?: Slot}, - {query: {slot?: number}}, - SignedProposerPreferencesList, - VersionMeta - >; - /** * Get AttesterSlashings from operations pool * Retrieves attester slashings known by the node but not necessarily incorporated into any block @@ -246,18 +226,6 @@ export type Endpoints = { EmptyResponseData, EmptyMeta >; - - /** - * Submit signed proposer preferences - * Submits signed proposer preferences to the beacon node. - */ - submitSignedProposerPreferences: Endpoint< - "POST", - {signedProposerPreferences: SignedProposerPreferencesList}, - {body: unknown; headers: {[MetaHeader.Version]: string}}, - EmptyResponseData, - EmptyMeta - >; }; export function getDefinitions(config: ChainForkConfig): RouteDefinitions { @@ -288,19 +256,6 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({query: {slot}}), - parseReq: ({query}) => ({slot: query.slot}), - schema: {query: {slot: Schema.Uint}}, - }, - resp: { - data: SignedProposerPreferencesListType, - meta: VersionCodec, - }, - }, getPoolAttesterSlashingsV2: { url: "/eth/v2/beacon/pool/attester_slashings", method: "GET", @@ -516,32 +471,5 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ - body: SignedProposerPreferencesListType.toJson(signedProposerPreferences), - headers: {[MetaHeader.Version]: ForkName.gloas}, - }), - parseReqJson: ({body, headers}) => { - toForkName(fromHeaders(headers, MetaHeader.Version)); - return {signedProposerPreferences: SignedProposerPreferencesListType.fromJson(body)}; - }, - writeReqSsz: ({signedProposerPreferences}) => ({ - body: SignedProposerPreferencesListType.serialize(signedProposerPreferences), - headers: {[MetaHeader.Version]: ForkName.gloas}, - }), - parseReqSsz: ({body, headers}) => { - toForkName(fromHeaders(headers, MetaHeader.Version)); - return {signedProposerPreferences: SignedProposerPreferencesListType.deserialize(body)}; - }, - schema: { - body: Schema.ObjectArray, - headers: {[MetaHeader.Version]: Schema.String}, - }, - }, - resp: EmptyResponseCodec, - }, }; } diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 446424f8996c..3a801a32472c 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -4,6 +4,8 @@ import { ForkPostDeneb, ForkPostGloas, ForkPreDeneb, + MIN_SEED_LOOKAHEAD, + SLOTS_PER_EPOCH, VALIDATOR_REGISTRY_LIMIT, isForkPostDeneb, isForkPostElectra, @@ -246,6 +248,10 @@ export const SignedValidatorRegistrationV1ListType = ArrayOf( ssz.bellatrix.SignedValidatorRegistrationV1, VALIDATOR_REGISTRY_LIMIT ); +export const SignedProposerPreferencesListType = ArrayOf( + ssz.gloas.SignedProposerPreferences, + (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH +); export type ValidatorIndices = ValueOf; export type AttesterDuty = ValueOf; @@ -273,6 +279,7 @@ export type SyncCommitteeSelectionList = ValueOf; export type LivenessResponseDataList = ValueOf; export type SignedValidatorRegistrationV1List = ValueOf; +export type SignedProposerPreferencesList = ValueOf; export type Endpoints = { /** @@ -646,6 +653,20 @@ export type Endpoints = { EmptyResponseData, EmptyMeta >; + + /** + * Submit signed proposer preferences + * + * Verifies given signed proposer preferences and publishes them on the `proposer_preferences` + * gossipsub topic. Supersedes `prepareBeaconProposer` and `registerValidator` from Gloas onwards. + */ + submitProposerPreferences: Endpoint< + "POST", + {signedProposerPreferences: SignedProposerPreferencesList}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, + EmptyResponseData, + EmptyMeta + >; }; export function getDefinitions(config: ChainForkConfig): RouteDefinitions { @@ -1210,6 +1231,33 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ + body: SignedProposerPreferencesListType.toJson(signedProposerPreferences), + headers: {[MetaHeader.Version]: config.getForkName(signedProposerPreferences[0]?.message.proposalSlot ?? 0)}, + }), + parseReqJson: ({body, headers}) => { + toForkName(fromHeaders(headers, MetaHeader.Version)); + return {signedProposerPreferences: SignedProposerPreferencesListType.fromJson(body)}; + }, + writeReqSsz: ({signedProposerPreferences}) => ({ + body: SignedProposerPreferencesListType.serialize(signedProposerPreferences), + headers: {[MetaHeader.Version]: config.getForkName(signedProposerPreferences[0]?.message.proposalSlot ?? 0)}, + }), + parseReqSsz: ({body, headers}) => { + toForkName(fromHeaders(headers, MetaHeader.Version)); + return {signedProposerPreferences: SignedProposerPreferencesListType.deserialize(body)}; + }, + schema: { + body: Schema.ObjectArray, + headers: {[MetaHeader.Version]: Schema.String}, + }, + }, + resp: EmptyResponseCodec, + }, }; } diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 0d8bcf5613c1..5b28865f07f2 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -119,10 +119,6 @@ export const testData: GenericServerTestCases = { args: {slot: 1}, res: {data: [ssz.gloas.PayloadAttestation.defaultValue()], meta: {version: ForkName.gloas}}, }, - getPoolProposerPreferences: { - args: {slot: 1}, - res: {data: [ssz.gloas.SignedProposerPreferences.defaultValue()], meta: {version: ForkName.gloas}}, - }, getPoolAttesterSlashingsV2: { args: undefined, res: {data: [ssz.electra.AttesterSlashing.defaultValue()], meta: {version: ForkName.electra}}, @@ -167,10 +163,6 @@ export const testData: GenericServerTestCases = { args: {payloadAttestationMessages: [ssz.gloas.PayloadAttestationMessage.defaultValue()]}, res: undefined, }, - submitSignedProposerPreferences: { - args: {signedProposerPreferences: [ssz.gloas.SignedProposerPreferences.defaultValue()]}, - res: undefined, - }, // state diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 8b0ce6bb4394..f5e28f147c1f 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -296,10 +296,10 @@ export const eventTestData: EventData = { data: ssz.gloas.SignedProposerPreferences.fromJson({ message: { dependent_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - proposal_slot: "10", - validator_index: "42", + proposal_slot: "32", + validator_index: "123", fee_recipient: "0x0000000000000000000000000000000000000000", - target_gas_limit: "30000000", + target_gas_limit: "60000000", }, signature: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index ac7aa4272a77..0a783e0948b2 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -157,4 +157,8 @@ export const testData: GenericServerTestCases = { args: {registrations: [ssz.bellatrix.SignedValidatorRegistrationV1.defaultValue()]}, res: undefined, }, + submitProposerPreferences: { + args: {signedProposerPreferences: [ssz.gloas.SignedProposerPreferences.defaultValue()]}, + res: undefined, + }, }; diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 709d0d8d0f4e..b3f7db55224f 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -1,7 +1,6 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import { - ForkName, ForkPostElectra, ForkPreElectra, SYNC_COMMITTEE_SUBNET_SIZE, @@ -17,15 +16,12 @@ import { GossipAction, PayloadAttestationError, PayloadAttestationErrorCode, - ProposerPreferencesError, - ProposerPreferencesErrorCode, SyncCommitteeError, } from "../../../../chain/errors/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js"; import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js"; import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js"; -import {validateGossipProposerPreferences} from "../../../../chain/validation/proposerPreferences.js"; import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js"; import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js"; @@ -66,55 +62,6 @@ export function getBeaconPoolApi({ return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}}; }, - async getPoolProposerPreferences({slot}) { - const fork = chain.config.getForkName(slot ?? chain.clock.currentSlot); - if (!isForkPostGloas(fork)) { - throw new ApiError(400, `Proposer preferences pool is not supported before Gloas fork=${fork}`); - } - - return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}}; - }, - - async submitSignedProposerPreferences({signedProposerPreferences}) { - const failures: FailureList = []; - - await Promise.all( - signedProposerPreferences.map(async (signed, i) => { - try { - await validateGossipProposerPreferences(chain, signed); - - chain.proposerPreferencesPool.add(signed); - await network.publishProposerPreferences(signed); - chain.emitter.emit(routes.events.EventType.proposerPreferences, { - version: ForkName.gloas, - data: signed, - }); - } catch (e) { - const logCtx = { - slot: signed.message.proposalSlot, - validatorIndex: signed.message.validatorIndex, - dependentRoot: toRootHex(signed.message.dependentRoot), - }; - - if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) { - logger.debug("Ignoring known signed proposer preferences", logCtx); - return; - } - - failures.push({index: i, message: (e as Error).message}); - logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, e as Error); - if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject"); - } - } - }) - ); - - if (failures.length > 0) { - throw new IndexedError("Error processing signed proposer preferences", failures); - } - }, - async getPoolAttesterSlashingsV2() { const fork = chain.config.getForkName(chain.clock.currentSlot); return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: fork}}; diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 54516c7c2902..bfbcb7ece5bc 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -65,6 +65,8 @@ import { AttestationError, AttestationErrorCode, GossipAction, + ProposerPreferencesError, + ProposerPreferencesErrorCode, SyncCommitteeError, SyncCommitteeErrorCode, } from "../../../chain/errors/index.js"; @@ -74,6 +76,7 @@ import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/prod import {RegenCaller} from "../../../chain/regen/index.js"; import {CheckpointHex} from "../../../chain/stateCache/types.js"; import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; +import {validateGossipProposerPreferences} from "../../../chain/validation/proposerPreferences.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js"; import {ZERO_HASH} from "../../../constants/index.js"; import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js"; @@ -1765,6 +1768,46 @@ export function getValidatorApi( }); }, + async submitProposerPreferences({signedProposerPreferences}) { + const failures: FailureList = []; + + await Promise.all( + signedProposerPreferences.map(async (signed, i) => { + try { + await validateGossipProposerPreferences(chain, signed); + + chain.proposerPreferencesPool.add(signed); + await network.publishProposerPreferences(signed); + chain.emitter.emit(routes.events.EventType.proposerPreferences, { + version: config.getForkName(signed.message.proposalSlot), + data: signed, + }); + } catch (e) { + const logCtx = { + slot: signed.message.proposalSlot, + validatorIndex: signed.message.validatorIndex, + dependentRoot: toRootHex(signed.message.dependentRoot), + }; + + if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) { + logger.debug("Ignoring known signed proposer preferences", logCtx); + return; + } + + failures.push({index: i, message: (e as Error).message}); + logger.verbose(`Error on submitProposerPreferences [${i}]`, logCtx, e as Error); + if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) { + chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject"); + } + } + }) + ); + + if (failures.length > 0) { + throw new IndexedError("Error processing signed proposer preferences", failures); + } + }, + async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) { const fork = config.getForkName(slot); diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 040af011d81d..a0d706a9f6f8 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -1260,7 +1260,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand chain.proposerPreferencesPool.add(signedProposerPreferences); chain.emitter.emit(routes.events.EventType.proposerPreferences, { - version: ForkName.gloas, + version: config.getForkName(signedProposerPreferences.message.proposalSlot), data: signedProposerPreferences, }); }, diff --git a/packages/validator/src/services/proposerPreferences.ts b/packages/validator/src/services/proposerPreferences.ts index 0c400a3bcb5c..197ec41cd532 100644 --- a/packages/validator/src/services/proposerPreferences.ts +++ b/packages/validator/src/services/proposerPreferences.ts @@ -110,7 +110,7 @@ export class ProposerPreferencesService { } try { - await this.api.beacon.submitSignedProposerPreferences({signedProposerPreferences: batch}); + await this.api.validator.submitProposerPreferences({signedProposerPreferences: batch}); // Only mark as submitted after the API call succeeds; a thrown error leaves the // slot eligible for retry on the next tick. for (const {submission, slot: submittedSlot} of pending) {