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 sync committee reward #6260

Merged
merged 32 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fb2eb85
Add block rewards api
ensi321 Dec 5, 2023
7b460e2
Add test
ensi321 Dec 5, 2023
8ef70db
Merge branch 'unstable' into rewards-api
ensi321 Dec 11, 2023
cea591c
Add unit test
ensi321 Dec 11, 2023
ca45352
Lint
ensi321 Dec 11, 2023
68b99d1
Address comment
ensi321 Dec 12, 2023
05c5cd7
Reduce code redundancy
ensi321 Dec 12, 2023
08a2540
Merge branch 'unstable' into rewards-api
ensi321 Dec 18, 2023
61db8ab
Read reward cache first before calculate
ensi321 Dec 27, 2023
c2eae82
Lint
ensi321 Dec 27, 2023
03b249a
Add endpoint definition for sync rewards
ensi321 Dec 28, 2023
dd31509
Merge branch 'ChainSafe:unstable' into sync-reward
ensi321 Jan 8, 2024
a9934b6
Merge branch 'ChainSafe:unstable' into sync-reward
ensi321 Jan 22, 2024
28bf477
Merge branch 'unstable' into sync-reward
ensi321 Jan 25, 2024
503ca60
Add calculation logic
ensi321 Jan 25, 2024
4470cd5
Lint
ensi321 Jan 25, 2024
017565e
Merge branch 'unstable' into sync-reward
ensi321 Feb 23, 2024
5302c93
Follow convention from block rewards
ensi321 Feb 23, 2024
b7447aa
Include getSyncCommitteeRewards in unit test
ensi321 Feb 23, 2024
ac6c427
Update packages/beacon-node/src/chain/rewards/syncCommitteeRewards.ts
ensi321 Feb 26, 2024
2034dcc
Update packages/beacon-node/src/api/impl/beacon/rewards/index.ts
ensi321 Feb 26, 2024
d889c40
Improve filtering logic
ensi321 Feb 26, 2024
39e057a
Early throw on empty preState in getBlockRewards
ensi321 Feb 26, 2024
56a7941
Add jsdoc
ensi321 Feb 26, 2024
45a158b
Address comment
ensi321 Feb 26, 2024
41ca887
Clarify comment
ensi321 Feb 26, 2024
945d9b0
Address comment
ensi321 Feb 29, 2024
f96efc4
Update packages/beacon-node/src/chain/rewards/syncCommitteeRewards.ts
ensi321 Feb 29, 2024
7ab065b
Improve naming of filters
ensi321 Feb 29, 2024
2f10a9a
Lint
ensi321 Feb 29, 2024
387cee7
Update packages/beacon-node/src/chain/rewards/syncCommitteeRewards.ts
ensi321 Mar 1, 2024
0fde64e
ids -> validatorIds
ensi321 Mar 1, 2024
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
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(
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
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",
"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> = {
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) {
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
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) {
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
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(
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
(reward) => filtersSet.has(reward.validatorIndex) || filtersSet.has(index2pubkey[reward.validatorIndex].toHex())
);
} else {
return rewards;
}
}
Loading