Skip to content

Commit

Permalink
feat: add consensus_block_value to produceBlockV3 (#6136)
Browse files Browse the repository at this point in the history
* Initial commit

* Add reward calculation

* Add todos

* Update productBlockV3 unit test

* Fix lint

* Get proper pre-state

* Fix test

* Simplify approach

* Code cleanup

* Fix test

* Fix test

* Introduce reward cache

* Fix bug

* Remove withConsensusBlockValue

* Update naming for better readability

* Update logic to choose block source

* Update packages/beacon-node/src/api/impl/validator/index.ts

* add some more debug logs

* fix rebase

---------

Co-authored-by: g11tech <develop@g11tech.io>
  • Loading branch information
ensi321 and g11tech authored Dec 8, 2023
1 parent 53378e1 commit 42b2efe
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 119 deletions.
11 changes: 6 additions & 5 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
StringType,
SubcommitteeIndex,
Wei,
Gwei,
} from "@lodestar/types";
import {ApiClientResponse} from "../../interfaces.js";
import {HttpStatusCode} from "../../utils/client/httpStatusCode.js";
Expand All @@ -27,7 +28,7 @@ import {
ArrayOf,
Schema,
WithVersion,
WithExecutionPayloadValue,
WithBlockValues,
reqOnlyBody,
ReqSerializers,
jsonType,
Expand All @@ -54,11 +55,11 @@ export type ExtraProduceBlockOps = {
strictFeeRecipientCheck?: boolean;
};

export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei} & (
export type ProduceBlockOrContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Gwei} & (
| {data: allForks.BeaconBlock; version: ForkPreBlobs}
| {data: allForks.BlockContents; version: ForkBlobs}
);
export type ProduceBlindedBlockOrContentsRes = {executionPayloadValue: Wei} & (
export type ProduceBlindedBlockOrContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Gwei} & (
| {data: allForks.BlindedBeaconBlock; version: ForkPreBlobs}
| {data: allForks.BlindedBlockContents; version: ForkBlobs}
);
Expand Down Expand Up @@ -715,12 +716,12 @@ export function getReturnTypes(): ReturnTypes<Api> {
{jsonCase: "eth2"}
);

