Skip to content

Commit

Permalink
feat: add endpoint for sync committee reward (#6260)
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

* Add endpoint definition for sync rewards

* Add calculation logic

* Lint

* Follow convention from block rewards

* Include getSyncCommitteeRewards in unit test

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

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

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

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

* Improve filtering logic

* Early throw on empty preState in getBlockRewards

* Add jsdoc

* Address comment

* Clarify comment

* Address comment

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

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

* Improve naming of filters

* Lint

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

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

* ids -> validatorIds

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
  • Loading branch information
ensi321 and nflaig committed Mar 4, 2024
1 parent d10ed38 commit 10c1b11
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 3 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/beacon/routes/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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";
export type {BlockRewards, SyncCommitteeRewards} from "./rewards.js";
// TODO: Review if re-exporting all these types is necessary
export type {
StateId,
Expand Down
47 changes: 47 additions & 0 deletions packages/api/src/beacon/routes/beacon/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
Schema,
ReqSerializers,
ContainerDataExecutionOptimistic,
ArrayOf,
} from "../../../utils/index.js";
import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js";
import {ApiClientResponse} from "../../../interfaces.js";
import {BlockId} from "./block.js";
import {ValidatorId} from "./state.js";

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

Expand Down Expand Up @@ -38,6 +40,14 @@ export type BlockRewards = {
attesterSlashings: number;
};

/**
* Rewards info for sync committee participation. Every reward value is in Gwei.
* Note: In the case that block proposer is present in `SyncCommitteeRewards`, the reward value only reflects rewards for
* participating in sync committee. Please refer to `BlockRewards.syncAggregate` for rewards of proposer including sync committee
* outputs into their block
*/
export type SyncCommitteeRewards = {validatorIndex: ValidatorIndex; reward: number}[];

export type Api = {
/**
* Get block rewards
Expand All @@ -54,18 +64,38 @@ export type Api = {
HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND
>
>;

/**
* Get sync committee rewards
* Returns participant reward value for each sync committee member at the given block.
*
* @param blockId Block identifier.
* Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \<slot\>, \<hex encoded blockRoot with 0x prefix\>.
* @param validatorIds List of validator indices or pubkeys to filter in
*/
getSyncCommitteeRewards(
blockId: BlockId,
validatorIds?: ValidatorId[]
): Promise<
ApiClientResponse<
{[HttpStatusCode.OK]: {data: SyncCommitteeRewards; 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"},
getSyncCommitteeRewards: {url: "/eth/v1/beacon/rewards/sync_committee/{block_id}", method: "POST"},
};

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

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
Expand All @@ -75,6 +105,14 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
parseReq: ({params}) => [params.block_id],
schema: {params: {block_id: Schema.StringRequired}},
},
getSyncCommitteeRewards: {
writeReq: (block_id, validatorIds) => ({params: {block_id: String(block_id)}, body: validatorIds || []}),
parseReq: ({params, body}) => [params.block_id, body],
schema: {
params: {block_id: Schema.StringRequired},
body: Schema.UintOrStringArray,
},
},
};
}

Expand All @@ -91,7 +129,16 @@ export function getReturnTypes(): ReturnTypes<Api> {
{jsonCase: "eth2"}
);

const SyncCommitteeRewardsResponse = new ContainerType(
{
validatorIndex: ssz.ValidatorIndex,
reward: ssz.UintNum64,
},
{jsonCase: "eth2"}
);

