Skip to content

Commit

Permalink
SSE emit payload attributes before each slot (#5281)
Browse files Browse the repository at this point in the history
* SSE emit payload attributes before each slot

* add comment
  • Loading branch information
g11tech authored Mar 22, 2023
1 parent de53bad commit f28adf6
Show file tree
Hide file tree
Showing 14 changed files with 157 additions and 11 deletions.
10 changes: 9 additions & 1 deletion packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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} = {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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) =>
Expand Down
5 changes: 5 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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(),
},
};
2 changes: 2 additions & 0 deletions packages/beacon-node/src/chain/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type BlockProcessOpts = {
* will still issue fcU for block proposal
*/
disableImportExecutionFcU?: boolean;
emitPayloadAttributes?: boolean;
};

export const defaultChainOptions: IChainOptions = {
Expand All @@ -56,4 +57,5 @@ export const defaultChainOptions: IChainOptions = {
suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
assertCorrectProgressiveBalances: false,
archiveStateEpochFrequency: 1024,
emitPayloadAttributes: false,
};
18 changes: 16 additions & 2 deletions packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
Expand Down
49 changes: 47 additions & 2 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<allForks.SSEPayloadAttributes> {
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 */
21 changes: 15 additions & 6 deletions packages/beacon-node/test/unit/chain/prepareNextSlot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -33,7 +36,8 @@ describe("PrepareNextSlot scheduler", () => {
let getForkStub: SinonStubFn<typeof config["getForkName"]>;
let updateBuilderStatus: SinonStubFn<IBeaconChain["updateBuilderStatus"]>;
let executionEngineStub: SinonStubbedInstance<ExecutionEngineHttp> & ExecutionEngineHttp;

const emitPayloadAttributes = true;
const proposerIndex = 0;
beforeEach(() => {
sandbox.useFakeTimers();
chainStub = sandbox.createStubInstance(BeaconChain) as StubbedChain;
Expand All @@ -42,9 +46,8 @@ describe("PrepareNextSlot scheduler", () => {
chainStub.clock = clockStub;
forkChoiceStub = sandbox.createStubInstance(ForkChoice) as SinonStubbedInstance<ForkChoice> & ForkChoice;
chainStub.forkChoice = forkChoiceStub;
const emitterStub = sandbox.createStubInstance(ChainEventEmitter) as SinonStubbedInstance<ChainEventEmitter> &
ChainEventEmitter;
chainStub.emitter = emitterStub;
const emitter = new ChainEventEmitter();
chainStub.emitter = emitter;
regenStub = sandbox.createStubInstance(StateRegenerator) as SinonStubbedInstance<StateRegenerator> &
StateRegenerator;
chainStub.regen = regenStub;
Expand All @@ -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);
});

Expand Down Expand Up @@ -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();
Expand All @@ -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;
});
});
9 changes: 9 additions & 0 deletions packages/cli/src/options/beaconNodeOptions/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"] {
Expand All @@ -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"],
};
}

Expand All @@ -47,6 +49,13 @@ export const options: CliCommandOptions<ChainArgs> = {
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",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/unit/options/beaconNodeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -115,6 +116,7 @@ describe("options / beaconNodeOptions", () => {
assertCorrectProgressiveBalances: true,
maxSkipSlots: 100,
archiveStateEpochFrequency: 1024,
emitPayloadAttributes: false,
},
eth1: {
enabled: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/allForks/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const allForksExecution = {
ExecutionPayloadHeader: bellatrix.ExecutionPayloadHeader,
BuilderBid: bellatrix.BuilderBid,
SignedBuilderBid: bellatrix.SignedBuilderBid,
SSEPayloadAttributes: bellatrix.SSEPayloadAttributes,
},
capella: {
BeaconBlockBody: capella.BeaconBlockBody,
Expand All @@ -71,6 +72,7 @@ export const allForksExecution = {
ExecutionPayloadHeader: capella.ExecutionPayloadHeader,
BuilderBid: capella.BuilderBid,
SignedBuilderBid: capella.SignedBuilderBid,
SSEPayloadAttributes: capella.SSEPayloadAttributes,
},
deneb: {
BeaconBlockBody: deneb.BeaconBlockBody,
Expand All @@ -81,6 +83,7 @@ export const allForksExecution = {
ExecutionPayloadHeader: deneb.ExecutionPayloadHeader,
BuilderBid: deneb.BuilderBid,
SignedBuilderBid: deneb.SignedBuilderBid,
SSEPayloadAttributes: capella.SSEPayloadAttributes,
},
};

Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/allForks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 = {
Expand Down
25 changes: 25 additions & 0 deletions packages/types/src/bellatrix/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
);
1 change: 1 addition & 0 deletions packages/types/src/bellatrix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export type ValidatorRegistrationV1 = ValueOf<typeof ssz.ValidatorRegistrationV1
export type SignedValidatorRegistrationV1 = ValueOf<typeof ssz.SignedValidatorRegistrationV1>;
export type BuilderBid = ValueOf<typeof ssz.BuilderBid>;
export type SignedBuilderBid = ValueOf<typeof ssz.SignedBuilderBid>;
export type SSEPayloadAttributes = ValueOf<typeof ssz.SSEPayloadAttributes>;

export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader;
Loading

0 comments on commit f28adf6

Please sign in to comment.