Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/api/src/beacon/routes/beacon/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ForkPreElectra,
MAX_PAYLOAD_ATTESTATIONS,
PTC_SIZE,
SLOTS_PER_EPOCH,
isForkPostElectra,
} from "@lodestar/params";
import {
Expand Down Expand Up @@ -46,6 +47,12 @@ 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<typeof AttestationListTypePhase0>;
type AttestationListElectra = ValueOf<typeof AttestationListTypeElectra>;
Expand All @@ -61,6 +68,7 @@ type SignedBLSToExecutionChangeList = ValueOf<typeof SignedBLSToExecutionChangeL
type SyncCommitteeMessageList = ValueOf<typeof SyncCommitteeMessageListType>;
type PayloadAttestationList = ValueOf<typeof PayloadAttestationListType>;
type PayloadAttestationMessageList = ValueOf<typeof PayloadAttestationMessageListType>;
type SignedProposerPreferencesList = ValueOf<typeof SignedProposerPreferencesListType>;

export type Endpoints = {
/**
Expand Down Expand Up @@ -99,6 +107,18 @@ 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
Expand Down Expand Up @@ -279,6 +299,18 @@ 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<Endpoints> {
Expand Down Expand Up @@ -322,6 +354,19 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
meta: VersionCodec,
},
},
getPoolProposerPreferences: {
url: "/eth/v1/beacon/pool/proposer_preferences",
method: "GET",
req: {
writeReq: ({slot}) => ({query: {slot}}),
parseReq: ({query}) => ({slot: query.slot}),
schema: {query: {slot: Schema.Uint}},
},
resp: {
data: SignedProposerPreferencesListType,
meta: VersionCodec,
},
},
getPoolAttesterSlashings: {
url: "/eth/v1/beacon/pool/attester_slashings",
method: "GET",
Expand Down Expand Up @@ -574,5 +619,32 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
},
resp: EmptyResponseCodec,
},
submitSignedProposerPreferences: {
url: "/eth/v1/beacon/pool/proposer_preferences",
method: "POST",
req: {
writeReqJson: ({signedProposerPreferences}) => ({
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,
},
};
}
5 changes: 5 additions & 0 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export enum EventType {
executionPayloadAvailable = "execution_payload_available",
/** The node has received a `SignedExecutionPayloadBid` (from P2P or API) that passes gossip validation on the `execution_payload_bid` topic */
executionPayloadBid = "execution_payload_bid",
/** The node has received a `SignedProposerPreferences` (from P2P or API) that passes gossip validation on the `proposer_preferences` topic */
proposerPreferences = "proposer_preferences",
}

export const eventTypes: {[K in EventType]: K} = {
Expand All @@ -132,6 +134,7 @@ export const eventTypes: {[K in EventType]: K} = {
[EventType.executionPayloadGossip]: EventType.executionPayloadGossip,
[EventType.executionPayloadAvailable]: EventType.executionPayloadAvailable,
[EventType.executionPayloadBid]: EventType.executionPayloadBid,
[EventType.proposerPreferences]: EventType.proposerPreferences,
};

export type EventData = {
Expand Down Expand Up @@ -199,6 +202,7 @@ export type EventData = {
blockRoot: RootHex;
};
[EventType.executionPayloadBid]: {version: ForkName; data: gloas.SignedExecutionPayloadBid};
[EventType.proposerPreferences]: {version: ForkName; data: gloas.SignedProposerPreferences};
};

export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType];
Expand Down Expand Up @@ -395,6 +399,7 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
{jsonCase: "eth2"}
),
[EventType.executionPayloadBid]: WithVersion((fork) => getPostGloasForkTypes(fork).SignedExecutionPayloadBid),
[EventType.proposerPreferences]: WithVersion((fork) => getPostGloasForkTypes(fork).SignedProposerPreferences),

[EventType.lightClientOptimisticUpdate]: WithVersion(
(fork) => getPostAltairForkTypes(fork).LightClientOptimisticUpdate
Expand Down
8 changes: 8 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ export const testData: GenericServerTestCases<Endpoints> = {
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}},
},
getPoolAttesterSlashings: {
args: undefined,
res: {data: [ssz.phase0.AttesterSlashing.defaultValue()]},
Expand Down Expand Up @@ -187,6 +191,10 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {payloadAttestationMessages: [ssz.gloas.PayloadAttestationMessage.defaultValue()]},
res: undefined,
},
submitSignedProposerPreferences: {
args: {signedProposerPreferences: [ssz.gloas.SignedProposerPreferences.defaultValue()]},
res: undefined,
},

// state

