diff --git a/packages/api/src/beacon/routes/beacon/index.ts b/packages/api/src/beacon/routes/beacon/index.ts index 4d0c8186fd22..92fcc2093188 100644 --- a/packages/api/src/beacon/routes/beacon/index.ts +++ b/packages/api/src/beacon/routes/beacon/index.ts @@ -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 @@ -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, @@ -34,7 +37,8 @@ export type { export type Api = block.Api & pool.Api & - state.Api & { + state.Api & + rewards.Api & { getGenesis(): Promise>; }; @@ -43,6 +47,7 @@ export const routesData: RoutesData = { ...block.routesData, ...pool.routesData, ...state.routesData, + ...rewards.routesData, }; export type ReqTypes = { @@ -56,6 +61,7 @@ export function getReqSerializers(config: ChainForkConfig) { ...block.getReqSerializers(config), ...pool.getReqSerializers(), ...state.getReqSerializers(), + ...rewards.getReqSerializers(), }; } @@ -65,5 +71,6 @@ export function getReturnTypes(): ReturnTypes { ...block.getReturnTypes(), ...pool.getReturnTypes(), ...state.getReturnTypes(), + ...rewards.getReturnTypes(), }; } diff --git a/packages/api/src/beacon/routes/beacon/rewards.ts b/packages/api/src/beacon/routes/beacon/rewards.ts new file mode 100644 index 000000000000..42dced7d5c3f --- /dev/null +++ b/packages/api/src/beacon/routes/beacon/rewards.ts @@ -0,0 +1,97 @@ +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; + +/** + * Rewards info for a single block. Every reward value is in Gwei. + */ +export type BlockRewards = { + /** Proposer of the block, the proposer index who receives these rewards */ + proposerIndex: ValidatorIndex; + /** Total block reward, equal to attestations + sync_aggregate + proposer_slashings + attester_slashings */ + total: number; + /** Block reward component due to included attestations */ + attestations: number; + /** Block reward component due to included sync_aggregate */ + syncAggregate: number; + /** Block reward component due to included proposer_slashings */ + proposerSlashings: number; + /** Block reward component due to included attester_slashings */ + 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", \, \. + */ + 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 = { + 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 { + 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 { + const BlockRewardsResponse = new ContainerType( + { + proposerIndex: ssz.ValidatorIndex, + total: ssz.UintNum64, + attestations: ssz.UintNum64, + syncAggregate: ssz.UintNum64, + proposerSlashings: ssz.UintNum64, + attesterSlashings: ssz.UintNum64, + }, + {jsonCase: "eth2"} + ); + + return { + getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse), + }; +} diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index f7d6cb9a077c..4d036fb2dd8d 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -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 @@ -123,6 +122,7 @@ const ignoredProperties: Record = { getBlockRoot: {response: ["finalized"]}, getBlockAttestations: {response: ["finalized"]}, getStateV2: {response: ["finalized"]}, + getBlockRewards: {response: ["finalized"]}, /* https://github.com/ChainSafe/lodestar/issues/6168 diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 6d6bc6576f56..9c39849906de 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -168,6 +168,23 @@ export const testData: GenericServerTestCases = { 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: { diff --git a/packages/beacon-node/src/api/impl/beacon/index.ts b/packages/beacon-node/src/api/impl/beacon/index.ts index d613e3c2d394..492e2f8ff8b1 100644 --- a/packages/beacon-node/src/api/impl/beacon/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/index.ts @@ -3,6 +3,7 @@ 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 @@ -10,6 +11,7 @@ export function getBeaconApi( const block = getBeaconBlockApi(modules); const pool = getBeaconPoolApi(modules); const state = getBeaconStateApi(modules); + const rewards = getBeaconRewardsApi(modules); const {chain, config} = modules; @@ -17,6 +19,7 @@ export function getBeaconApi( ...block, ...pool, ...state, + ...rewards, async getGenesis() { return { diff --git a/packages/beacon-node/src/api/impl/beacon/rewards/index.ts b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts new file mode 100644 index 000000000000..03a182359d90 --- /dev/null +++ b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts @@ -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): ServerApi { + return { + async getBlockRewards(blockId) { + const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const data = await chain.getBlockRewards(block.message); + return {data, executionOptimistic}; + }, + }; +} diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index f20bc0dbffa2..20a6ca343565 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -76,6 +76,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 {BlockRewards, computeBlockRewards} from "./rewards/blockRewards.js"; import {ShufflingCache} from "./shufflingCache.js"; import {StateContextCache} from "./stateCache/stateContextCache.js"; import {SeenGossipBlockInput} from "./seenCache/index.js"; @@ -991,4 +992,15 @@ export class BeaconChain implements IBeaconChain { } } } + + async getBlockRewards(block: allForks.FullOrBlindedBeaconBlock): Promise { + 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)}`); + } + + return computeBlockRewards(block, preState.clone(), postState?.clone()); + } } diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 3939457a8ac3..99c1b7ea0c4a 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -52,6 +52,7 @@ import {AssembledBlockType, BlockAttributes, BlockType} from "./produceBlock/pro import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; import {SeenGossipBlockInput} from "./seenCache/index.js"; import {ShufflingCache} from "./shufflingCache.js"; +import {BlockRewards} from "./rewards/blockRewards.js"; export {BlockType, type AssembledBlockType}; export {type ProposerPreparationData}; @@ -198,6 +199,8 @@ export interface IBeaconChain { regenCanAcceptWork(): boolean; blsThreadPoolCanAcceptWork(): boolean; + + getBlockRewards(blockRef: allForks.FullOrBlindedBeaconBlock): Promise; } export type SSZObjectType = diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index be481de9abc8..b861378ff440 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -35,6 +35,7 @@ export interface IStateRegenerator extends IStateRegeneratorInternal { dropCache(): void; dumpCacheSummary(): routes.lodestar.StateCacheItem[]; getStateSync(stateRoot: RootHex): CachedBeaconStateAllForks | null; + getPreStateSync(block: allForks.BeaconBlock): CachedBeaconStateAllForks | null; getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null; getClosestHeadState(head: ProtoBlock): CachedBeaconStateAllForks | null; pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void; diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index 928c2e399b9a..a65c462227f3 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -71,6 +71,40 @@ export class QueuedStateRegenerator implements IStateRegenerator { return this.stateCache.get(stateRoot); } + getPreStateSync(block: allForks.BeaconBlock): CachedBeaconStateAllForks | null { + const parentRoot = toHexString(block.parentRoot); + const parentBlock = this.forkChoice.getBlockHex(parentRoot); + if (!parentBlock) { + throw new RegenError({ + code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE, + blockRoot: block.parentRoot, + }); + } + + const parentEpoch = computeEpochAtSlot(parentBlock.slot); + const blockEpoch = computeEpochAtSlot(block.slot); + + // Check the checkpoint cache (if the pre-state is a checkpoint state) + if (parentEpoch < blockEpoch) { + const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch); + if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) { + return checkpointState; + } + } + + // Check the state cache, only if the state doesn't need to go through an epoch transition. + // Otherwise the state transition may not be cached and wasted. Queue for regen since the + // work required will still be significant. + if (parentEpoch === blockEpoch) { + const state = this.stateCache.get(parentBlock.stateRoot); + if (state) { + return state; + } + } + + return null; + } + getCheckpointStateSync(cp: CheckpointHex): CachedBeaconStateAllForks | null { return this.checkpointStateCache.get(cp); } @@ -137,34 +171,10 @@ export class QueuedStateRegenerator implements IStateRegenerator { this.metrics?.regenFnCallTotal.inc({caller: rCaller, entrypoint: RegenFnName.getPreState}); // First attempt to fetch the state from caches before queueing - const parentRoot = toHexString(block.parentRoot); - const parentBlock = this.forkChoice.getBlockHex(parentRoot); - if (!parentBlock) { - throw new RegenError({ - code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE, - blockRoot: block.parentRoot, - }); - } + const cachedState = this.getPreStateSync(block); - const parentEpoch = computeEpochAtSlot(parentBlock.slot); - const blockEpoch = computeEpochAtSlot(block.slot); - - // Check the checkpoint cache (if the pre-state is a checkpoint state) - if (parentEpoch < blockEpoch) { - const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch); - if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) { - return checkpointState; - } - } - - // Check the state cache, only if the state doesn't need to go through an epoch transition. - // Otherwise the state transition may not be cached and wasted. Queue for regen since the - // work required will still be significant. - if (parentEpoch === blockEpoch) { - const state = this.stateCache.get(parentBlock.stateRoot); - if (state) { - return state; - } + if (cachedState !== null) { + return cachedState; } // The state is not immediately available in the caches, enqueue the job diff --git a/packages/beacon-node/src/chain/rewards/blockRewards.ts b/packages/beacon-node/src/chain/rewards/blockRewards.ts new file mode 100644 index 000000000000..bd8bf3537582 --- /dev/null +++ b/packages/beacon-node/src/chain/rewards/blockRewards.ts @@ -0,0 +1,137 @@ +import { + CachedBeaconStateAllForks, + CachedBeaconStateAltair, + CachedBeaconStatePhase0, + getAttesterSlashableIndices, + processAttestationsAltair, +} from "@lodestar/state-transition"; +import {allForks, altair, phase0} from "@lodestar/types"; +import {ForkName, WHISTLEBLOWER_REWARD_QUOTIENT} from "@lodestar/params"; +import {routes} from "@lodestar/api"; + +export type BlockRewards = routes.beacon.BlockRewards; +type SubRewardValue = number; // All reward values should be integer + +/** + * Calculate total proposer block rewards given block and the beacon state of the same slot before the block is applied (preState) + * postState can be passed in to read reward cache if available + * Standard (Non MEV) rewards for proposing a block consists of: + * 1) Including attestations from (beacon) committee + * 2) Including attestations from sync committee + * 3) Reporting slashable behaviours from proposer and attester + */ +export async function computeBlockRewards( + block: allForks.BeaconBlock, + preState: CachedBeaconStateAllForks, + postState?: CachedBeaconStateAllForks +): Promise { + const fork = preState.config.getForkName(block.slot); + const {attestations: cachedAttestationsReward = 0, syncAggregate: cachedSyncAggregateReward = 0} = + postState?.proposerRewards ?? {}; + let blockAttestationReward = cachedAttestationsReward; + let syncAggregateReward = cachedSyncAggregateReward; + + if (blockAttestationReward === 0) { + blockAttestationReward = + fork === ForkName.phase0 + ? computeBlockAttestationRewardPhase0(block as phase0.BeaconBlock, preState as CachedBeaconStatePhase0) + : computeBlockAttestationRewardAltair(block as altair.BeaconBlock, preState as CachedBeaconStateAltair); + } + + if (syncAggregateReward === 0) { + syncAggregateReward = computeSyncAggregateReward(block as altair.BeaconBlock, preState as CachedBeaconStateAltair); + } + + const blockProposerSlashingReward = computeBlockProposerSlashingReward(block, preState); + const blockAttesterSlashingReward = computeBlockAttesterSlashingReward(block, preState); + + const total = + blockAttestationReward + syncAggregateReward + blockProposerSlashingReward + blockAttesterSlashingReward; + + return { + proposerIndex: block.proposerIndex, + total, + attestations: blockAttestationReward, + syncAggregate: syncAggregateReward, + proposerSlashings: blockProposerSlashingReward, + attesterSlashings: blockAttesterSlashingReward, + }; +} + +/** + * TODO: Calculate rewards received by block proposer for including attestations. + */ +function computeBlockAttestationRewardPhase0( + _block: phase0.BeaconBlock, + _preState: CachedBeaconStatePhase0 +): SubRewardValue { + throw new Error("Unsupported fork! Block attestation reward calculation is not available in phase0"); +} + +/** + * Calculate rewards received by block proposer for including attestations since Altair. + * Reuses `processAttestationsAltair()`. Has dependency on RewardCache + */ +function computeBlockAttestationRewardAltair( + block: altair.BeaconBlock, + preState: CachedBeaconStateAltair +): SubRewardValue { + const fork = preState.config.getForkSeq(block.slot); + const {attestations} = block.body; + + processAttestationsAltair(fork, preState, attestations, false); + + return preState.proposerRewards.attestations; +} + +function computeSyncAggregateReward(block: altair.BeaconBlock, preState: CachedBeaconStateAltair): SubRewardValue { + if (block.body.syncAggregate !== undefined) { + const {syncCommitteeBits} = block.body.syncAggregate; + const {syncProposerReward} = preState.epochCtx; + + return syncCommitteeBits.getTrueBitIndexes().length * Math.floor(syncProposerReward); // syncProposerReward should already be integer + } else { + return 0; // phase0 block does not have syncAggregate + } +} + +/** + * Calculate rewards received by block proposer for including proposer slashings. + * All proposer slashing rewards go to block proposer and none to whistleblower as of Deneb + */ +function computeBlockProposerSlashingReward( + block: allForks.BeaconBlock, + state: CachedBeaconStateAllForks +): SubRewardValue { + let proposerSlashingReward = 0; + + for (const proposerSlashing of block.body.proposerSlashings) { + const offendingProposerIndex = proposerSlashing.signedHeader1.message.proposerIndex; + const offendingProposerBalance = state.validators.getReadonly(offendingProposerIndex).effectiveBalance; + + proposerSlashingReward += Math.floor(offendingProposerBalance / WHISTLEBLOWER_REWARD_QUOTIENT); + } + + return proposerSlashingReward; +} + +/** + * Calculate rewards received by block proposer for including attester slashings. + * All attester slashing rewards go to block proposer and none to whistleblower as of Deneb + */ +function computeBlockAttesterSlashingReward( + block: allForks.BeaconBlock, + preState: CachedBeaconStateAllForks +): SubRewardValue { + let attesterSlashingReward = 0; + + for (const attesterSlashing of block.body.attesterSlashings) { + for (const offendingAttesterIndex of getAttesterSlashableIndices(attesterSlashing)) { + const offendingAttesterBalance = preState.validators.getReadonly(offendingAttesterIndex).effectiveBalance; + + attesterSlashingReward += Math.floor(offendingAttesterBalance / WHISTLEBLOWER_REWARD_QUOTIENT); + } + } + + return attesterSlashingReward; +} diff --git a/packages/beacon-node/test/unit/chain/rewards/blockRewards.test.ts b/packages/beacon-node/test/unit/chain/rewards/blockRewards.test.ts new file mode 100644 index 000000000000..f0d85ce3220f --- /dev/null +++ b/packages/beacon-node/test/unit/chain/rewards/blockRewards.test.ts @@ -0,0 +1,181 @@ +import {describe, it, expect} from "vitest"; +import {SYNC_COMMITTEE_SIZE} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import { + CachedBeaconStateAllForks, + DataAvailableStatus, + ExecutionPayloadStatus, + stateTransition, +} from "@lodestar/state-transition"; +import { + generatePerfTestCachedStateAltair, + cachedStateAltairPopulateCaches, + // eslint-disable-next-line import/no-relative-packages +} from "../../../../../state-transition/test/perf/util.js"; +// eslint-disable-next-line import/no-relative-packages +import {BlockAltairOpts, getBlockAltair} from "../../../../../state-transition/test/perf/block/util.js"; +import {computeBlockRewards} from "../../../../src/chain/rewards/blockRewards.js"; + +describe("chain / rewards / blockRewards", () => { + const testCases: {id: string; opts: BlockAltairOpts}[] = [ + { + id: "Normal case", + opts: { + proposerSlashingLen: 1, + attesterSlashingLen: 2, + attestationLen: 90, + depositsLen: 0, + voluntaryExitLen: 0, + bitsLen: 90, + syncCommitteeBitsLen: Math.round(SYNC_COMMITTEE_SIZE * 0.7), + }, + }, + { + id: "Attestation only", + opts: { + proposerSlashingLen: 0, + attesterSlashingLen: 0, + attestationLen: 90, + depositsLen: 0, + voluntaryExitLen: 0, + bitsLen: 90, + syncCommitteeBitsLen: 0, + }, + }, + { + id: "Sync aggregate only", + opts: { + proposerSlashingLen: 0, + attesterSlashingLen: 0, + attestationLen: 0, + depositsLen: 0, + voluntaryExitLen: 0, + bitsLen: 90, + syncCommitteeBitsLen: Math.round(SYNC_COMMITTEE_SIZE * 0.7), + }, + }, + { + id: "Proposer slashing only", + opts: { + proposerSlashingLen: 2, + attesterSlashingLen: 0, + attestationLen: 0, + depositsLen: 0, + voluntaryExitLen: 0, + bitsLen: 90, + syncCommitteeBitsLen: 0, + }, + }, + { + id: "Attester slashing only", + opts: { + proposerSlashingLen: 0, + attesterSlashingLen: 5, + attestationLen: 0, + depositsLen: 0, + voluntaryExitLen: 0, + bitsLen: 90, + syncCommitteeBitsLen: 0, + }, + }, + ]; + + for (const {id, opts} of testCases) { + it(`${id}`, async () => { + const state = generatePerfTestCachedStateAltair(); + const block = getBlockAltair(state, opts); + // Populate permanent root caches of the block + ssz.altair.BeaconBlock.hashTreeRoot(block.message); + // Populate tree root caches of the state + state.hashTreeRoot(); + cachedStateAltairPopulateCaches(state); + const calculatedBlockReward = await computeBlockRewards(block.message, state as CachedBeaconStateAllForks); + const {proposerIndex, total, attestations, syncAggregate, proposerSlashings, attesterSlashings} = + calculatedBlockReward; + + // Sanity check + expect(proposerIndex).toBe(block.message.proposerIndex); + expect(total).toBe(attestations + syncAggregate + proposerSlashings + attesterSlashings); + if (opts.syncCommitteeBitsLen === 0) { + expect(syncAggregate).toBe(0); + } + if (opts.attestationLen === 0) { + expect(attestations).toBe(0); + } + if (opts.proposerSlashingLen === 0) { + expect(proposerSlashings).toBe(0); + } + if (opts.attesterSlashingLen === 0) { + expect(attesterSlashings).toBe(0); + } + + const postState = stateTransition(state as CachedBeaconStateAllForks, block, { + executionPayloadStatus: ExecutionPayloadStatus.valid, + dataAvailableStatus: DataAvailableStatus.available, + verifyProposer: false, + verifySignatures: false, + verifyStateRoot: false, + }); + + // Cross check with rewardCache + const rewardCache = postState.proposerRewards; + expect(total).toBe(rewardCache.attestations + rewardCache.syncAggregate + rewardCache.slashing); + expect(attestations).toBe(rewardCache.attestations); + expect(syncAggregate).toBe(rewardCache.syncAggregate); + expect(proposerSlashings + attesterSlashings).toBe(rewardCache.slashing); + }); + } + + // Check if `computeBlockRewards` consults reward cache in the post state first + it("Check reward cache", async () => { + const preState = generatePerfTestCachedStateAltair(); + const {opts} = testCases[0]; // Use opts of `normal case` + const block = getBlockAltair(preState, testCases[0].opts); + // Populate permanent root caches of the block + ssz.altair.BeaconBlock.hashTreeRoot(block.message); + // Populate tree root caches of the state + preState.hashTreeRoot(); + cachedStateAltairPopulateCaches(preState); + + const postState = stateTransition(preState as CachedBeaconStateAllForks, block, { + executionPayloadStatus: ExecutionPayloadStatus.valid, + dataAvailableStatus: DataAvailableStatus.available, + verifyProposer: false, + verifySignatures: false, + verifyStateRoot: false, + }); + + // Set postState's reward cache + const rewardCache = postState.proposerRewards; // Grab original reward cache before overwritten + postState.proposerRewards = {attestations: 1000, syncAggregate: 1001, slashing: 1002}; + + const calculatedBlockReward = await computeBlockRewards( + block.message, + preState as CachedBeaconStateAllForks, + postState + ); + const {proposerIndex, total, attestations, syncAggregate, proposerSlashings, attesterSlashings} = + calculatedBlockReward; + + expect(proposerIndex).toBe(block.message.proposerIndex); + expect(total).toBe(attestations + syncAggregate + proposerSlashings + attesterSlashings); + if (opts.syncCommitteeBitsLen === 0) { + expect(syncAggregate).toBe(0); + } + if (opts.attestationLen === 0) { + expect(attestations).toBe(0); + } + if (opts.proposerSlashingLen === 0) { + expect(proposerSlashings).toBe(0); + } + if (opts.attesterSlashingLen === 0) { + expect(attesterSlashings).toBe(0); + } + + // Cross check with rewardCache + expect(attestations).toBe(1000); + expect(syncAggregate).toBe(1001); + expect(proposerSlashings + attesterSlashings).not.toBe(1002); + expect(proposerSlashings + attesterSlashings).toBe(rewardCache.slashing); + }); +}); diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 8786c0f6e358..0ef460e784af 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -61,3 +61,5 @@ export {ExecutionPayloadStatus, DataAvailableStatus, type BlockExternalData} fro export {becomesNewEth1Data} from "./block/processEth1Data.js"; // Withdrawals for new blocks export {getExpectedWithdrawals} from "./block/processWithdrawals.js"; + +export {getAttestationParticipationStatus, processAttestationsAltair} from "./block/processAttestationsAltair.js";