Skip to content

Commit

Permalink
feat: add endpoint for Altair block reward (#6178)
Browse files Browse the repository at this point in the history
* Add block rewards api

* Add test

* Add unit test

* Lint

* Address comment

* Reduce code redundancy

* Read reward cache first before calculate

* Lint

* Partially address comments

* Accept optional postState to get the reward cache

* Update test

* lint

* Update packages/beacon-node/src/chain/rewards/blockRewards.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* Update packages/beacon-node/src/chain/rewards/blockRewards.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* Update packages/beacon-node/src/chain/rewards/blockRewards.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* Update packages/beacon-node/src/chain/rewards/blockRewards.ts

Co-authored-by: Nico Flaig <nflaig@protonmail.com>

* Rename proposerRewards to blockRewards. Fix import

* Remove getBlockRewards from api ignore list

* Fix test

* Rename state to preState

* Add description to fields in BlockRewards

* Clean up imports

* Use jsdoc to document properties

* Apply suggestions from code review

* Add `getPreStateSync()`

* fix: clone states to compute block rewards

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
Co-authored-by: Cayman <caymannava@gmail.com>
Co-authored-by: Tuyen Nguyen <vutuyen2636@gmail.com>
  • Loading branch information
4 people authored Feb 22, 2024
1 parent cd59df3 commit f47cc18
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 29 deletions.
9 changes: 8 additions & 1 deletion packages/api/src/beacon/routes/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {RoutesData, ReturnTypes, reqEmpty, ContainerData} from "../../../utils/i
import * as block from "./block.js";
import * as pool from "./pool.js";
import * as state from "./state.js";
import * as rewards from "./rewards.js";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

Expand All @@ -15,9 +16,11 @@ import * as state from "./state.js";
export * as block from "./block.js";
export * as pool from "./pool.js";
export * as state from "./state.js";
export * as rewards from "./rewards.js";
export {BroadcastValidation} from "./block.js";
export type {BlockId, BlockHeaderResponse} from "./block.js";
export type {AttestationFilters} from "./pool.js";
export type {BlockRewards} from "./rewards.js";
// TODO: Review if re-exporting all these types is necessary
export type {
StateId,
Expand All @@ -34,7 +37,8 @@ export type {

export type Api = block.Api &
pool.Api &
state.Api & {
state.Api &
rewards.Api & {
getGenesis(): Promise<ApiClientResponse<{[HttpStatusCode.OK]: {data: phase0.Genesis}}>>;
};

Expand All @@ -43,6 +47,7 @@ export const routesData: RoutesData<Api> = {
...block.routesData,
...pool.routesData,
...state.routesData,
...rewards.routesData,
};

export type ReqTypes = {
Expand All @@ -56,6 +61,7 @@ export function getReqSerializers(config: ChainForkConfig) {
...block.getReqSerializers(config),
...pool.getReqSerializers(),
...state.getReqSerializers(),
...rewards.getReqSerializers(),
};
}

Expand All @@ -65,5 +71,6 @@ export function getReturnTypes(): ReturnTypes<Api> {
...block.getReturnTypes(),
...pool.getReturnTypes(),
...state.getReturnTypes(),
...rewards.getReturnTypes(),
};
}
97 changes: 97 additions & 0 deletions packages/api/src/beacon/routes/beacon/rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {ContainerType} from "@chainsafe/ssz";
import {ssz, ValidatorIndex} from "@lodestar/types";

import {
RoutesData,
ReturnTypes,
Schema,
ReqSerializers,
ContainerDataExecutionOptimistic,
} from "../../../utils/index.js";
import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js";
import {ApiClientResponse} from "../../../interfaces.js";
import {BlockId} from "./block.js";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

/**
* True if the response references an unverified execution payload. Optimistic information may be invalidated at
* a later time. If the field is not present, assume the False value.
*/
export type ExecutionOptimistic = boolean;

/**
* Rewards info for a single block. Every reward value is in Gwei.
*/
export type BlockRewards = {
/** Proposer of the block, the proposer index who receives these rewards */
proposerIndex: ValidatorIndex;
/** Total block reward, equal to attestations + sync_aggregate + proposer_slashings + attester_slashings */
total: number;
/** Block reward component due to included attestations */
attestations: number;
/** Block reward component due to included sync_aggregate */
syncAggregate: number;
/** Block reward component due to included proposer_slashings */
proposerSlashings: number;
/** Block reward component due to included attester_slashings */
attesterSlashings: number;
};

export type Api = {
/**
* Get block rewards
* Returns the info of rewards received by the block proposer
*
* @param blockId Block identifier.
* Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \<slot\>, \<hex encoded blockRoot with 0x prefix\>.
*/
getBlockRewards(
blockId: BlockId
): Promise<
ApiClientResponse<
{[HttpStatusCode.OK]: {data: BlockRewards; executionOptimistic: ExecutionOptimistic}},
HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND
>
>;
};

/**
* Define javascript values for each route
*/
export const routesData: RoutesData<Api> = {
getBlockRewards: {url: "/eth/v1/beacon/rewards/blocks/{block_id}", method: "GET"},
};

export type ReqTypes = {
/* eslint-disable @typescript-eslint/naming-convention */
getBlockRewards: {params: {block_id: string}};
};

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
return {
getBlockRewards: {
writeReq: (block_id) => ({params: {block_id: String(block_id)}}),
parseReq: ({params}) => [params.block_id],
schema: {params: {block_id: Schema.StringRequired}},
},
};
}

export function getReturnTypes(): ReturnTypes<Api> {
const BlockRewardsResponse = new ContainerType(
{
proposerIndex: ssz.ValidatorIndex,
total: ssz.UintNum64,
attestations: ssz.UintNum64,
syncAggregate: ssz.UintNum64,
proposerSlashings: ssz.UintNum64,
attesterSlashings: ssz.UintNum64,
},
{jsonCase: "eth2"}
);

return {
getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse),
};
}
2 changes: 1 addition & 1 deletion packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ const ignoredOperations = [
/* missing route */
/* https://github.com/ChainSafe/lodestar/issues/5694 */
"getSyncCommitteeRewards",
"getBlockRewards",
"getAttestationsRewards",
"getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697
"getBlindedBlock", // https://github.com/ChainSafe/lodestar/issues/5699
Expand Down Expand Up @@ -123,6 +122,7 @@ const ignoredProperties: Record<string, IgnoredProperty> = {
getBlockRoot: {response: ["finalized"]},
getBlockAttestations: {response: ["finalized"]},
getStateV2: {response: ["finalized"]},
getBlockRewards: {response: ["finalized"]},

/*
https://github.com/ChainSafe/lodestar/issues/6168
Expand Down
17 changes: 17 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,23 @@ export const testData: GenericServerTestCases<Api> = {
res: {executionOptimistic: true, data: {validators: [1300], validatorAggregates: [[1300]]}},
},

// reward

getBlockRewards: {
args: ["head"],
res: {
executionOptimistic: true,
data: {
proposerIndex: 0,
total: 15,
attestations: 8,
syncAggregate: 4,
proposerSlashings: 2,
attesterSlashings: 1,
},
},
},

// -

getGenesis: {
Expand Down
3 changes: 3 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import {ApiModules} from "../types.js";
import {getBeaconBlockApi} from "./blocks/index.js";
import {getBeaconPoolApi} from "./pool/index.js";
import {getBeaconStateApi} from "./state/index.js";
import {getBeaconRewardsApi} from "./rewards/index.js";

export function getBeaconApi(
modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db">
): ServerApi<routes.beacon.Api> {
const block = getBeaconBlockApi(modules);
const pool = getBeaconPoolApi(modules);
const state = getBeaconStateApi(modules);
const rewards = getBeaconRewardsApi(modules);

const {chain, config} = modules;

return {
...block,
...pool,
...state,
...rewards,

async getGenesis() {
return {
Expand Down
13 changes: 13 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/rewards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {routes, ServerApi} from "@lodestar/api";
import {ApiModules} from "../../types.js";
import {resolveBlockId} from "../blocks/utils.js";

export function getBeaconRewardsApi({chain}: Pick<ApiModules, "chain">): ServerApi<routes.beacon.rewards.Api> {
return {
async getBlockRewards(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
const data = await chain.getBlockRewards(block.message);
return {data, executionOptimistic};
},
};
}
12 changes: 12 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produ
import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js";
import {BlockInput} from "./blocks/types.js";
import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js";
import {BlockRewards, computeBlockRewards} from "./rewards/blockRewards.js";
import {ShufflingCache} from "./shufflingCache.js";
import {StateContextCache} from "./stateCache/stateContextCache.js";
import {SeenGossipBlockInput} from "./seenCache/index.js";
Expand Down Expand Up @@ -991,4 +992,15 @@ export class BeaconChain implements IBeaconChain {
}
}
}

async getBlockRewards(block: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards> {
const preState = this.regen.getPreStateSync(block);
const postState = this.regen.getStateSync(toHexString(block.stateRoot)) ?? undefined;

if (preState === null) {
throw Error(`Pre-state is unavailable given block's parent root ${toHexString(block.parentRoot)}`);
}

return computeBlockRewards(block, preState.clone(), postState?.clone());
}
}
3 changes: 3 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {AssembledBlockType, BlockAttributes, BlockType} from "./produceBlock/pro
import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js";
import {SeenGossipBlockInput} from "./seenCache/index.js";
import {ShufflingCache} from "./shufflingCache.js";
import {BlockRewards} from "./rewards/blockRewards.js";

export {BlockType, type AssembledBlockType};
export {type ProposerPreparationData};
Expand Down Expand Up @@ -198,6 +199,8 @@ export interface IBeaconChain {

regenCanAcceptWork(): boolean;
blsThreadPoolCanAcceptWork(): boolean;

getBlockRewards(blockRef: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards>;
}

export type SSZObjectType =
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/regen/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface IStateRegenerator extends IStateRegeneratorInternal {
dropCache(): void;
dumpCacheSummary(): routes.lodestar.StateCacheItem[];
getStateSync(stateRoot: RootHex): CachedBeaconStateAllForks | null;
getPreStateSync(block: allForks.BeaconBlock): CachedBeaconStateAllForks | null;
getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null;
getClosestHeadState(head: ProtoBlock): CachedBeaconStateAllForks | null;
pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void;
Expand Down
64 changes: 37 additions & 27 deletions packages/beacon-node/src/chain/regen/queued.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ export class QueuedStateRegenerator implements IStateRegenerator {
return this.stateCache.get(stateRoot);
}

getPreStateSync(block: allForks.BeaconBlock): CachedBeaconStateAllForks | null {
const parentRoot = toHexString(block.parentRoot);
const parentBlock = this.forkChoice.getBlockHex(parentRoot);
if (!parentBlock) {
throw new RegenError({
code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
blockRoot: block.parentRoot,
});
}

const parentEpoch = computeEpochAtSlot(parentBlock.slot);
const blockEpoch = computeEpochAtSlot(block.slot);

// Check the checkpoint cache (if the pre-state is a checkpoint state)
if (parentEpoch < blockEpoch) {
const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch);
if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) {
return checkpointState;
}
}

// Check the state cache, only if the state doesn't need to go through an epoch transition.
// Otherwise the state transition may not be cached and wasted. Queue for regen since the
// work required will still be significant.
if (parentEpoch === blockEpoch) {
const state = this.stateCache.get(parentBlock.stateRoot);
if (state) {
return state;
}
}

return null;
}

getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null {
return this.checkpointStateCache.get(cp);
}
Expand Down Expand Up @@ -137,34 +171,10 @@ export class QueuedStateRegenerator implements IStateRegenerator {
this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getPreState});

// First attempt to fetch the state from caches before queueing
const parentRoot = toHexString(block.parentRoot);
const parentBlock = this.forkChoice.getBlockHex(parentRoot);
if (!parentBlock) {
throw new RegenError({
code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
blockRoot: block.parentRoot,
});
}
const cachedState = this.getPreStateSync(block);

const parentEpoch = computeEpochAtSlot(parentBlock.slot);
const blockEpoch = computeEpochAtSlot(block.slot);

// Check the checkpoint cache (if the pre-state is a checkpoint state)
if (parentEpoch < blockEpoch) {
const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch);
if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) {
return checkpointState;
}
}

// Check the state cache, only if the state doesn't need to go through an epoch transition.
// Otherwise the state transition may not be cached and wasted. Queue for regen since the
// work required will still be significant.
if (parentEpoch === blockEpoch) {
const state = this.stateCache.get(parentBlock.stateRoot);
if (state) {
return state;
}
if (cachedState !== null) {
return cachedState;
}

// The state is not immediately available in the caches, enqueue the job
Expand Down
Loading

0 comments on commit f47cc18

Please sign in to comment.