From f28adf60cb9bed448d362fcd1af117d68ae71536 Mon Sep 17 00:00:00 2001 From: g11tech Date: Wed, 22 Mar 2023 16:39:46 +0530 Subject: [PATCH] SSE emit payload attributes before each slot (#5281) * SSE emit payload attributes before each slot * add comment --- packages/api/src/beacon/routes/events.ts | 10 +++- .../api/test/unit/beacon/testData/events.ts | 5 ++ packages/beacon-node/src/chain/options.ts | 2 + .../beacon-node/src/chain/prepareNextSlot.ts | 18 ++++++- .../chain/produceBlock/produceBlockBody.ts | 49 ++++++++++++++++++- .../test/unit/chain/prepareNextSlot.test.ts | 21 +++++--- .../src/options/beaconNodeOptions/chain.ts | 9 ++++ .../unit/options/beaconNodeOptions.test.ts | 2 + packages/types/src/allForks/sszTypes.ts | 3 ++ packages/types/src/allForks/types.ts | 5 ++ packages/types/src/bellatrix/sszTypes.ts | 25 ++++++++++ packages/types/src/bellatrix/types.ts | 1 + packages/types/src/capella/sszTypes.ts | 17 +++++++ packages/types/src/capella/types.ts | 1 + 14 files changed, 157 insertions(+), 11 deletions(-) diff --git a/packages/api/src/beacon/routes/events.ts b/packages/api/src/beacon/routes/events.ts index b75d802faeff..0f504e3f5af9 100644 --- a/packages/api/src/beacon/routes/events.ts +++ b/packages/api/src/beacon/routes/events.ts @@ -1,8 +1,9 @@ import {Epoch, phase0, capella, Slot, ssz, StringType, RootHex, altair, UintNum64, allForks} from "@lodestar/types"; import {ContainerType} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; +import {isForkExecution, ForkName} from "@lodestar/params"; -import {RouteDef, TypeJson} from "../../utils/index.js"; +import {RouteDef, TypeJson, WithVersion} from "../../utils/index.js"; import {HttpStatusCode} from "../../utils/client/httpStatusCode.js"; import {ApiClientResponse} from "../../interfaces.js"; @@ -36,6 +37,8 @@ export enum EventType { lightClientFinalityUpdate = "light_client_finality_update", /** New or better light client update available */ lightClientUpdate = "light_client_update", + /** Payload attributes for block proposal */ + payloadAttributes = "payload_attributes", } export const eventTypes: {[K in EventType]: K} = { @@ -50,6 +53,7 @@ export const eventTypes: {[K in EventType]: K} = { [EventType.lightClientOptimisticUpdate]: EventType.lightClientOptimisticUpdate, [EventType.lightClientFinalityUpdate]: EventType.lightClientFinalityUpdate, [EventType.lightClientUpdate]: EventType.lightClientUpdate, + [EventType.payloadAttributes]: EventType.payloadAttributes, }; export type EventData = { @@ -90,6 +94,7 @@ export type EventData = { [EventType.lightClientOptimisticUpdate]: allForks.LightClientOptimisticUpdate; [EventType.lightClientFinalityUpdate]: allForks.LightClientFinalityUpdate; [EventType.lightClientUpdate]: allForks.LightClientUpdate; + [EventType.payloadAttributes]: {version: ForkName; data: allForks.SSEPayloadAttributes}; }; export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType]; @@ -182,6 +187,9 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type ), [EventType.contributionAndProof]: ssz.altair.SignedContributionAndProof, + [EventType.payloadAttributes]: WithVersion((fork) => + isForkExecution(fork) ? ssz.allForksExecution[fork].SSEPayloadAttributes : ssz.bellatrix.SSEPayloadAttributes + ), [EventType.lightClientOptimisticUpdate]: { toJson: (data) => diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 1e1bfdfc5500..cfd976cd8578 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -1,4 +1,5 @@ import {ssz} from "@lodestar/types"; +import {ForkName} from "@lodestar/params"; import {Api, EventData, EventType} from "../../../../src/beacon/routes/events.js"; import {GenericServerTestCases} from "../../../utils/genericServerTest.js"; @@ -104,4 +105,8 @@ export const eventTestData: EventData = { signatureSlot: ssz.Slot.defaultValue(), }, [EventType.lightClientUpdate]: ssz.altair.LightClientUpdate.defaultValue(), + [EventType.payloadAttributes]: { + version: ForkName.bellatrix, + data: ssz.bellatrix.SSEPayloadAttributes.defaultValue(), + }, }; diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 0024244f0787..5f9254e4fa45 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -44,6 +44,7 @@ export type BlockProcessOpts = { * will still issue fcU for block proposal */ disableImportExecutionFcU?: boolean; + emitPayloadAttributes?: boolean; }; export const defaultChainOptions: IChainOptions = { @@ -56,4 +57,5 @@ export const defaultChainOptions: IChainOptions = { suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient, assertCorrectProgressiveBalances: false, archiveStateEpochFrequency: 1024, + emitPayloadAttributes: false, }; diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index dc4a9184fca8..f969429ed77f 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -2,12 +2,13 @@ import {computeEpochAtSlot, isExecutionStateType, computeTimeAtSlot} from "@lode import {ChainForkConfig} from "@lodestar/config"; import {ForkSeq, SLOTS_PER_EPOCH, ForkExecution} from "@lodestar/params"; import {Slot} from "@lodestar/types"; -import {Logger, sleep} from "@lodestar/utils"; +import {Logger, sleep, fromHex} from "@lodestar/utils"; +import {routes} from "@lodestar/api"; import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js"; import {Metrics} from "../metrics/index.js"; import {TransitionConfigurationV1} from "../execution/engine/interface.js"; import {ChainEvent} from "./emitter.js"; -import {prepareExecutionPayload} from "./produceBlock/produceBlockBody.js"; +import {prepareExecutionPayload, getPayloadAttributesForSSE} from "./produceBlock/produceBlockBody.js"; import {IBeaconChain} from "./interface.js"; import {RegenCaller} from "./regen/index.js"; @@ -156,6 +157,19 @@ export class PrepareNextSlotScheduler { feeRecipient, }); } + + // If emitPayloadAttributes is true emit a SSE payloadAttributes event + if (this.chain.opts.emitPayloadAttributes === true) { + const data = await getPayloadAttributesForSSE(fork as ForkExecution, this.chain, { + prepareState, + prepareSlot, + parentBlockRoot: fromHex(headRoot), + // The likely consumers of this API are builders and will anyway ignore the + // feeRecipient, so just pass zero hash for now till a real use case arises + feeRecipient: "0x0000000000000000000000000000000000000000000000000000000000000000", + }); + this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork}); + } } } catch (e) { this.metrics?.precomputeNextEpochTransition.count.inc({result: "error"}, 1); diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index a95166897f60..7541c2a19062 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -28,7 +28,7 @@ import { } from "@lodestar/state-transition"; import {ChainForkConfig} from "@lodestar/config"; import {ForkSeq, ForkExecution, isForkExecution} from "@lodestar/params"; -import {toHex, sleep, Logger} from "@lodestar/utils"; +import {toHex, sleep, Logger, fromHex} from "@lodestar/utils"; import type {BeaconChain} from "../chain.js"; import {PayloadId, IExecutionEngine, IExecutionBuilder, PayloadAttributes} from "../../execution/index.js"; @@ -418,7 +418,7 @@ async function prepareExecutionPayloadHeader( return chain.executionBuilder.getHeader(state.slot, parentHash, proposerPubKey); } -async function getExecutionPayloadParentHash( +export async function getExecutionPayloadParentHash( chain: { eth1: IEth1ForBlockProduction; config: ChainForkConfig; @@ -452,4 +452,49 @@ async function getExecutionPayloadParentHash( } } +export async function getPayloadAttributesForSSE( + fork: ForkExecution, + chain: { + eth1: IEth1ForBlockProduction; + config: ChainForkConfig; + }, + { + prepareState, + prepareSlot, + parentBlockRoot, + feeRecipient, + }: {prepareState: CachedBeaconStateExecutions; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string} +): Promise { + const parentHashRes = await getExecutionPayloadParentHash(chain, prepareState); + + if (!parentHashRes.isPremerge) { + const {parentHash} = parentHashRes; + const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); + const prevRandao = getRandaoMix(prepareState, prepareState.epochCtx.epoch); + const payloadAttributes = { + timestamp, + prevRandao, + suggestedFeeRecipient: fromHex(feeRecipient), + }; + + if (ForkSeq[fork] >= ForkSeq.capella) { + (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = getExpectedWithdrawals( + prepareState as CachedBeaconStateCapella + ).withdrawals; + } + + const ssePayloadAttributes: allForks.SSEPayloadAttributes = { + proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot), + proposalSlot: prepareSlot, + proposalBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber + 1, + parentBlockRoot, + parentBlockHash: parentHash, + payloadAttributes, + }; + return ssePayloadAttributes; + } else { + throw Error("The execution is still pre-merge"); + } +} + /** process_sync_committee_contributions is implemented in syncCommitteeContribution.getSyncAggregate */ diff --git a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts index 89f4bf1f79b1..0a03aceddba1 100644 --- a/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts +++ b/packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts @@ -5,8 +5,10 @@ import {ForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {WinstonLogger} from "@lodestar/utils"; import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {ChainForkConfig} from "@lodestar/config"; +import {routes} from "@lodestar/api"; import {BeaconChain, ChainEventEmitter} from "../../../src/chain/index.js"; import {IBeaconChain} from "../../../src/chain/interface.js"; +import {IChainOptions} from "../../../src/chain/options.js"; import {LocalClock} from "../../../src/chain/clock/index.js"; import {PrepareNextSlotScheduler} from "../../../src/chain/prepareNextSlot.js"; import {StateRegenerator} from "../../../src/chain/regen/index.js"; @@ -17,8 +19,9 @@ import {PayloadIdCache} from "../../../src/execution/engine/payloadIdCache.js"; import {ExecutionEngineHttp} from "../../../src/execution/engine/http.js"; import {IExecutionEngine} from "../../../src/execution/engine/interface.js"; import {StubbedChainMutable} from "../../utils/stub/index.js"; +import {zeroProtoBlock} from "../../utils/mocks/chain/chain.js"; -type StubbedChain = StubbedChainMutable<"clock" | "forkChoice" | "emitter" | "regen">; +type StubbedChain = StubbedChainMutable<"clock" | "forkChoice" | "emitter" | "regen" | "opts">; describe("PrepareNextSlot scheduler", () => { const sandbox = sinon.createSandbox(); @@ -33,7 +36,8 @@ describe("PrepareNextSlot scheduler", () => { let getForkStub: SinonStubFn; let updateBuilderStatus: SinonStubFn; let executionEngineStub: SinonStubbedInstance & ExecutionEngineHttp; - + const emitPayloadAttributes = true; + const proposerIndex = 0; beforeEach(() => { sandbox.useFakeTimers(); chainStub = sandbox.createStubInstance(BeaconChain) as StubbedChain; @@ -42,9 +46,8 @@ describe("PrepareNextSlot scheduler", () => { chainStub.clock = clockStub; forkChoiceStub = sandbox.createStubInstance(ForkChoice) as SinonStubbedInstance & ForkChoice; chainStub.forkChoice = forkChoiceStub; - const emitterStub = sandbox.createStubInstance(ChainEventEmitter) as SinonStubbedInstance & - ChainEventEmitter; - chainStub.emitter = emitterStub; + const emitter = new ChainEventEmitter(); + chainStub.emitter = emitter; regenStub = sandbox.createStubInstance(StateRegenerator) as SinonStubbedInstance & StateRegenerator; chainStub.regen = regenStub; @@ -60,6 +63,8 @@ describe("PrepareNextSlot scheduler", () => { ExecutionEngineHttp; ((chainStub as unknown) as {executionEngine: IExecutionEngine}).executionEngine = executionEngineStub; ((chainStub as unknown) as {config: ChainForkConfig}).config = (config as unknown) as ChainForkConfig; + chainStub.opts = {emitPayloadAttributes} as IChainOptions; + scheduler = new PrepareNextSlotScheduler(chainStub, config, null, loggerStub, abortController.signal); }); @@ -135,12 +140,15 @@ describe("PrepareNextSlot scheduler", () => { }); it("bellatrix - should prepare payload", async () => { + const spy = sinon.spy(); + chainStub.emitter.on(routes.events.EventType.payloadAttributes, spy); getForkStub.returns(ForkName.bellatrix); - chainStub.recomputeForkChoiceHead.returns({slot: SLOTS_PER_EPOCH - 3} as ProtoBlock); + chainStub.recomputeForkChoiceHead.returns({...zeroProtoBlock, slot: SLOTS_PER_EPOCH - 3} as ProtoBlock); forkChoiceStub.getJustifiedBlock.returns({} as ProtoBlock); forkChoiceStub.getFinalizedBlock.returns({} as ProtoBlock); updateBuilderStatus.returns(void 0); const state = generateCachedBellatrixState(); + sinon.stub(state.epochCtx, "getBeaconProposer").returns(proposerIndex); regenStub.getBlockSlotState.resolves(state); beaconProposerCacheStub.get.returns("0x fee recipient address"); ((executionEngineStub as unknown) as {payloadIdCache: PayloadIdCache}).payloadIdCache = new PayloadIdCache(); @@ -157,5 +165,6 @@ describe("PrepareNextSlot scheduler", () => { expect(forkChoiceStub.getFinalizedBlock, "expect forkChoice.getFinalizedBlock to be called").to.be.called; expect(executionEngineStub.notifyForkchoiceUpdate, "expect executionEngine.notifyForkchoiceUpdate to be called").to .be.calledOnce; + expect(spy).to.be.calledOnce; }); }); diff --git a/packages/cli/src/options/beaconNodeOptions/chain.ts b/packages/cli/src/options/beaconNodeOptions/chain.ts index 1a1b18e811ba..f21a14b830d8 100644 --- a/packages/cli/src/options/beaconNodeOptions/chain.ts +++ b/packages/cli/src/options/beaconNodeOptions/chain.ts @@ -17,6 +17,7 @@ export type ChainArgs = { "chain.maxSkipSlots": number; "safe-slots-to-import-optimistically": number; "chain.archiveStateEpochFrequency": number; + emitPayloadAttributes: boolean; }; export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { @@ -35,6 +36,7 @@ export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { maxSkipSlots: args["chain.maxSkipSlots"], safeSlotsToImportOptimistically: args["safe-slots-to-import-optimistically"], archiveStateEpochFrequency: args["chain.archiveStateEpochFrequency"], + emitPayloadAttributes: args["emitPayloadAttributes"], }; } @@ -47,6 +49,13 @@ export const options: CliCommandOptions = { group: "chain", }, + emitPayloadAttributes: { + type: "boolean", + defaultDescription: String(defaultOptions.chain.emitPayloadAttributes), + description: "Flag to SSE emit execution payloadAttributes before every slot", + group: "chain", + }, + "chain.blsVerifyAllMultiThread": { hidden: true, type: "boolean", diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index 44946258f76e..080d2436c476 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -29,6 +29,7 @@ describe("options / beaconNodeOptions", () => { "chain.maxSkipSlots": 100, "safe-slots-to-import-optimistically": 256, "chain.archiveStateEpochFrequency": 1024, + emitPayloadAttributes: false, eth1: true, "eth1.providerUrl": "http://my.node:8545", @@ -115,6 +116,7 @@ describe("options / beaconNodeOptions", () => { assertCorrectProgressiveBalances: true, maxSkipSlots: 100, archiveStateEpochFrequency: 1024, + emitPayloadAttributes: false, }, eth1: { enabled: true, diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index d55e45269e0e..a38fc8d63d75 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -60,6 +60,7 @@ export const allForksExecution = { ExecutionPayloadHeader: bellatrix.ExecutionPayloadHeader, BuilderBid: bellatrix.BuilderBid, SignedBuilderBid: bellatrix.SignedBuilderBid, + SSEPayloadAttributes: bellatrix.SSEPayloadAttributes, }, capella: { BeaconBlockBody: capella.BeaconBlockBody, @@ -71,6 +72,7 @@ export const allForksExecution = { ExecutionPayloadHeader: capella.ExecutionPayloadHeader, BuilderBid: capella.BuilderBid, SignedBuilderBid: capella.SignedBuilderBid, + SSEPayloadAttributes: capella.SSEPayloadAttributes, }, deneb: { BeaconBlockBody: deneb.BeaconBlockBody, @@ -81,6 +83,7 @@ export const allForksExecution = { ExecutionPayloadHeader: deneb.ExecutionPayloadHeader, BuilderBid: deneb.BuilderBid, SignedBuilderBid: deneb.SignedBuilderBid, + SSEPayloadAttributes: capella.SSEPayloadAttributes, }, }; diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index d03bb1e20d3d..4f906926a5cb 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -88,6 +88,8 @@ export type LightClientOptimisticUpdate = export type LightClientStore = altair.LightClientStore | capella.LightClientStore | deneb.LightClientStore; export type SignedBeaconBlockAndBlobsSidecar = deneb.SignedBeaconBlockAndBlobsSidecar; + +export type SSEPayloadAttributes = bellatrix.SSEPayloadAttributes | capella.SSEPayloadAttributes; /** * Types known to change between forks */ @@ -214,6 +216,9 @@ export type AllForksExecutionSSZTypes = { SignedBuilderBid: AllForksTypeOf< typeof bellatrixSsz.SignedBuilderBid | typeof capellaSsz.SignedBuilderBid | typeof denebSsz.SignedBuilderBid >; + SSEPayloadAttributes: AllForksTypeOf< + typeof bellatrixSsz.SSEPayloadAttributes | typeof capellaSsz.SSEPayloadAttributes + >; }; export type AllForksBlindedSSZTypes = { diff --git a/packages/types/src/bellatrix/sszTypes.ts b/packages/types/src/bellatrix/sszTypes.ts index 51b1aac37827..5604ecf72ab6 100644 --- a/packages/types/src/bellatrix/sszTypes.ts +++ b/packages/types/src/bellatrix/sszTypes.ts @@ -215,3 +215,28 @@ export const SignedBuilderBid = new ContainerType( }, {typeName: "SignedBuilderBid", jsonCase: "eth2"} ); + +// PayloadAttributes primarily for SSE event +export const PayloadAttributes = new ContainerType( + {timestamp: UintNum64, prevRandao: Bytes32, suggestedFeeRecipient: ExecutionAddress}, + {typeName: "PayloadAttributes", jsonCase: "eth2"} +); + +export const SSEPayloadAttributesCommon = new ContainerType( + { + proposerIndex: UintNum64, + proposalSlot: Slot, + proposalBlockNumber: UintNum64, + parentBlockRoot: Root, + parentBlockHash: Root, + }, + {typeName: "SSEPayloadAttributesCommon", jsonCase: "eth2"} +); + +export const SSEPayloadAttributes = new ContainerType( + { + ...SSEPayloadAttributesCommon.fields, + payloadAttributes: PayloadAttributes, + }, + {typeName: "SSEPayloadAttributes", jsonCase: "eth2"} +); diff --git a/packages/types/src/bellatrix/types.ts b/packages/types/src/bellatrix/types.ts index b18560bcd348..f322e09920c3 100644 --- a/packages/types/src/bellatrix/types.ts +++ b/packages/types/src/bellatrix/types.ts @@ -18,5 +18,6 @@ export type ValidatorRegistrationV1 = ValueOf; export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; +export type SSEPayloadAttributes = ValueOf; export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; diff --git a/packages/types/src/capella/sszTypes.ts b/packages/types/src/capella/sszTypes.ts index 858de7d635bd..e5646eea6674 100644 --- a/packages/types/src/capella/sszTypes.ts +++ b/packages/types/src/capella/sszTypes.ts @@ -260,3 +260,20 @@ export const LightClientStore = new ContainerType( }, {typeName: "LightClientStore", jsonCase: "eth2"} ); + +// PayloadAttributes primarily for SSE event +export const PayloadAttributes = new ContainerType( + { + ...bellatrixSsz.PayloadAttributes.fields, + withdrawals: Withdrawals, + }, + {typeName: "PayloadAttributes", jsonCase: "eth2"} +); + +export const SSEPayloadAttributes = new ContainerType( + { + ...bellatrixSsz.SSEPayloadAttributesCommon.fields, + payloadAttributes: PayloadAttributes, + }, + {typeName: "SSEPayloadAttributes", jsonCase: "eth2"} +); diff --git a/packages/types/src/capella/types.ts b/packages/types/src/capella/types.ts index 6d2e09d5ca2a..386f96ecd280 100644 --- a/packages/types/src/capella/types.ts +++ b/packages/types/src/capella/types.ts @@ -23,6 +23,7 @@ export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadH export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; +export type SSEPayloadAttributes = ValueOf; export type LightClientHeader = ValueOf; export type LightClientBootstrap = ValueOf;