Skip to content

Commit

Permalink
feat: add endpoint for attestations reward (#6484)
Browse files Browse the repository at this point in the history
* Define route and route test

* Update types

* Partial implementation

* Lint

* Complete total reward

* Add comment

* Lint

* Update packages/api/src/beacon/routes/beacon/rewards.ts

Co-authored-by: Nico Flaig <[email protected]>

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

Co-authored-by: Nico Flaig <[email protected]>

* Update packages/api/src/beacon/routes/beacon/rewards.ts

Co-authored-by: Nico Flaig <[email protected]>

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

Co-authored-by: Nico Flaig <[email protected]>

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

Co-authored-by: Nico Flaig <[email protected]>

* Update comment

* Update comment

* getAttestationsRewards returns object instead of tuple

* Lint

* Review PR

* Remove stale issue ref

* Add missing await

---------

Co-authored-by: Nico Flaig <[email protected]>
  • Loading branch information
ensi321 and nflaig authored Mar 18, 2024
1 parent f672c10 commit 0a7aa46
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 4 deletions.
8 changes: 7 additions & 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,13 @@ 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, SyncCommitteeRewards} from "./rewards.js";
export type {
BlockRewards,
AttestationsRewards,
IdealAttestationsReward,
TotalAttestationsReward,
SyncCommitteeRewards,
} from "./rewards.js";
// TODO: Review if re-exporting all these types is necessary
export type {
StateId,
Expand Down
93 changes: 92 additions & 1 deletion packages/api/src/beacon/routes/beacon/rewards.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ContainerType} from "@chainsafe/ssz";
import {ssz, ValidatorIndex} from "@lodestar/types";
import {Epoch, ssz, ValidatorIndex} from "@lodestar/types";

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

/**
* Rewards for a single set of (ideal or actual depending on usage) attestations. Reward value is in Gwei
*/
type AttestationsReward = {
/** Reward for head vote. Could be negative to indicate penalty */
head: number;
/** Reward for target vote. Could be negative to indicate penalty */
target: number;
/** Reward for source vote. Could be negative to indicate penalty */
source: number;
/** Inclusion delay reward (phase0 only) */
inclusionDelay: number;
/** Inactivity penalty. Should be a negative number to indicate penalty */
inactivity: number;
};

/**
* Rewards info for ideal attestations ie. Maximum rewards could be earned by making timely head, target and source vote.
* `effectiveBalance` is in Gwei
*/
export type IdealAttestationsReward = AttestationsReward & {effectiveBalance: number};

/**
* Rewards info for actual attestations
*/
export type TotalAttestationsReward = AttestationsReward & {validatorIndex: ValidatorIndex};

export type AttestationsRewards = {
idealRewards: IdealAttestationsReward[];
totalRewards: TotalAttestationsReward[];
};

/**
* 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
Expand All @@ -64,6 +96,22 @@ export type Api = {
HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND
>
>;
/**
* Get attestations rewards
* Negative values indicate penalties. `inactivity` can only be either 0 or negative number since it is penalty only
*
* @param epoch The epoch to get rewards info from
* @param validatorIds List of validator indices or pubkeys to filter in
*/
getAttestationsRewards(
epoch: Epoch,
validatorIds?: ValidatorId[]
): Promise<
ApiClientResponse<
{[HttpStatusCode.OK]: {data: AttestationsRewards; executionOptimistic: ExecutionOptimistic}},
HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND
>
>;

/**
* Get sync committee rewards
Expand All @@ -89,12 +137,14 @@ export type Api = {
*/
export const routesData: RoutesData<Api> = {
getBlockRewards: {url: "/eth/v1/beacon/rewards/blocks/{block_id}", method: "GET"},
getAttestationsRewards: {url: "/eth/v1/beacon/rewards/attestations/{epoch}", method: "POST"},
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}};
getAttestationsRewards: {params: {epoch: number}; body: ValidatorId[]};
getSyncCommitteeRewards: {params: {block_id: string}; body: ValidatorId[]};
};