Expand Down
14 changes: 14 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,20 @@ export const eventTestData: EventData = {
slot: 10,
blockRoot: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
},
[EventType.proposerPreferences]: {
version: ForkName.gloas,
data: ssz.gloas.SignedProposerPreferences.fromJson({
message: {
dependent_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
proposal_slot: "10",
validator_index: "42",
fee_recipient: "0x0000000000000000000000000000000000000000",
gas_limit: "30000000",
},
signature:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
}),
},
[EventType.executionPayloadBid]: {
version: ForkName.gloas,
data: ssz.gloas.SignedExecutionPayloadBid.fromJson({
Expand Down
53 changes: 53 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
import {
ForkName,
ForkPostElectra,
ForkPreElectra,
SYNC_COMMITTEE_SUBNET_SIZE,
Expand All @@ -16,12 +17,15 @@ 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";
Expand Down Expand Up @@ -81,6 +85,55 @@ 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 getPoolAttesterSlashings() {
const fork = chain.config.getForkName(chain.clock.currentSlot);

Expand Down
3 changes: 3 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
ExecutionPayloadBidPool,
OpPool,
PayloadAttestationPool,
ProposerPreferencesPool,
SyncCommitteeMessagePool,
SyncContributionAndProofPool,
} from "./opPools/index.js";
Expand Down Expand Up @@ -180,6 +181,7 @@ export class BeaconChain implements IBeaconChain {
readonly syncContributionAndProofPool;
readonly executionPayloadBidPool: ExecutionPayloadBidPool;
readonly payloadAttestationPool: PayloadAttestationPool;
readonly proposerPreferencesPool = new ProposerPreferencesPool();
readonly opPool: OpPool;

// Gossip seen cache
Expand Down Expand Up @@ -1462,6 +1464,7 @@ export class BeaconChain implements IBeaconChain {
this.executionPayloadBidPool.prune(slot);
this.seenExecutionPayloadBids.prune(slot);
this.seenProposerPreferences.prune(slot);
this.proposerPreferencesPool.prune(slot);
this.seenAttestationDatas.onSlot(slot);
this.reprocessController.onSlot(slot);

Expand Down
23 changes: 22 additions & 1 deletion packages/beacon-node/src/chain/errors/executionPayloadBid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export enum ExecutionPayloadBidErrorCode {
UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
INVALID_SLOT = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SLOT",
INVALID_SIGNATURE = "EXECUTION_PAYLOAD_BID_ERROR_INVALID_SIGNATURE",
NO_MATCHING_PROPOSER_PREFERENCES = "EXECUTION_PAYLOAD_BID_ERROR_NO_MATCHING_PROPOSER_PREFERENCES",
PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH",
PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH = "EXECUTION_PAYLOAD_BID_ERROR_PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH",
}

export type ExecutionPayloadBidErrorType =
Expand All @@ -36,6 +39,24 @@ export type ExecutionPayloadBidErrorType =
}
| {code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT; parentBlockRoot: RootHex}
| {code: ExecutionPayloadBidErrorCode.INVALID_SLOT; builderIndex: BuilderIndex; slot: Slot}
| {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot};
| {code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE; builderIndex: BuilderIndex; slot: Slot}
| {
code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES;
slot: Slot;
parentBlockRoot: RootHex;
dependentRoot: RootHex;
}
| {
code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH;
builderIndex: BuilderIndex;
bidFeeRecipient: string;
expectedFeeRecipient: string;
}
| {
code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH;
builderIndex: BuilderIndex;
bidGasLimit: number;
expectedGasLimit: number;
};

export class ExecutionPayloadBidError extends GossipActionError<ExecutionPayloadBidErrorType> {}
2 changes: 2 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
ExecutionPayloadBidPool,
OpPool,
PayloadAttestationPool,
ProposerPreferencesPool,
SyncCommitteeMessagePool,
SyncContributionAndProofPool,
} from "./opPools/index.js";
Expand Down Expand Up @@ -124,6 +125,7 @@ export interface IBeaconChain {
readonly syncContributionAndProofPool: SyncContributionAndProofPool;
readonly executionPayloadBidPool: ExecutionPayloadBidPool;
readonly payloadAttestationPool: PayloadAttestationPool;
readonly proposerPreferencesPool: ProposerPreferencesPool;
readonly opPool: OpPool;

// Gossip seen cache
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/opPools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export {AttestationPool} from "./attestationPool.js";
export {ExecutionPayloadBidPool} from "./executionPayloadBidPool.js";
export {OpPool} from "./opPool.js";
export {PayloadAttestationPool} from "./payloadAttestationPool.js";
export {ProposerPreferencesPool} from "./proposerPreferencesPool.js";
export {SyncCommitteeMessagePool} from "./syncCommitteeMessagePool.js";
export {SyncContributionAndProofPool} from "./syncContributionAndProofPool.js";
Loading
Loading