const produceBlockOrContents = WithExecutionPayloadValue(
const produceBlockOrContents = WithBlockValues(
WithVersion<allForks.BeaconBlockOrContents>((fork: ForkName) =>
isForkBlobs(fork) ? allForksBlockContentsResSerializer(fork) : ssz[fork].BeaconBlock
)
) as TypeJson<ProduceBlockOrContentsRes>;
const produceBlindedBlockOrContents = WithExecutionPayloadValue(
const produceBlindedBlockOrContents = WithBlockValues(
WithVersion<allForks.BlindedBeaconBlockOrContents>((fork: ForkName) =>
isForkBlobs(fork)
? allForksBlindedBlockContentsResSerializer(fork)
Expand Down
16 changes: 11 additions & 5 deletions packages/api/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,20 +179,26 @@ export function WithExecutionOptimistic<T extends {data: unknown}>(
}

/**
* SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei}`
* SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei, consensusBlockValue: GWei}`
*/
export function WithExecutionPayloadValue<T extends {data: unknown}>(
export function WithBlockValues<T extends {data: unknown}>(
type: TypeJson<T>
): TypeJson<T & {executionPayloadValue: bigint}> {
): TypeJson<T & {executionPayloadValue: bigint; consensusBlockValue: bigint}> {
return {
toJson: ({executionPayloadValue, ...data}) => ({
toJson: ({executionPayloadValue, consensusBlockValue, ...data}) => ({
...(type.toJson(data as unknown as T) as Record<string, unknown>),
execution_payload_value: executionPayloadValue.toString(),
consensus_block_value: consensusBlockValue.toString(),
}),
fromJson: ({execution_payload_value, ...data}: T & {execution_payload_value: string}) => ({
fromJson: ({
execution_payload_value,
consensus_block_value,
...data
}: T & {execution_payload_value: string; consensus_block_value: string}) => ({
...type.fromJson(data),
// For cross client usage where beacon or validator are of separate clients, executionPayloadValue could be missing
executionPayloadValue: BigInt(execution_payload_value ?? "0"),
consensusBlockValue: BigInt(consensus_block_value ?? "0"),
}),
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const testData: GenericServerTestCases<Api> = {
data: ssz.altair.BeaconBlock.defaultValue(),
version: ForkName.altair,
executionPayloadValue: ssz.Wei.defaultValue(),
consensusBlockValue: ssz.Gwei.defaultValue(),
},
},
produceBlockV3: {
Expand All @@ -68,6 +69,7 @@ export const testData: GenericServerTestCases<Api> = {
data: ssz.altair.BeaconBlock.defaultValue(),
version: ForkName.altair,
executionPayloadValue: ssz.Wei.defaultValue(),
consensusBlockValue: ssz.Gwei.defaultValue(),
executionPayloadBlinded: false,
},
},
Expand All @@ -77,6 +79,7 @@ export const testData: GenericServerTestCases<Api> = {
data: ssz.bellatrix.BlindedBeaconBlock.defaultValue(),
version: ForkName.bellatrix,
executionPayloadValue: ssz.Wei.defaultValue(),
consensusBlockValue: ssz.Gwei.defaultValue(),
},
},
produceAttestationData: {
Expand Down
36 changes: 27 additions & 9 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export function getValidatorApi({
let timer;
try {
timer = metrics?.blockProductionTime.startTimer();
const {block, executionPayloadValue} = await chain.produceBlindedBlock({
const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlindedBlock({
slot,
randaoReveal,
graffiti: toGraffitiBuffer(graffiti || ""),
Expand All @@ -325,6 +325,7 @@ export function getValidatorApi({
logger.verbose("Produced blinded block", {
slot,
executionPayloadValue,
consensusBlockValue,
root: toHexString(config.getBlindedForkTypes(slot).BeaconBlock.hashTreeRoot(block)),
});

Expand All @@ -342,9 +343,10 @@ export function getValidatorApi({
data: {blindedBlock: block, blindedBlobSidecars} as allForks.BlindedBlockContents,
version,
executionPayloadValue,
consensusBlockValue,
};
} else {
return {data: block, version, executionPayloadValue};
return {data: block, version, executionPayloadValue, consensusBlockValue};
}
} finally {
if (timer) timer({source});
Expand Down Expand Up @@ -378,13 +380,12 @@ export function getValidatorApi({
let timer;
try {
timer = metrics?.blockProductionTime.startTimer();
const {block, executionPayloadValue} = await chain.produceBlock({
const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlock({
slot,
randaoReveal,
graffiti: toGraffitiBuffer(graffiti || ""),
feeRecipient,
});

const version = config.getForkName(block.slot);
if (strictFeeRecipientCheck && feeRecipient && isForkExecution(version)) {
const blockFeeRecipient = toHexString((block as bellatrix.BeaconBlock).body.executionPayload.feeRecipient);
Expand All @@ -398,6 +399,7 @@ export function getValidatorApi({
logger.verbose("Produced execution block", {
slot,
executionPayloadValue,
consensusBlockValue,
root: toHexString(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)),
});
if (chain.opts.persistProducedBlocks) {
Expand All @@ -409,9 +411,14 @@ export function getValidatorApi({
if (blobSidecars === undefined) {
throw Error("blobSidecars missing in cache");
}
return {data: {block, blobSidecars} as allForks.BlockContents, version, executionPayloadValue};
return {
data: {block, blobSidecars} as allForks.BlockContents,
version,
executionPayloadValue,
consensusBlockValue,
};
} else {
return {data: block, version, executionPayloadValue};
return {data: block, version, executionPayloadValue, consensusBlockValue};
}
} finally {
if (timer) timer({source});
Expand Down Expand Up @@ -531,15 +538,18 @@ export function getValidatorApi({

const builderPayloadValue = blindedBlock?.executionPayloadValue ?? BigInt(0);
const enginePayloadValue = fullBlock?.executionPayloadValue ?? BigInt(0);
const consensusBlockValueBuilder = blindedBlock?.consensusBlockValue ?? BigInt(0);
const consensusBlockValueEngine = fullBlock?.consensusBlockValue ?? BigInt(0);

const blockValueBuilder = builderPayloadValue + consensusBlockValueBuilder;
const blockValueEngine = enginePayloadValue + consensusBlockValueEngine;

let selectedSource: ProducedBlockSource | null = null;

if (fullBlock && blindedBlock) {
switch (builderSelection) {
case routes.validator.BuilderSelection.MaxProfit: {
// If executionPayloadValues are zero, than choose builder as most likely beacon didn't provide executionPayloadValue
// and builder blocks are most likely thresholded by a min bid
if (enginePayloadValue >= builderPayloadValue && enginePayloadValue !== BigInt(0)) {
if (blockValueEngine >= blockValueBuilder) {
selectedSource = ProducedBlockSource.engine;
} else {
selectedSource = ProducedBlockSource.builder;
Expand All @@ -562,20 +572,28 @@ export function getValidatorApi({
// winston logger doesn't like bigint
enginePayloadValue: `${enginePayloadValue}`,
builderPayloadValue: `${builderPayloadValue}`,
consensusBlockValueEngine: `${consensusBlockValueEngine}`,
consensusBlockValueBuilder: `${consensusBlockValueBuilder}`,
blockValueEngine: `${blockValueEngine}`,
blockValueBuilder: `${blockValueBuilder}`,
slot,
});
} else if (fullBlock && !blindedBlock) {
selectedSource = ProducedBlockSource.engine;
logger.verbose("Selected engine block: no builder block produced", {
// winston logger doesn't like bigint
enginePayloadValue: `${enginePayloadValue}`,
consensusBlockValueEngine: `${consensusBlockValueEngine}`,
blockValueEngine: `${blockValueEngine}`,
slot,
});
} else if (blindedBlock && !fullBlock) {
selectedSource = ProducedBlockSource.builder;
logger.verbose("Selected builder block: no engine block produced", {
// winston logger doesn't like bigint
builderPayloadValue: `${builderPayloadValue}`,
consensusBlockValueBuilder: `${consensusBlockValueBuilder}`,
blockValueBuilder: `${blockValueBuilder}`,
slot,
});
}
Expand Down
17 changes: 11 additions & 6 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "node:path";
import {CompositeTypeAny, fromHexString, toHexString, TreeView, Type} from "@chainsafe/ssz";
import {CompositeTypeAny, fromHexString, TreeView, Type, toHexString} from "@chainsafe/ssz";
import {
BeaconStateAllForks,
CachedBeaconStateAllForks,
Expand Down Expand Up @@ -27,6 +27,7 @@ import {
Wei,
bellatrix,
isBlindedBeaconBlock,
Gwei,
} from "@lodestar/types";
import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {ProcessShutdownCallback} from "@lodestar/validator";
Expand Down Expand Up @@ -469,20 +470,22 @@ export class BeaconChain implements IBeaconChain {
return data && {block: data, executionOptimistic: false};
}

produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}> {
produceBlock(
blockAttributes: BlockAttributes
): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}> {
return this.produceBlockWrapper<BlockType.Full>(BlockType.Full, blockAttributes);
}

produceBlindedBlock(
blockAttributes: BlockAttributes
): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}> {
): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}> {
return this.produceBlockWrapper<BlockType.Blinded>(BlockType.Blinded, blockAttributes);
}

async produceBlockWrapper<T extends BlockType>(
blockType: T,
{randaoReveal, graffiti, slot, feeRecipient}: BlockAttributes
): Promise<{block: AssembledBlockType<T>; executionPayloadValue: Wei}> {
): Promise<{block: AssembledBlockType<T>; executionPayloadValue: Wei; consensusBlockValue: Gwei}> {
const head = this.forkChoice.getHead();
const state = await this.regen.getBlockSlotState(
head.blockRoot,
Expand Down Expand Up @@ -523,7 +526,9 @@ export class BeaconChain implements IBeaconChain {
stateRoot: ZERO_HASH,
body,
} as AssembledBlockType<T>;
block.stateRoot = computeNewStateRoot(this.metrics, state, block);

const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block);
block.stateRoot = newStateRoot;
const blockRoot =
blockType === BlockType.Full
? this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block)
Expand Down Expand Up @@ -575,7 +580,7 @@ export class BeaconChain implements IBeaconChain {
);
}

return {block, executionPayloadValue};
return {block, executionPayloadValue, consensusBlockValue: proposerReward};
}

/**
Expand Down
20 changes: 17 additions & 3 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import {CompositeTypeAny, TreeView, Type} from "@chainsafe/ssz";
import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, deneb, Wei} from "@lodestar/types";
import {
allForks,
UintNum64,
Root,
phase0,
Slot,
RootHex,
Epoch,
ValidatorIndex,
deneb,
Wei,
Gwei,
} from "@lodestar/types";
import {
BeaconStateAllForks,
CachedBeaconStateAllForks,
Expand Down Expand Up @@ -141,10 +153,12 @@ export interface IBeaconChain {

getBlobSidecars(beaconBlock: deneb.BeaconBlock): deneb.BlobSidecars;

produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei}>;
produceBlock(
blockAttributes: BlockAttributes
): Promise<{block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}>;
produceBlindedBlock(
blockAttributes: BlockAttributes
): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei}>;
): Promise<{block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Gwei}>;

/** Process a block until complete */
processBlock(block: BlockInput, opts?: ImportBlockOpts): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ExecutionPayloadStatus,
stateTransition,
} from "@lodestar/state-transition";
import {allForks, Root} from "@lodestar/types";
import {allForks, Gwei, Root} from "@lodestar/types";
import {ZERO_HASH} from "../../constants/index.js";
import {Metrics} from "../../metrics/index.js";

Expand All @@ -17,7 +17,7 @@ export function computeNewStateRoot(
metrics: Metrics | null,
state: CachedBeaconStateAllForks,
block: allForks.FullOrBlindedBeaconBlock
): Root {
): {newStateRoot: Root; proposerReward: Gwei} {
// Set signature to zero to re-use stateTransition() function which requires the SignedBeaconBlock type
const blockEmptySig = {message: block, signature: ZERO_HASH} as allForks.FullOrBlindedSignedBeaconBlock;

Expand All @@ -41,5 +41,8 @@ export function computeNewStateRoot(
metrics
);

return postState.hashTreeRoot();
const {attestations, syncAggregate, slashing} = postState.proposerRewards;
const proposerReward = BigInt(attestations + syncAggregate + slashing);

return {newStateRoot: postState.hashTreeRoot(), proposerReward};
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe("api/validator - produceBlockV2", function () {

const fullBlock = ssz.bellatrix.BeaconBlock.defaultValue();
const executionPayloadValue = ssz.Wei.defaultValue();
const consensusBlockValue = ssz.Gwei.defaultValue();

const currentSlot = 100000;
vi.spyOn(server.chainStub.clock, "currentSlot", "get").mockReturnValue(currentSlot);
Expand All @@ -84,7 +85,7 @@ describe("api/validator - produceBlockV2", function () {
const feeRecipient = "0xcccccccccccccccccccccccccccccccccccccccc";

const api = getValidatorApi(modules);
server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, executionPayloadValue});
server.chainStub.produceBlock.mockResolvedValue({block: fullBlock, executionPayloadValue, consensusBlockValue});

// check if expectedFeeRecipient is passed to produceBlock
await api.produceBlockV2(slot, randaoReveal, graffiti, {feeRecipient});
Expand Down
Loading

0 comments on commit 42b2efe

Please sign in to comment.