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 Altair block reward #6178

Merged
merged 28 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6679119
Add block rewards api
ensi321 Dec 5, 2023
f67421b
Add test
ensi321 Dec 5, 2023
0b030f2
Add unit test
ensi321 Dec 11, 2023
5e3f0f5
Lint
ensi321 Dec 11, 2023
c2da457
Address comment
ensi321 Dec 12, 2023
1e47858
Reduce code redundancy
ensi321 Dec 12, 2023
ac9bb42
Read reward cache first before calculate
ensi321 Dec 27, 2023
1fc3201
Lint
ensi321 Dec 27, 2023
f702c2b
Partially address comments
ensi321 Jan 11, 2024
6c14d0c
Accept optional postState to get the reward cache
ensi321 Jan 11, 2024
f331e5a
Update test
ensi321 Jan 11, 2024
db5cc41
lint
ensi321 Jan 11, 2024
edc8c3c
Update packages/beacon-node/src/chain/rewards/blockRewards.ts
ensi321 Jan 22, 2024
f543045
Update packages/beacon-node/src/chain/rewards/blockRewards.ts
ensi321 Jan 22, 2024
b605529
Update packages/beacon-node/src/chain/rewards/blockRewards.ts
ensi321 Jan 22, 2024
45734fa
Update packages/beacon-node/src/chain/rewards/blockRewards.ts
ensi321 Jan 22, 2024
f80daf4
Rename proposerRewards to blockRewards. Fix import
ensi321 Jan 22, 2024
3f8d42c
Remove getBlockRewards from api ignore list
ensi321 Jan 22, 2024
f2bbe95
Fix test
ensi321 Jan 23, 2024
cde10e9
Rename state to preState
ensi321 Jan 25, 2024
a6e2fc4
Add description to fields in BlockRewards
ensi321 Jan 25, 2024
ee42dde
Clean up imports
ensi321 Jan 25, 2024
b6f68d0
Use jsdoc to document properties
nflaig Jan 25, 2024
5c10acf
Apply suggestions from code review
wemeetagain Jan 25, 2024
47e9650
Merge branch 'ChainSafe:unstable' into rewards-api
ensi321 Jan 30, 2024
719ab1d
Add `getPreStateSync()`
ensi321 Jan 30, 2024
51bbf5e
fix: clone states to compute block rewards
twoeths Feb 20, 2024
eceea02
Merge pull request #2 from tuyennhv/tuyennhv/rewards-api
ensi321 Feb 20, 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
1 change: 1 addition & 0 deletions packages/api/src/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {Api} from "./routes/index.js";
export * as routes from "./routes/index.js";
export {getClient} from "./client/index.js";
export type {Api};
export type {BlockRewards} from "./routes/index.js";
ensi321 marked this conversation as resolved.
Show resolved Hide resolved

// Declare namespaces for CLI options
export type ApiNamespace = keyof Api;
Expand Down
9 changes: 8 additions & 1 deletion packages/api/src/beacon/routes/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {RoutesData, ReturnTypes, reqEmpty, ContainerData} from "../../../utils/i
import * as block from "./block.js";
import * as pool from "./pool.js";
import * as state from "./state.js";
import * as rewards from "./rewards.js";

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

