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 17 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) */
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
inclusionDelay: number;
/* Inactivity penalty. Should be a negative number to indicate penalty */
inactivity: number;
nflaig marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Rewards info for ideal attestations ie. Maximum rewards could be earned by making timely head, target and source vote.
* `effectiveBalance` is in gwei
nflaig marked this conversation as resolved.
Show resolved Hide resolved
*/
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, filters) => ({params: {epoch: epoch}, body: filters || []}),
nflaig marked this conversation as resolved.
Show resolved Hide resolved
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)),
};
}
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 */
nflaig marked this conversation as resolved.
Show resolved Hide resolved
"getAttestationsRewards",
/* https://github.com/ChainSafe/lodestar/issues/6058 */
"postStateValidators",
"postStateValidatorBalances",
Expand Down Expand Up @@ -125,6 +124,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
3 changes: 3 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,9 @@ export function getBeaconRewardsApi({chain}: Pick<ApiModules, "chain">): ServerA
const data = await chain.getBlockRewards(block.message);
return {data, executionOptimistic};
},
async getAttestationsRewards(epoch, validatorIds) {
return chain.getAttestationsRewards(epoch, validatorIds);
nflaig marked this conversation as resolved.
Show resolved Hide resolved
},
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<{data: AttestationsRewards; executionOptimistic: boolean}> {
nflaig marked this conversation as resolved.
Show resolved Hide resolved
// 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 attestationsRewards = await computeAttestationsRewards(epoch, cachedState, this.config, validatorIds);

return {data: attestationsRewards, executionOptimistic};
nflaig marked this conversation as resolved.
Show resolved Hide resolved
}

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<{data: AttestationsRewards; executionOptimistic: boolean}>;
nflaig marked this conversation as resolved.
Show resolved Hide resolved
getSyncCommitteeRewards(
blockRef: allForks.FullOrBlindedBeaconBlock,
validatorIds?: (ValidatorIndex | string)[]
Expand Down
Loading
Loading