return {
getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse),
getSyncCommitteeRewards: ContainerDataExecutionOptimistic(ArrayOf(SyncCommitteeRewardsResponse)),
};
}
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 @@ -87,7 +87,6 @@ const testDatas = {
const ignoredOperations = [
/* missing route */
/* https://github.com/ChainSafe/lodestar/issues/5694 */
"getSyncCommitteeRewards",
"getAttestationsRewards",
/* https://github.com/ChainSafe/lodestar/issues/6058 */
"postStateValidators",
Expand Down Expand Up @@ -126,6 +125,7 @@ const ignoredProperties: Record<string, IgnoredProperty> = {
getBlockAttestations: {response: ["finalized"]},
getStateV2: {response: ["finalized"]},
getBlockRewards: {response: ["finalized"]},
getSyncCommitteeRewards: {response: ["finalized"]},

/*
https://github.com/ChainSafe/lodestar/issues/6168
Expand Down
5 changes: 5 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {GenericServerTestCases} from "../../../utils/genericServerTest.js";
const root = new Uint8Array(32).fill(1);
const randao = new Uint8Array(32).fill(1);
const balance = 32e9;
const reward = 32e9;
const pubkeyHex = toHexString(Buffer.alloc(48, 1));

const blockHeaderResponse: BlockHeaderResponse = {
Expand Down Expand Up @@ -184,6 +185,10 @@ export const testData: GenericServerTestCases<Api> = {
},
},
},
getSyncCommitteeRewards: {
args: ["head", ["1300"]],
res: {executionOptimistic: true, data: [{validatorIndex: 1300, reward}]},
},

// -

Expand Down
5 changes: 5 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/rewards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ export function getBeaconRewardsApi({chain}: Pick<ApiModules, "chain">): ServerA
const data = await chain.getBlockRewards(block.message);
return {data, executionOptimistic};
},
async getSyncCommitteeRewards(blockId, validatorIds) {
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
const data = await chain.getSyncCommitteeRewards(block.message, validatorIds);
return {data, executionOptimistic};
},
};
}
17 changes: 16 additions & 1 deletion packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {ShufflingCache} from "./shufflingCache.js";
import {StateContextCache} from "./stateCache/stateContextCache.js";
import {SeenGossipBlockInput} from "./seenCache/index.js";
import {CheckpointStateCache} from "./stateCache/stateContextCheckpointsCache.js";
import {SyncCommitteeRewards, computeSyncCommitteeRewards} from "./rewards/syncCommitteeRewards.js";

/**
* Arbitrary constants, blobs and payloads should be consumed immediately in the same slot
Expand Down Expand Up @@ -995,12 +996,26 @@ 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)}`);
}

const postState = this.regen.getStateSync(toHexString(block.stateRoot)) ?? undefined;

return computeBlockRewards(block, preState.clone(), postState?.clone());
}

async getSyncCommitteeRewards(
block: allForks.FullOrBlindedBeaconBlock,
validatorIds?: (ValidatorIndex | string)[]
): Promise<SyncCommitteeRewards> {
const preState = this.regen.getPreStateSync(block);

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

return computeSyncCommitteeRewards(block, preState.clone(), validatorIds);
}
}
5 changes: 5 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js";
import {SeenGossipBlockInput} from "./seenCache/index.js";
import {ShufflingCache} from "./shufflingCache.js";
import {BlockRewards} from "./rewards/blockRewards.js";
import {SyncCommitteeRewards} from "./rewards/syncCommitteeRewards.js";

export {BlockType, type AssembledBlockType};
export {type ProposerPreparationData};
Expand Down Expand Up @@ -201,6 +202,10 @@ export interface IBeaconChain {
blsThreadPoolCanAcceptWork(): boolean;

getBlockRewards(blockRef: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards>;
getSyncCommitteeRewards(
blockRef: allForks.FullOrBlindedBeaconBlock,
validatorIds?: (ValidatorIndex | string)[]
): Promise<SyncCommitteeRewards>;
}

export type SSZObjectType =
Expand Down
57 changes: 57 additions & 0 deletions packages/beacon-node/src/chain/rewards/syncCommitteeRewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "@lodestar/state-transition";
import {ValidatorIndex, allForks, altair} from "@lodestar/types";
import {ForkName, SYNC_COMMITTEE_SIZE} from "@lodestar/params";
import {routes} from "@lodestar/api";

export type SyncCommitteeRewards = routes.beacon.SyncCommitteeRewards;
type BalanceRecord = {val: number}; // Use val for convenient way to increment/decrement balance

export async function computeSyncCommitteeRewards(
block: allForks.BeaconBlock,
preState: CachedBeaconStateAllForks,
validatorIds?: (ValidatorIndex | string)[]
): Promise<SyncCommitteeRewards> {
const fork = preState.config.getForkName(block.slot);
if (fork === ForkName.phase0) {
throw Error("Cannot get sync rewards as phase0 block does not have sync committee");
}

const altairBlock = block as altair.BeaconBlock;
const preStateAltair = preState as CachedBeaconStateAltair;
const {index2pubkey} = preStateAltair.epochCtx;

// Bound committeeIndices in case it goes beyond SYNC_COMMITTEE_SIZE just to be safe
const committeeIndices = preStateAltair.epochCtx.currentSyncCommitteeIndexed.validatorIndices.slice(
0,
SYNC_COMMITTEE_SIZE
);
const {syncParticipantReward} = preStateAltair.epochCtx;
const {syncCommitteeBits} = altairBlock.body.syncAggregate;

// Use balance of each committee as starting point such that we cap the penalty to avoid balance dropping below 0
const balances: Map<ValidatorIndex, BalanceRecord> = new Map(
committeeIndices.map((i) => [i, {val: preStateAltair.balances.get(i)}])
);

for (const i of committeeIndices) {
const balanceRecord = balances.get(i) as BalanceRecord;
if (syncCommitteeBits.get(i)) {
// Positive rewards for participants
balanceRecord.val += syncParticipantReward;
} else {
// Negative rewards for non participants
balanceRecord.val = Math.max(0, balanceRecord.val - syncParticipantReward);
}
}

const rewards = Array.from(balances, ([validatorIndex, v]) => ({validatorIndex, reward: v.val}));

if (validatorIds !== undefined) {
const filtersSet = new Set(validatorIds);
return rewards.filter(
(reward) => filtersSet.has(reward.validatorIndex) || filtersSet.has(index2pubkey[reward.validatorIndex].toHex())
);
} else {
return rewards;
}
}

0 comments on commit 10c1b11

Please sign in to comment.