Expand All @@ -105,6 +155,14 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
parseReq: ({params}) => [params.block_id],
schema: {params: {block_id: Schema.StringRequired}},
},
getAttestationsRewards: {
writeReq: (epoch, validatorIds) => ({params: {epoch: epoch}, body: validatorIds || []}),
parseReq: ({params, body}) => [params.epoch, body],
schema: {
params: {epoch: Schema.UintRequired},
body: Schema.UintOrStringArray,
},
},
getSyncCommitteeRewards: {
writeReq: (block_id, validatorIds) => ({params: {block_id: String(block_id)}, body: validatorIds || []}),
parseReq: ({params, body}) => [params.block_id, body],
Expand All @@ -129,6 +187,38 @@ export function getReturnTypes(): ReturnTypes<Api> {
{jsonCase: "eth2"}
);

const IdealAttestationsRewardsResponse = new ContainerType(
{
head: ssz.UintNum64,
target: ssz.UintNum64,
source: ssz.UintNum64,
inclusionDelay: ssz.UintNum64,
inactivity: ssz.UintNum64,
effectiveBalance: ssz.UintNum64,
},
{jsonCase: "eth2"}
);

const TotalAttestationsRewardsResponse = new ContainerType(
{
head: ssz.UintNum64,
target: ssz.UintNum64,
source: ssz.UintNum64,
inclusionDelay: ssz.UintNum64,
inactivity: ssz.UintNum64,
validatorIndex: ssz.ValidatorIndex,
},
{jsonCase: "eth2"}
);

const AttestationsRewardsResponse = new ContainerType(
{
idealRewards: ArrayOf(IdealAttestationsRewardsResponse),
totalRewards: ArrayOf(TotalAttestationsRewardsResponse),
},
{jsonCase: "eth2"}
);

const SyncCommitteeRewardsResponse = new ContainerType(
{
validatorIndex: ssz.ValidatorIndex,
Expand All @@ -139,6 +229,7 @@ export function getReturnTypes(): ReturnTypes<Api> {

return {
getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse),
getAttestationsRewards: ContainerDataExecutionOptimistic(AttestationsRewardsResponse),
getSyncCommitteeRewards: ContainerDataExecutionOptimistic(ArrayOf(SyncCommitteeRewardsResponse)),
};
}
3 changes: 1 addition & 2 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ const testDatas = {

const ignoredOperations = [
/* missing route */
/* https://github.com/ChainSafe/lodestar/issues/5694 */
"getAttestationsRewards",
/* https://github.com/ChainSafe/lodestar/issues/6058 */
"postStateValidators",
"postStateValidatorBalances",
Expand Down Expand Up @@ -125,6 +123,7 @@ const ignoredProperties: Record<string, IgnoredProperty> = {
getBlockAttestations: {response: ["finalized"]},
getStateV2: {response: ["finalized"]},
getBlockRewards: {response: ["finalized"]},
getAttestationsRewards: {response: ["finalized"]},
getSyncCommitteeRewards: {response: ["finalized"]},

/*
Expand Down
29 changes: 29 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,35 @@ export const testData: GenericServerTestCases<Api> = {
res: {executionOptimistic: true, data: [{validatorIndex: 1300, reward}]},
},

getAttestationsRewards: {
args: [10, ["1300"]],
res: {
executionOptimistic: true,
data: {
idealRewards: [
{
head: 0,
target: 10,
source: 20,
inclusionDelay: 30,
inactivity: 40,
effectiveBalance: 50,
},
],
totalRewards: [
{
head: 0,
target: 10,
source: 20,
inclusionDelay: 30,
inactivity: 40,
validatorIndex: 50,
},
],
},
},
},

// -

getGenesis: {
Expand Down
4 changes: 4 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,6 +9,10 @@ export function getBeaconRewardsApi({chain}: Pick<ApiModules, "chain">): ServerA
const data = await chain.getBlockRewards(block.message);
return {data, executionOptimistic};
},
async getAttestationsRewards(epoch, validatorIds) {
const {rewards, executionOptimistic} = await chain.getAttestationsRewards(epoch, validatorIds);
return {data: rewards, executionOptimistic};
},
async getSyncCommitteeRewards(blockId, validatorIds) {
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
const data = await chain.getSyncCommitteeRewards(block.message, validatorIds);
Expand Down
28 changes: 28 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Index2PubkeyCache,
PubkeyIndexMap,
EpochShuffling,
computeEndSlotAtEpoch,
} from "@lodestar/state-transition";
import {BeaconConfig} from "@lodestar/config";
import {
Expand Down Expand Up @@ -82,6 +83,7 @@ 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";
import {AttestationsRewards, computeAttestationsRewards} from "./rewards/attestationsRewards.js";

/**
* Arbitrary constants, blobs and payloads should be consumed immediately in the same slot
Expand Down Expand Up @@ -1006,6 +1008,32 @@ export class BeaconChain implements IBeaconChain {
return computeBlockRewards(block, preState.clone(), postState?.clone());
}

async getAttestationsRewards(
epoch: Epoch,
validatorIds?: (ValidatorIndex | string)[]
): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean}> {
// We use end slot of (epoch + 1) to ensure we have seen all attestations. On-time or late. Any late attestation beyond this slot is not considered
const slot = computeEndSlotAtEpoch(epoch + 1);
const stateResult = await this.getStateBySlot(slot, {allowRegen: false}); // No regen if state not in cache

if (stateResult === null) {
throw Error(`State is unavailable for slot ${slot}`);
}

const {executionOptimistic} = stateResult;
const stateRoot = toHexString(stateResult.state.hashTreeRoot());

const cachedState = this.regen.getStateSync(stateRoot);

if (cachedState === null) {
throw Error(`State is not in cache for slot ${slot}`);
}

const rewards = await computeAttestationsRewards(epoch, cachedState, this.config, validatorIds);

return {rewards, executionOptimistic};
}

async getSyncCommitteeRewards(
block: allForks.FullOrBlindedBeaconBlock,
validatorIds?: (ValidatorIndex | string)[]
Expand Down
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 {AttestationsRewards} from "./rewards/attestationsRewards.js";
import {SyncCommitteeRewards} from "./rewards/syncCommitteeRewards.js";

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

getBlockRewards(blockRef: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards>;
getAttestationsRewards(
epoch: Epoch,
validatorIds?: (ValidatorIndex | string)[]
): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean}>;
getSyncCommitteeRewards(
blockRef: allForks.FullOrBlindedBeaconBlock,
validatorIds?: (ValidatorIndex | string)[]
Expand Down
Loading

0 comments on commit 0a7aa46

Please sign in to comment.