Expand All @@ -15,9 +16,11 @@ import * as state from "./state.js";
export * as block from "./block.js";
export * as pool from "./pool.js";
export * as state from "./state.js";
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";
// TODO: Review if re-exporting all these types is necessary
export type {
StateId,
Expand All @@ -34,7 +37,8 @@ export type {

export type Api = block.Api &
pool.Api &
state.Api & {
state.Api &
rewards.Api & {
getGenesis(): Promise<ApiClientResponse<{[HttpStatusCode.OK]: {data: phase0.Genesis}}>>;
};

Expand All @@ -43,6 +47,7 @@ export const routesData: RoutesData<Api> = {
...block.routesData,
...pool.routesData,
...state.routesData,
...rewards.routesData,
};

export type ReqTypes = {
Expand All @@ -56,6 +61,7 @@ export function getReqSerializers(config: ChainForkConfig) {
...block.getReqSerializers(config),
...pool.getReqSerializers(),
...state.getReqSerializers(),
...rewards.getReqSerializers(),
};
}

Expand All @@ -65,5 +71,6 @@ export function getReturnTypes(): ReturnTypes<Api> {
...block.getReturnTypes(),
...pool.getReturnTypes(),
...state.getReturnTypes(),
...rewards.getReturnTypes(),
};
}
88 changes: 88 additions & 0 deletions packages/api/src/beacon/routes/beacon/rewards.ts
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {ContainerType} from "@chainsafe/ssz";
import {ssz, ValidatorIndex} from "@lodestar/types";

import {
RoutesData,
ReturnTypes,
Schema,
ReqSerializers,
ContainerDataExecutionOptimistic,
} from "../../../utils/index.js";
import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js";
import {ApiClientResponse} from "../../../interfaces.js";
import {BlockId} from "./block.js";

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

/**
* True if the response references an unverified execution payload. Optimistic information may be invalidated at
* a later time. If the field is not present, assume the False value.
*/
export type ExecutionOptimistic = boolean;

export type BlockRewards = {
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
proposerIndex: ValidatorIndex;
total: number;
attestations: number;
syncAggregate: number;
proposerSlashings: number;
attesterSlashings: number;
};

export type Api = {
/**
* Get block rewards
* Returns the info of rewards received by the block proposer
*
* @param blockId Block identifier.
* Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \<slot\>, \<hex encoded blockRoot with 0x prefix\>.
*/
getBlockRewards(
blockId: BlockId
): Promise<
ApiClientResponse<
{[HttpStatusCode.OK]: {data: BlockRewards; 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"},
};

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

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
return {
getBlockRewards: {
writeReq: (block_id) => ({params: {block_id: String(block_id)}}),
parseReq: ({params}) => [params.block_id],
schema: {params: {block_id: Schema.StringRequired}},
},
};
}

export function getReturnTypes(): ReturnTypes<Api> {
const BlockRewardsResponse = new ContainerType(
{
proposerIndex: ssz.ValidatorIndex,
total: ssz.UintNum64,
attestations: ssz.UintNum64,
nflaig marked this conversation as resolved.
Show resolved Hide resolved
syncAggregate: ssz.UintNum64,
proposerSlashings: ssz.UintNum64,
attesterSlashings: ssz.UintNum64,
},
{jsonCase: "eth2"}
);

return {
getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse),
};
}
2 changes: 2 additions & 0 deletions packages/api/src/beacon/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type Api = {
validator: ValidatorApi;
};

export type {BlockRewards} from "./beacon/index.js";

// Reasoning of the API definitions
// ================================
//
Expand Down
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 @@ -88,7 +88,6 @@ const ignoredOperations = [
/* missing route */
/* https://github.com/ChainSafe/lodestar/issues/5694 */
"getSyncCommitteeRewards",
"getBlockRewards",
"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> = {
getBlockRoot: {response: ["finalized"]},
getBlockAttestations: {response: ["finalized"]},
getStateV2: {response: ["finalized"]},
getBlockRewards: {response: ["finalized"]},

/*
https://github.com/ChainSafe/lodestar/issues/6168
Expand Down
17 changes: 17 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,23 @@ export const testData: GenericServerTestCases<Api> = {
res: {executionOptimistic: true, data: {validators: [1300], validatorAggregates: [[1300]]}},
},

// reward

getBlockRewards: {
args: ["head"],
res: {
executionOptimistic: true,
data: {
proposerIndex: 0,
total: 15,
attestations: 8,
syncAggregate: 4,
proposerSlashings: 2,
attesterSlashings: 1,
},
},
},

// -

getGenesis: {
Expand Down
3 changes: 3 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import {ApiModules} from "../types.js";
import {getBeaconBlockApi} from "./blocks/index.js";
import {getBeaconPoolApi} from "./pool/index.js";
import {getBeaconStateApi} from "./state/index.js";
import {getBeaconRewardsApi} from "./rewards/index.js";

export function getBeaconApi(
modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db">
): ServerApi<routes.beacon.Api> {
const block = getBeaconBlockApi(modules);
const pool = getBeaconPoolApi(modules);
const state = getBeaconStateApi(modules);
const rewards = getBeaconRewardsApi(modules);

const {chain, config} = modules;

return {
...block,
...pool,
...state,
...rewards,

async getGenesis() {
return {
Expand Down
13 changes: 13 additions & 0 deletions packages/beacon-node/src/api/impl/beacon/rewards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {routes, ServerApi} from "@lodestar/api";
import {ApiModules} from "../../types.js";
import {resolveBlockId} from "../blocks/utils.js";

export function getBeaconRewardsApi({chain}: Pick<ApiModules, "chain">): ServerApi<routes.beacon.rewards.Api> {
return {
async getBlockRewards(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
const data = await chain.getBlockRewards(block.message);
return {data, executionOptimistic};
},
};
}
9 changes: 9 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {ProcessShutdownCallback} from "@lodestar/validator";
import {Logger, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toHex} from "@lodestar/utils";
import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";

import {type BlockRewards} from "@lodestar/api";
import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js";
import {IBeaconDb} from "../db/index.js";
import {Metrics} from "../metrics/index.js";
Expand Down Expand Up @@ -76,6 +77,7 @@ import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produ
import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js";
import {BlockInput} from "./blocks/types.js";
import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js";
import {computeBlockRewards} from "./rewards/blockRewards.js";
import {ShufflingCache} from "./shufflingCache.js";
import {StateContextCache} from "./stateCache/stateContextCache.js";
import {SeenGossipBlockInput} from "./seenCache/index.js";
Expand Down Expand Up @@ -991,4 +993,11 @@ export class BeaconChain implements IBeaconChain {
}
}
}

async getBlockRewards(block: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards> {
const preState = (await this.regen.getPreState(block, {dontTransferCache: true}, RegenCaller.restApi)).clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little sketchy that this can trigger a regen.
Is this the only non-debug endpoint that can do that?

Copy link
Contributor Author

@ensi321 ensi321 Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little sketchy that this can trigger a regen.

What's your concern in particular? I don't think getPreState modifies anything under the hood. To get the preState of a block I don't see a way without calling regen. At some point regen.getState() must be called.

Is this the only non-debug endpoint that can do that?

This is the only one that I know of cc. @tuyennhv

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only non-debug endpoint that can do that?

It looks like this is the only one, can be confirmed by searching for allowRegen which is only set to true for debug state apis

What's your concern in particular?

The main concern is probably that you can use this API to easily DoS our public nodes as regen is quite expensive.

What about adding rewards APIs as their own namespace and disabling them by default?

e.g. we also have light client APIs on their own namespace even though those are part of /beacon as per spec

lightclient: () => lightclient.getRoutes(config, api.lightclient),

we could then simply not enable rewards APIs by default here

api: ["beacon", "config", "events", "node", "validator", "lightclient"],

Copy link
Contributor Author

@ensi321 ensi321 Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding rewards APIs as their own namespace and disabling them by default?

I read the documentation of other CL clients and looks like everyone has rewards endpoint enabled by default. Don’t know if the users expect this endpoint would be enabled on the public nodes.

Alternatively, we can limit the queried block to any block from last finalized checkpoint to head similar to what Teku behaves under prune mode: https://docs.teku.consensys.io/how-to/use-rewards-api#limitations . This way we only need to check stateCache and checkpointStateCache without triggering a regen request

The main concern is probably that you can use this API to easily DoS our public nodes as regen is quite expensive.

My wishful thinking is to have two priority tiers for RegenRequest. One being essential and one being non-essential. Any debug and reward endpoint that triggers regen should have a low priority such that when JobItemQueue size reaches a certain threshold, it rejects new RegenRequest that has low priority.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we can limit the queried block from last finalized checkpoint to head

This is already the case now, it isn't fetching historical states, but this can still trigger block / epoch transitions if the requested state isn't already in a cache. In practice, in healthy network conditions, we have all states between finalized and head in cache. But it can become a problem in periods of non-finality.

We could add and use a regen.getPreStateSync that acts like regen.getStateSync in that it only checks cached state values and returns undefined if the state isn't cached. IMO this is a good compromise, since it allows us to serve the data in most cases, but doesn't open us up to any DoS issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside, It may be worth thinking deeper about our strategy around serving more expensive queries.

Should we support expensive queries? Behind a feature flag or flags? If so, what does that look like?
Behind extra namespaces for each additional feature-set in the APIs?
What does the architecture of generating expensive data look like? Queue that deprioritizes non-urgent work? Separate worker that handles expensive queries? Other?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this is really a concern because with PersistentCheckpointStateCache, using getPreState means it will reload checkpoint state if needed

We could add and use a regen.getPreStateSync that acts like regen.getStateSync in that it only checks cached state values and returns undefined if the state isn't cached. IMO this is a good compromise, since it allows us to serve the data in most cases, but doesn't open us up to any DoS issues.

in this specific scenario, if we can get a cached pre state, we can also get post state and get the cached reward from post state, so it's no use to have getPreStateSync()

I think we only want to support getting cached reward in post state for lodestar (it means it'll work for 64-96 blocks for now). If demand arises from any specific use case, we can enhance later

Copy link
Contributor Author

@ensi321 ensi321 Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this specific scenario, if we can get a cached pre state, we can also get post state and get the cached reward from post state, so it's no use to have getPreStateSync()

For the purpose of this endpoint, I echo with @wemeetagain 's idea to only serve blocks which its corresponding postState is cached.
So we should just drop the preState parameter in computeBlockRewards(block, preState, postState), and also all the reward calculation in blockRewards.ts and solely rely on cached values
Update: Looks like we still need the preState to calculate proposer and attester slashing rewards instead of simply checking the cache because RewardCache combines both into a single slashing value. Will still need getPreStateSync() for this PR

so it's no use to have getPreStateSync()

The other two rewards endpoint (attestation and sync committee) both require preState so we will need to have getPreStateSync() implemented at some point. We can avoid getPreStateSync() for this endpoint because we have block rewards cached in BeaconStateCache.proposerRewards: RewardCache but that's not the case for the other endpoints.

const postState = this.regen.getStateSync(toHexString(block.stateRoot)) ?? undefined;
const result = computeBlockRewards(block, preState, postState);
return result;
}
}
3 changes: 3 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {BeaconConfig} from "@lodestar/config";
import {Logger} from "@lodestar/utils";

import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {type BlockRewards} from "@lodestar/api";
import {IEth1ForBlockProduction} from "../eth1/index.js";
import {IExecutionEngine, IExecutionBuilder} from "../execution/index.js";
import {Metrics} from "../metrics/metrics.js";
Expand Down Expand Up @@ -198,6 +199,8 @@ export interface IBeaconChain {

regenCanAcceptWork(): boolean;
blsThreadPoolCanAcceptWork(): boolean;

getBlockRewards(blockRef: allForks.FullOrBlindedBeaconBlock): Promise<BlockRewards>;
}

export type SSZObjectType =
Expand Down
Loading
Loading