Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add endpoint for attestations reward #6484

Merged
merged 20 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"},
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
};

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
Loading