From ebe0d172b15d4dcb8f47c17bb26d9bd53a6fd09a Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 5 Apr 2024 23:21:10 +0100 Subject: [PATCH 1/6] feat: add finalized metadata field to HTTP API responses --- .../api/src/beacon/routes/beacon/block.ts | 29 +++-- .../api/src/beacon/routes/beacon/rewards.ts | 40 +++++-- .../api/src/beacon/routes/beacon/state.ts | 108 +++++++++++++----- packages/api/src/beacon/routes/debug.ts | 37 ++++-- packages/api/src/utils/types.ts | 16 +++ .../api/test/unit/beacon/oapiSpec.test.ts | 23 ---- .../api/test/unit/beacon/testData/beacon.ts | 38 +++--- .../api/test/unit/beacon/testData/debug.ts | 9 +- .../src/api/impl/beacon/blocks/index.ts | 31 +++-- .../src/api/impl/beacon/blocks/utils.ts | 4 +- .../src/api/impl/beacon/rewards/index.ts | 12 +- .../src/api/impl/beacon/state/index.ts | 32 ++++-- .../src/api/impl/beacon/state/utils.ts | 6 +- .../beacon-node/src/api/impl/debug/index.ts | 8 +- packages/beacon-node/src/chain/chain.ts | 50 +++++--- packages/beacon-node/src/chain/interface.ts | 14 ++- .../api/impl/beacon/state/endpoint.test.ts | 4 +- .../beacon/blocks/getBlockHeaders.test.ts | 4 +- packages/cli/test/sim/endpoints.test.ts | 2 + .../cli/test/utils/mockBeaconApiServer.ts | 2 +- .../unit/proof_provider/payload_store.test.ts | 1 + .../test/unit/services/attestation.test.ts | 2 +- .../unit/services/attestationDuties.test.ts | 4 +- .../unit/services/syncCommitteDuties.test.ts | 2 +- .../test/unit/services/syncCommittee.test.ts | 2 +- 25 files changed, 322 insertions(+), 158 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index 16664b59ead6..7dd5511a22f6 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -16,6 +16,7 @@ import { ContainerDataExecutionOptimistic, WithExecutionOptimistic, ContainerData, + WithFinalized, } from "../../../utils/index.js"; import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; import {parseAcceptHeader, writeAcceptHeader} from "../../../utils/acceptHeader.js"; @@ -32,6 +33,11 @@ export type BlockId = RootHex | Slot | "head" | "genesis" | "finalized"; */ export type ExecutionOptimistic = boolean; +/** + * True if the response references the finalized history of the chain, as determined by fork choice. + */ +export type Finalized = boolean; + export type BlockHeaderResponse = { root: Root; canonical: boolean; @@ -66,6 +72,7 @@ export type BlockV2Response = T extends "ssz" [HttpStatusCode.OK]: { data: allForks.SignedBeaconBlock; executionOptimistic: ExecutionOptimistic; + finalized: Finalized; version: ForkName; }; }, @@ -103,6 +110,7 @@ export type Api = { [HttpStatusCode.OK]: { data: phase0.Attestation[]; executionOptimistic: ExecutionOptimistic; + finalized: Finalized; }; }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND @@ -121,6 +129,7 @@ export type Api = { [HttpStatusCode.OK]: { data: BlockHeaderResponse; executionOptimistic: ExecutionOptimistic; + finalized: Finalized; }; }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND @@ -139,6 +148,7 @@ export type Api = { [HttpStatusCode.OK]: { data: BlockHeaderResponse[]; executionOptimistic: ExecutionOptimistic; + finalized: Finalized; }; }, HttpStatusCode.BAD_REQUEST @@ -157,6 +167,7 @@ export type Api = { [HttpStatusCode.OK]: { data: {root: Root}; executionOptimistic: ExecutionOptimistic; + finalized: Finalized; }; }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND @@ -236,7 +247,11 @@ export type Api = { indices?: number[] ): Promise< ApiClientResponse<{ - [HttpStatusCode.OK]: {executionOptimistic: ExecutionOptimistic; data: deneb.BlobSidecars}; + [HttpStatusCode.OK]: { + data: deneb.BlobSidecars; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; }> >; }; @@ -395,11 +410,11 @@ export function getReturnTypes(): ReturnTypes { return { getBlock: ContainerData(ssz.phase0.SignedBeaconBlock), - getBlockV2: WithExecutionOptimistic(WithVersion((fork) => ssz[fork].SignedBeaconBlock)), - getBlockAttestations: ContainerDataExecutionOptimistic(ArrayOf(ssz.phase0.Attestation)), - getBlockHeader: ContainerDataExecutionOptimistic(BeaconHeaderResType), - getBlockHeaders: ContainerDataExecutionOptimistic(ArrayOf(BeaconHeaderResType)), - getBlockRoot: ContainerDataExecutionOptimistic(RootContainer), - getBlobSidecars: ContainerDataExecutionOptimistic(ssz.deneb.BlobSidecars), + getBlockV2: WithFinalized(WithExecutionOptimistic(WithVersion((fork) => ssz[fork].SignedBeaconBlock))), + getBlockAttestations: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ssz.phase0.Attestation))), + getBlockHeader: WithFinalized(ContainerDataExecutionOptimistic(BeaconHeaderResType)), + getBlockHeaders: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(BeaconHeaderResType))), + getBlockRoot: WithFinalized(ContainerDataExecutionOptimistic(RootContainer)), + getBlobSidecars: WithFinalized(ContainerDataExecutionOptimistic(ssz.deneb.BlobSidecars)), }; } diff --git a/packages/api/src/beacon/routes/beacon/rewards.ts b/packages/api/src/beacon/routes/beacon/rewards.ts index e317587df3bd..f2136760c290 100644 --- a/packages/api/src/beacon/routes/beacon/rewards.ts +++ b/packages/api/src/beacon/routes/beacon/rewards.ts @@ -8,6 +8,7 @@ import { ReqSerializers, ContainerDataExecutionOptimistic, ArrayOf, + WithFinalized, } from "../../../utils/index.js"; import {HttpStatusCode} from "../../../utils/client/httpStatusCode.js"; import {ApiClientResponse} from "../../../interfaces.js"; @@ -22,6 +23,11 @@ import {ValidatorId} from "./state.js"; */ export type ExecutionOptimistic = boolean; +/** + * True if the response references the finalized history of the chain, as determined by fork choice. + */ +export type Finalized = boolean; + /** * Rewards info for a single block. Every reward value is in Gwei. */ @@ -88,11 +94,15 @@ export type Api = { * @param blockId Block identifier. * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", \, \. */ - getBlockRewards( - blockId: BlockId - ): Promise< + getBlockRewards(blockId: BlockId): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: BlockRewards; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: BlockRewards; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -108,7 +118,13 @@ export type Api = { validatorIds?: ValidatorId[] ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: AttestationsRewards; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: AttestationsRewards; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -126,7 +142,13 @@ export type Api = { validatorIds?: ValidatorId[] ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: SyncCommitteeRewards; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: SyncCommitteeRewards; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -228,8 +250,8 @@ export function getReturnTypes(): ReturnTypes { ); return { - getBlockRewards: ContainerDataExecutionOptimistic(BlockRewardsResponse), - getAttestationsRewards: ContainerDataExecutionOptimistic(AttestationsRewardsResponse), - getSyncCommitteeRewards: ContainerDataExecutionOptimistic(ArrayOf(SyncCommitteeRewardsResponse)), + getBlockRewards: WithFinalized(ContainerDataExecutionOptimistic(BlockRewardsResponse)), + getAttestationsRewards: WithFinalized(ContainerDataExecutionOptimistic(AttestationsRewardsResponse)), + getSyncCommitteeRewards: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(SyncCommitteeRewardsResponse))), }; } diff --git a/packages/api/src/beacon/routes/beacon/state.ts b/packages/api/src/beacon/routes/beacon/state.ts index 322d7ba5e796..e9897eb3e07c 100644 --- a/packages/api/src/beacon/routes/beacon/state.ts +++ b/packages/api/src/beacon/routes/beacon/state.ts @@ -10,6 +10,7 @@ import { Schema, ReqSerializers, ReqSerializer, + WithFinalized, } from "../../../utils/index.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -23,6 +24,11 @@ export type ValidatorId = string | number; */ export type ExecutionOptimistic = boolean; +/** + * True if the response references the finalized history of the chain, as determined by fork choice. + */ +export type Finalized = boolean; + export type ValidatorStatus = | "active" | "pending_initialized" @@ -85,11 +91,15 @@ export type Api = { * @param stateId State identifier. * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. */ - getStateRoot( - stateId: StateId - ): Promise< + getStateRoot(stateId: StateId): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: {root: Root}; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: {root: Root}; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -100,11 +110,15 @@ export type Api = { * @param stateId State identifier. * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. */ - getStateFork( - stateId: StateId - ): Promise< + getStateFork(stateId: StateId): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: phase0.Fork; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: phase0.Fork; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -121,7 +135,13 @@ export type Api = { epoch?: Epoch ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: {randao: Root}; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: {randao: Root}; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -133,11 +153,15 @@ export type Api = { * @param stateId State identifier. * Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \, \. */ - getStateFinalityCheckpoints( - stateId: StateId - ): Promise< + getStateFinalityCheckpoints(stateId: StateId): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: FinalityCheckpoints; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: FinalityCheckpoints; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -155,7 +179,13 @@ export type Api = { filters?: ValidatorFilters ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: ValidatorResponse[]; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: ValidatorResponse[]; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -172,7 +202,13 @@ export type Api = { validatorId: ValidatorId ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: ValidatorResponse; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: ValidatorResponse; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -189,7 +225,13 @@ export type Api = { indices?: ValidatorId[] ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: ValidatorBalance[]; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: ValidatorBalance[]; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST > >; @@ -208,7 +250,13 @@ export type Api = { filters?: CommitteesFilters ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: EpochCommitteeResponse[]; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: EpochCommitteeResponse[]; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -218,7 +266,13 @@ export type Api = { epoch?: Epoch ): Promise< ApiClientResponse< - {[HttpStatusCode.OK]: {data: EpochSyncCommitteeResponse; executionOptimistic: ExecutionOptimistic}}, + { + [HttpStatusCode.OK]: { + data: EpochSyncCommitteeResponse; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + }; + }, HttpStatusCode.BAD_REQUEST | HttpStatusCode.NOT_FOUND > >; @@ -376,14 +430,14 @@ export function getReturnTypes(): ReturnTypes { ); return { - getStateRoot: ContainerDataExecutionOptimistic(RootContainer), - getStateFork: ContainerDataExecutionOptimistic(ssz.phase0.Fork), - getStateRandao: ContainerDataExecutionOptimistic(RandaoContainer), - getStateFinalityCheckpoints: ContainerDataExecutionOptimistic(FinalityCheckpoints), - getStateValidators: ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponse)), - getStateValidator: ContainerDataExecutionOptimistic(ValidatorResponse), - getStateValidatorBalances: ContainerDataExecutionOptimistic(ArrayOf(ValidatorBalance)), - getEpochCommittees: ContainerDataExecutionOptimistic(ArrayOf(EpochCommitteeResponse)), - getEpochSyncCommittees: ContainerDataExecutionOptimistic(EpochSyncCommitteesResponse), + getStateRoot: WithFinalized(ContainerDataExecutionOptimistic(RootContainer)), + getStateFork: WithFinalized(ContainerDataExecutionOptimistic(ssz.phase0.Fork)), + getStateRandao: WithFinalized(ContainerDataExecutionOptimistic(RandaoContainer)), + getStateFinalityCheckpoints: WithFinalized(ContainerDataExecutionOptimistic(FinalityCheckpoints)), + getStateValidators: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorResponse))), + getStateValidator: WithFinalized(ContainerDataExecutionOptimistic(ValidatorResponse)), + getStateValidatorBalances: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(ValidatorBalance))), + getEpochCommittees: WithFinalized(ContainerDataExecutionOptimistic(ArrayOf(EpochCommitteeResponse))), + getEpochSyncCommittees: WithFinalized(ContainerDataExecutionOptimistic(EpochSyncCommitteesResponse)), }; } diff --git a/packages/api/src/beacon/routes/debug.ts b/packages/api/src/beacon/routes/debug.ts index 99403e61fe58..773ae86728b5 100644 --- a/packages/api/src/beacon/routes/debug.ts +++ b/packages/api/src/beacon/routes/debug.ts @@ -14,12 +14,13 @@ import { ReqSerializer, ContainerDataExecutionOptimistic, WithExecutionOptimistic, + WithFinalized, ContainerData, } from "../../utils/index.js"; import {HttpStatusCode} from "../../utils/client/httpStatusCode.js"; import {parseAcceptHeader, writeAcceptHeader} from "../../utils/acceptHeader.js"; import {ApiClientResponse, ResponseFormat} from "../../interfaces.js"; -import {ExecutionOptimistic, StateId} from "./beacon/state.js"; +import {ExecutionOptimistic, Finalized, StateId} from "./beacon/state.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -85,7 +86,9 @@ export type Api = { stateId: StateId, format?: "json" ): Promise< - ApiClientResponse<{[HttpStatusCode.OK]: {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic}}> + ApiClientResponse<{ + [HttpStatusCode.OK]: {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic; finalized: Finalized}; + }> >; getState(stateId: StateId, format: "ssz"): Promise>; getState( @@ -93,7 +96,9 @@ export type Api = { format?: ResponseFormat ): Promise< ApiClientResponse<{ - [HttpStatusCode.OK]: Uint8Array | {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic}; + [HttpStatusCode.OK]: + | Uint8Array + | {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic; finalized: Finalized}; }> >; @@ -110,7 +115,12 @@ export type Api = { format?: "json" ): Promise< ApiClientResponse<{ - [HttpStatusCode.OK]: {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic; version: ForkName}; + [HttpStatusCode.OK]: { + data: allForks.BeaconState; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + version: ForkName; + }; }> >; getStateV2(stateId: StateId, format: "ssz"): Promise>; @@ -121,7 +131,12 @@ export type Api = { ApiClientResponse<{ [HttpStatusCode.OK]: | Uint8Array - | {data: allForks.BeaconState; executionOptimistic: ExecutionOptimistic; version: ForkName}; + | { + data: allForks.BeaconState; + executionOptimistic: ExecutionOptimistic; + finalized: Finalized; + version: ForkName; + }; }> >; }; @@ -185,10 +200,14 @@ export function getReturnTypes(): ReturnTypes { getDebugChainHeads: ContainerData(ArrayOf(SlotRoot)), getDebugChainHeadsV2: ContainerData(ArrayOf(SlotRootExecutionOptimistic)), getProtoArrayNodes: ContainerData(ArrayOf(protoNodeSszType)), - getState: ContainerDataExecutionOptimistic(ssz.phase0.BeaconState), - getStateV2: WithExecutionOptimistic( - // Teku returns fork as UPPERCASE - WithVersion((fork: ForkName) => ssz[fork.toLowerCase() as ForkName].BeaconState as TypeJson) + getState: WithFinalized(ContainerDataExecutionOptimistic(ssz.phase0.BeaconState)), + getStateV2: WithFinalized( + WithExecutionOptimistic( + // Teku returns fork as UPPERCASE + WithVersion( + (fork: ForkName) => ssz[fork.toLowerCase() as ForkName].BeaconState as TypeJson + ) + ) ), }; } diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts index e218a71e3033..999fd93bdc81 100644 --- a/packages/api/src/utils/types.ts +++ b/packages/api/src/utils/types.ts @@ -178,6 +178,22 @@ export function WithExecutionOptimistic( }; } +/** + * SSZ factory helper to wrap an existing type with `{finalized: boolean}` + */ +export function WithFinalized(type: TypeJson): TypeJson { + return { + toJson: ({finalized, ...data}) => ({ + ...(type.toJson(data as unknown as T) as Record), + finalized, + }), + fromJson: ({finalized, ...data}: T & {finalized: boolean}) => ({ + ...type.fromJson(data), + finalized, + }), + }; +} + /** * SSZ factory helper to wrap an existing type with `{executionPayloadValue: Wei, consensusBlockValue: Wei}` */ diff --git a/packages/api/test/unit/beacon/oapiSpec.test.ts b/packages/api/test/unit/beacon/oapiSpec.test.ts index 229098dc7a3e..6324a0cd56be 100644 --- a/packages/api/test/unit/beacon/oapiSpec.test.ts +++ b/packages/api/test/unit/beacon/oapiSpec.test.ts @@ -103,29 +103,6 @@ const ignoredOperations = [ ]; const ignoredProperties: Record = { - /* - https://github.com/ChainSafe/lodestar/issues/5693 - missing finalized - */ - getStateRoot: {response: ["finalized"]}, - getStateFork: {response: ["finalized"]}, - getStateFinalityCheckpoints: {response: ["finalized"]}, - getStateValidators: {response: ["finalized"]}, - getStateValidator: {response: ["finalized"]}, - getStateValidatorBalances: {response: ["finalized"]}, - getEpochCommittees: {response: ["finalized"]}, - getEpochSyncCommittees: {response: ["finalized"]}, - getStateRandao: {response: ["finalized"]}, - getBlockHeaders: {response: ["finalized"]}, - getBlockHeader: {response: ["finalized"]}, - getBlockV2: {response: ["finalized"]}, - getBlockRoot: {response: ["finalized"]}, - getBlockAttestations: {response: ["finalized"]}, - getStateV2: {response: ["finalized"]}, - getBlockRewards: {response: ["finalized"]}, - getAttestationsRewards: {response: ["finalized"]}, - getSyncCommitteeRewards: {response: ["finalized"]}, - /* https://github.com/ChainSafe/lodestar/issues/6168 /query/syncing_status - must be integer diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index fb7ea4efeaae..88894b81d5f0 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -37,23 +37,28 @@ export const testData: GenericServerTestCases = { }, getBlockV2: { args: ["head", "json"], - res: {executionOptimistic: true, data: ssz.bellatrix.SignedBeaconBlock.defaultValue(), version: ForkName.bellatrix}, + res: { + executionOptimistic: true, + finalized: false, + data: ssz.bellatrix.SignedBeaconBlock.defaultValue(), + version: ForkName.bellatrix, + }, }, getBlockAttestations: { args: ["head"], - res: {executionOptimistic: true, data: [ssz.phase0.Attestation.defaultValue()]}, + res: {executionOptimistic: true, finalized: false, data: [ssz.phase0.Attestation.defaultValue()]}, }, getBlockHeader: { args: ["head"], - res: {executionOptimistic: true, data: blockHeaderResponse}, + res: {executionOptimistic: true, finalized: false, data: blockHeaderResponse}, }, getBlockHeaders: { args: [{slot: 1, parentRoot: toHexString(root)}], - res: {executionOptimistic: true, data: [blockHeaderResponse]}, + res: {executionOptimistic: true, finalized: false, data: [blockHeaderResponse]}, }, getBlockRoot: { args: ["head"], - res: {executionOptimistic: true, data: {root}}, + res: {executionOptimistic: true, finalized: false, data: {root}}, }, publishBlock: { args: [ssz.phase0.SignedBeaconBlock.defaultValue()], @@ -73,7 +78,7 @@ export const testData: GenericServerTestCases = { }, getBlobSidecars: { args: ["head", [0]], - res: {executionOptimistic: true, data: ssz.deneb.BlobSidecars.defaultValue()}, + res: {executionOptimistic: true, finalized: false, data: ssz.deneb.BlobSidecars.defaultValue()}, }, // pool @@ -127,20 +132,21 @@ export const testData: GenericServerTestCases = { getStateRoot: { args: ["head"], - res: {executionOptimistic: true, data: {root}}, + res: {executionOptimistic: true, finalized: false, data: {root}}, }, getStateFork: { args: ["head"], - res: {executionOptimistic: true, data: ssz.phase0.Fork.defaultValue()}, + res: {executionOptimistic: true, finalized: false, data: ssz.phase0.Fork.defaultValue()}, }, getStateRandao: { args: ["head", 1], - res: {executionOptimistic: true, data: {randao}}, + res: {executionOptimistic: true, finalized: false, data: {randao}}, }, getStateFinalityCheckpoints: { args: ["head"], res: { executionOptimistic: true, + finalized: false, data: { previousJustified: ssz.phase0.Checkpoint.defaultValue(), currentJustified: ssz.phase0.Checkpoint.defaultValue(), @@ -150,23 +156,23 @@ export const testData: GenericServerTestCases = { }, getStateValidators: { args: ["head", {id: [pubkeyHex, "1300"], status: ["active_ongoing"]}], - res: {executionOptimistic: true, data: [validatorResponse]}, + res: {executionOptimistic: true, finalized: false, data: [validatorResponse]}, }, getStateValidator: { args: ["head", pubkeyHex], - res: {executionOptimistic: true, data: validatorResponse}, + res: {executionOptimistic: true, finalized: false, data: validatorResponse}, }, getStateValidatorBalances: { args: ["head", ["1300"]], - res: {executionOptimistic: true, data: [{index: 1300, balance}]}, + res: {executionOptimistic: true, finalized: false, data: [{index: 1300, balance}]}, }, getEpochCommittees: { args: ["head", {index: 1, slot: 2, epoch: 3}], - res: {executionOptimistic: true, data: [{index: 1, slot: 2, validators: [1300]}]}, + res: {executionOptimistic: true, finalized: false, data: [{index: 1, slot: 2, validators: [1300]}]}, }, getEpochSyncCommittees: { args: ["head", 1], - res: {executionOptimistic: true, data: {validators: [1300], validatorAggregates: [[1300]]}}, + res: {executionOptimistic: true, finalized: false, data: {validators: [1300], validatorAggregates: [[1300]]}}, }, // reward @@ -175,6 +181,7 @@ export const testData: GenericServerTestCases = { args: ["head"], res: { executionOptimistic: true, + finalized: false, data: { proposerIndex: 0, total: 15, @@ -187,13 +194,14 @@ export const testData: GenericServerTestCases = { }, getSyncCommitteeRewards: { args: ["head", ["1300"]], - res: {executionOptimistic: true, data: [{validatorIndex: 1300, reward}]}, + res: {executionOptimistic: true, finalized: false, data: [{validatorIndex: 1300, reward}]}, }, getAttestationsRewards: { args: [10, ["1300"]], res: { executionOptimistic: true, + finalized: false, data: { idealRewards: [ { diff --git a/packages/api/test/unit/beacon/testData/debug.ts b/packages/api/test/unit/beacon/testData/debug.ts index 3ceda4574605..aa595046b8ba 100644 --- a/packages/api/test/unit/beacon/testData/debug.ts +++ b/packages/api/test/unit/beacon/testData/debug.ts @@ -47,10 +47,15 @@ export const testData: GenericServerTestCases = { }, getState: { args: ["head", "json"], - res: {executionOptimistic: true, data: ssz.phase0.BeaconState.defaultValue()}, + res: {executionOptimistic: true, finalized: false, data: ssz.phase0.BeaconState.defaultValue()}, }, getStateV2: { args: ["head", "json"], - res: {executionOptimistic: true, data: ssz.altair.BeaconState.defaultValue(), version: ForkName.altair}, + res: { + executionOptimistic: true, + finalized: false, + data: ssz.altair.BeaconState.defaultValue(), + version: ForkName.altair, + }, }, }; diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 79e258960b2c..8701efe9f87c 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -256,6 +256,8 @@ export function getBeaconBlockApi({ // If one block in the response contains an optimistic block, mark the entire response as optimistic let executionOptimistic = false; + // If one block in the response is non finalized, mark the entire response as unfinalized + let finalized = true; const result: routes.beacon.BlockHeaderResponse[] = []; if (filters.parentRoot) { @@ -275,12 +277,14 @@ export function getBeaconBlockApi({ if (isOptimisticBlock(canonical)) { executionOptimistic = true; } + finalized = false; } } }) ); return { executionOptimistic, + finalized, data: result.filter( (item) => // skip if no slot filter @@ -297,18 +301,21 @@ export function getBeaconBlockApi({ if (filters.slot !== undefined) { // future slot if (filters.slot > headSlot) { - return {executionOptimistic: false, data: []}; + return {executionOptimistic: false, finalized: false, data: []}; } const canonicalBlock = await chain.getCanonicalBlockAtSlot(filters.slot); // skip slot if (!canonicalBlock) { - return {executionOptimistic: false, data: []}; + return {executionOptimistic: false, finalized: false, data: []}; } const canonicalRoot = config .getForkTypes(canonicalBlock.block.message.slot) .BeaconBlock.hashTreeRoot(canonicalBlock.block.message); result.push(toBeaconHeaderResponse(config, canonicalBlock.block, true)); + if (!canonicalBlock.finalized) { + finalized = false; + } // fork blocks // TODO: What is this logic? @@ -317,6 +324,7 @@ export function getBeaconBlockApi({ if (isOptimisticBlock(summary)) { executionOptimistic = true; } + finalized = false; if (summary.blockRoot !== toHexString(canonicalRoot)) { const block = await db.block.get(fromHexString(summary.blockRoot)); @@ -330,14 +338,16 @@ export function getBeaconBlockApi({ return { executionOptimistic, + finalized, data: result, }; }, async getBlockHeader(blockId) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); return { executionOptimistic, + finalized, data: toBeaconHeaderResponse(config, block, true), }; }, @@ -353,21 +363,23 @@ export function getBeaconBlockApi({ }, async getBlockV2(blockId, format?: ResponseFormat) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); if (format === "ssz") { return config.getForkTypes(block.message.slot).SignedBeaconBlock.serialize(block); } return { executionOptimistic, + finalized, data: block, version: config.getForkName(block.message.slot), }; }, async getBlockAttestations(blockId) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); return { executionOptimistic, + finalized, data: Array.from(block.message.body.attestations), }; }, @@ -381,6 +393,7 @@ export function getBeaconBlockApi({ if (slot === head.slot) { return { executionOptimistic: isOptimisticBlock(head), + finalized: false, data: {root: fromHexString(head.blockRoot)}, }; } @@ -389,6 +402,7 @@ export function getBeaconBlockApi({ const state = chain.getHeadState(); return { executionOptimistic: isOptimisticBlock(head), + finalized: head.slot <= chain.forkChoice.getFinalizedBlock().slot, data: {root: state.blockRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT)}, }; } @@ -396,14 +410,16 @@ export function getBeaconBlockApi({ const head = chain.forkChoice.getHead(); return { executionOptimistic: isOptimisticBlock(head), + finalized: false, data: {root: fromHexString(head.blockRoot)}, }; } // Slow path - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); return { executionOptimistic, + finalized, data: {root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)}, }; }, @@ -420,7 +436,7 @@ export function getBeaconBlockApi({ }, async getBlobSidecars(blockId, indices) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); const blockRoot = config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); let {blobSidecars} = (await db.blobSidecars.get(blockRoot)) ?? {}; @@ -434,6 +450,7 @@ export function getBeaconBlockApi({ return { executionOptimistic, + finalized, data: indices ? blobSidecars.filter(({index}) => indices.includes(index)) : blobSidecars, }; }, diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts index 175b15bbc9cc..e69e4fa74419 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts @@ -25,7 +25,7 @@ export function toBeaconHeaderResponse( export async function resolveBlockId( chain: IBeaconChain, blockId: routes.beacon.BlockId -): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean}> { +): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean}> { const res = await resolveBlockIdOrNull(chain, blockId); if (!res) { throw new ApiError(404, `No block found for id '${blockId}'`); @@ -37,7 +37,7 @@ export async function resolveBlockId( async function resolveBlockIdOrNull( chain: IBeaconChain, blockId: routes.beacon.BlockId -): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> { +): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { blockId = String(blockId).toLowerCase(); if (blockId === "head") { return chain.getBlockByRoot(chain.forkChoice.getHead().blockRoot); diff --git a/packages/beacon-node/src/api/impl/beacon/rewards/index.ts b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts index 8b94c1c29174..f1c7d3eb6b8e 100644 --- a/packages/beacon-node/src/api/impl/beacon/rewards/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts @@ -5,18 +5,18 @@ import {resolveBlockId} from "../blocks/utils.js"; export function getBeaconRewardsApi({chain}: Pick): ServerApi { return { async getBlockRewards(blockId) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); const data = await chain.getBlockRewards(block.message); - return {data, executionOptimistic}; + return {data, executionOptimistic, finalized}; }, async getAttestationsRewards(epoch, validatorIds) { - const {rewards, executionOptimistic} = await chain.getAttestationsRewards(epoch, validatorIds); - return {data: rewards, executionOptimistic}; + const {rewards, executionOptimistic, finalized} = await chain.getAttestationsRewards(epoch, validatorIds); + return {data: rewards, executionOptimistic, finalized}; }, async getSyncCommitteeRewards(blockId, validatorIds) { - const {block, executionOptimistic} = await resolveBlockId(chain, blockId); + const {block, executionOptimistic, finalized} = await resolveBlockId(chain, blockId); const data = await chain.getSyncCommitteeRewards(block.message, validatorIds); - return {data, executionOptimistic}; + return {data, executionOptimistic, finalized}; }, }; } diff --git a/packages/beacon-node/src/api/impl/beacon/state/index.ts b/packages/beacon-node/src/api/impl/beacon/state/index.ts index c9f74b45a9f2..275b0614af0f 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -24,29 +24,31 @@ export function getBeaconStateApi({ }: Pick): ServerApi { async function getState( stateId: routes.beacon.StateId - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean}> { + ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean}> { return resolveStateId(chain, stateId); } return { async getStateRoot(stateId) { - const {state, executionOptimistic} = await getState(stateId); + const {state, executionOptimistic, finalized} = await getState(stateId); return { executionOptimistic, + finalized, data: {root: state.hashTreeRoot()}, }; }, async getStateFork(stateId) { - const {state, executionOptimistic} = await getState(stateId); + const {state, executionOptimistic, finalized} = await getState(stateId); return { executionOptimistic, + finalized, data: state.fork, }; }, async getStateRandao(stateId, epoch) { - const {state, executionOptimistic} = await getState(stateId); + const {state, executionOptimistic, finalized} = await getState(stateId); const stateEpoch = computeEpochAtSlot(state.slot); const usedEpoch = epoch ?? stateEpoch; @@ -58,6 +60,7 @@ export function getBeaconStateApi({ return { executionOptimistic, + finalized, data: { randao, }, @@ -65,9 +68,10 @@ export function getBeaconStateApi({ }, async getStateFinalityCheckpoints(stateId) { - const {state, executionOptimistic} = await getState(stateId); + const {state, executionOptimistic, finalized} = await getState(stateId); return { executionOptimistic, + finalized, data: { currentJustified: state.currentJustifiedCheckpoint, previousJustified: state.previousJustifiedCheckpoint, @@ -77,7 +81,7 @@ export function getBeaconStateApi({ }, async getStateValidators(stateId, filters) { - const {state, executionOptimistic} = await resolveStateId(chain, stateId); + const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); const currentEpoch = getCurrentEpoch(state); const {validators, balances} = state; // Get the validators sub tree once for all the loop const {pubkey2index} = chain.getHeadState().epochCtx; @@ -103,12 +107,14 @@ export function getBeaconStateApi({ } return { executionOptimistic, + finalized, data: validatorResponses, }; } else if (filters?.status) { const validatorsByStatus = filterStateValidatorsByStatus(filters.status, state, pubkey2index, currentEpoch); return { executionOptimistic, + finalized, data: validatorsByStatus, }; } @@ -123,12 +129,13 @@ export function getBeaconStateApi({ return { executionOptimistic, + finalized, data: resp, }; }, async getStateValidator(stateId, validatorId) { - const {state, executionOptimistic} = await resolveStateId(chain, stateId); + const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); const {pubkey2index} = chain.getHeadState().epochCtx; const resp = getStateValidatorIndex(validatorId, state, pubkey2index); @@ -139,6 +146,7 @@ export function getBeaconStateApi({ const validatorIndex = resp.validatorIndex; return { executionOptimistic, + finalized, data: toValidatorResponse( validatorIndex, state.validators.getReadonly(validatorIndex), @@ -149,7 +157,7 @@ export function getBeaconStateApi({ }, async getStateValidatorBalances(stateId, indices) { - const {state, executionOptimistic} = await resolveStateId(chain, stateId); + const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); if (indices) { const headState = chain.getHeadState(); @@ -169,6 +177,7 @@ export function getBeaconStateApi({ } return { executionOptimistic, + finalized, data: balances, }; } @@ -181,12 +190,13 @@ export function getBeaconStateApi({ } return { executionOptimistic, + finalized, data: resp, }; }, async getEpochCommittees(stateId, filters) { - const {state, executionOptimistic} = await resolveStateId(chain, stateId); + const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); const stateCached = state as CachedBeaconStateAltair; if (stateCached.epochCtx === undefined) { @@ -218,6 +228,7 @@ export function getBeaconStateApi({ return { executionOptimistic, + finalized, data: committeesFlat, }; }, @@ -228,7 +239,7 @@ export function getBeaconStateApi({ */ async getEpochSyncCommittees(stateId, epoch) { // TODO: Should pick a state with the provided epoch too - const {state, executionOptimistic} = await resolveStateId(chain, stateId); + const {state, executionOptimistic, finalized} = await resolveStateId(chain, stateId); // TODO: If possible compute the syncCommittees in advance of the fork and expose them here. // So the validators can prepare and potentially attest the first block. Not critical tho, it's very unlikely @@ -246,6 +257,7 @@ export function getBeaconStateApi({ return { executionOptimistic, + finalized, data: { validators: syncCommitteeCache.validatorIndices, // TODO: This is not used by the validator and will be deprecated soon diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index a1e4d43086ff..5b493868255b 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -12,7 +12,7 @@ export async function resolveStateId( chain: IBeaconChain, stateId: routes.beacon.StateId, opts?: StateGetOpts -): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean}> { +): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean}> { const stateRes = await resolveStateIdOrNull(chain, stateId, opts); if (!stateRes) { throw new ApiError(404, `No state found for id '${stateId}'`); @@ -25,12 +25,12 @@ async function resolveStateIdOrNull( chain: IBeaconChain, stateId: routes.beacon.StateId, opts?: StateGetOpts -): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean} | null> { +): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { if (stateId === "head") { // TODO: This is not OK, head and headState must be fetched atomically const head = chain.forkChoice.getHead(); const headState = chain.getHeadState(); - return {state: headState, executionOptimistic: isOptimisticBlock(head)}; + return {state: headState, executionOptimistic: isOptimisticBlock(head), finalized: false}; } if (stateId === "genesis") { diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index 22ba4e607c6b..36945b1b1fe1 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -37,24 +37,24 @@ export function getDebugApi({chain, config}: Pick { + ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot >= finalizedBlock.slot) { @@ -417,7 +417,7 @@ export class BeaconChain implements IBeaconChain { {dontTransferCache: true}, RegenCaller.restApi ); - return {state, executionOptimistic: isOptimisticBlock(block)}; + return {state, executionOptimistic: isOptimisticBlock(block), finalized: false}; } else { // Just check if state is already in the cache. If it's not dialed to the correct slot, // do not bother in advancing the state. restApiCanTriggerRegen == false means do no work @@ -427,25 +427,29 @@ export class BeaconChain implements IBeaconChain { } const state = this.regen.getStateSync(block.stateRoot); - return state && {state, executionOptimistic: isOptimisticBlock(block)}; + return state && {state, executionOptimistic: isOptimisticBlock(block), finalized: false}; } } else { // request for finalized state // do not attempt regen, just check if state is already in DB const state = await this.db.stateArchive.get(slot); - return state && {state, executionOptimistic: false}; + return state && {state, executionOptimistic: false, finalized: true}; } } async getStateByStateRoot( stateRoot: RootHex, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean} | null> { + ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null> { if (opts?.allowRegen) { const state = await this.regen.getState(stateRoot, RegenCaller.restApi); const block = this.forkChoice.getBlock(state.latestBlockHeader.hashTreeRoot()); - return {state, executionOptimistic: block != null && isOptimisticBlock(block)}; + return { + state, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: state.epochCtx.epoch <= this.forkChoice.getFinalizedCheckpoint().epoch, + }; } // TODO: This can only fulfill requests for a very narrow set of roots. @@ -456,21 +460,29 @@ export class BeaconChain implements IBeaconChain { const cachedStateCtx = this.regen.getStateSync(stateRoot); if (cachedStateCtx) { const block = this.forkChoice.getBlock(cachedStateCtx.latestBlockHeader.hashTreeRoot()); - return {state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block)}; + return { + state: cachedStateCtx, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: cachedStateCtx.epochCtx.epoch <= this.forkChoice.getFinalizedCheckpoint().epoch, + }; } const data = await this.db.stateArchive.getByRoot(fromHexString(stateRoot)); - return data && {state: data, executionOptimistic: false}; + return data && {state: data, executionOptimistic: false, finalized: true}; } getStateByCheckpoint( checkpoint: CheckpointWithHex - ): {state: BeaconStateAllForks; executionOptimistic: boolean} | null { + ): {state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null { // TODO: this is not guaranteed to work with new state caches, should work on this before we turn n-historical state on const cachedStateCtx = this.regen.getCheckpointStateSync(checkpoint); if (cachedStateCtx) { const block = this.forkChoice.getBlock(cachedStateCtx.latestBlockHeader.hashTreeRoot()); - return {state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block)}; + return { + state: cachedStateCtx, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: cachedStateCtx.epochCtx.epoch <= this.forkChoice.getFinalizedCheckpoint().epoch, + }; } return null; @@ -478,7 +490,7 @@ export class BeaconChain implements IBeaconChain { async getCanonicalBlockAtSlot( slot: Slot - ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> { + ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot > finalizedBlock.slot) { // Unfinalized slot, attempt to find in fork-choice @@ -486,7 +498,7 @@ export class BeaconChain implements IBeaconChain { if (block) { const data = await this.db.block.get(fromHexString(block.blockRoot)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block)}; + return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; } } // A non-finalized slot expected to be found in the hot db, could be archived during @@ -495,24 +507,24 @@ export class BeaconChain implements IBeaconChain { } const data = await this.db.blockArchive.get(slot); - return data && {block: data, executionOptimistic: false}; + return data && {block: data, executionOptimistic: false, finalized: true}; } async getBlockByRoot( root: string - ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> { + ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { const block = this.forkChoice.getBlockHex(root); if (block) { const data = await this.db.block.get(fromHexString(root)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block)}; + return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; } // If block is not found in hot db, try cold db since there could be an archive cycle happening // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getByRoot(fromHexString(root)); - return data && {block: data, executionOptimistic: false}; + return data && {block: data, executionOptimistic: false, finalized: true}; } async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { @@ -1051,7 +1063,7 @@ export class BeaconChain implements IBeaconChain { async getAttestationsRewards( epoch: Epoch, validatorIds?: (ValidatorIndex | string)[] - ): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean}> { + ): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean; finalized: 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 @@ -1060,7 +1072,7 @@ export class BeaconChain implements IBeaconChain { throw Error(`State is unavailable for slot ${slot}`); } - const {executionOptimistic} = stateResult; + const {executionOptimistic, finalized} = stateResult; const stateRoot = toHexString(stateResult.state.hashTreeRoot()); const cachedState = this.regen.getStateSync(stateRoot); @@ -1071,7 +1083,7 @@ export class BeaconChain implements IBeaconChain { const rewards = await computeAttestationsRewards(epoch, cachedState, this.config, validatorIds); - return {rewards, executionOptimistic}; + return {rewards, executionOptimistic, finalized}; } async getSyncCommitteeRewards( diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 9ae703475cb6..a277834d76b7 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -138,16 +138,16 @@ export interface IBeaconChain { getStateBySlot( slot: Slot, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean} | null>; + ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>; /** Returns a local state by state root */ getStateByStateRoot( stateRoot: RootHex, opts?: StateGetOpts - ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean} | null>; + ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>; /** Returns a cached state by checkpoint */ getStateByCheckpoint( checkpoint: CheckpointWithHex - ): {state: BeaconStateAllForks; executionOptimistic: boolean} | null; + ): {state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null; /** * Since we can have multiple parallel chains, @@ -156,11 +156,13 @@ export interface IBeaconChain { */ getCanonicalBlockAtSlot( slot: Slot - ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null>; + ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; /** * Get local block by root, does not fetch from the network */ - getBlockByRoot(root: RootHex): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null>; + getBlockByRoot( + root: RootHex + ): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents; @@ -210,7 +212,7 @@ export interface IBeaconChain { getAttestationsRewards( epoch: Epoch, validatorIds?: (ValidatorIndex | string)[] - ): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean}>; + ): Promise<{rewards: AttestationsRewards; executionOptimistic: boolean; finalized: boolean}>; getSyncCommitteeRewards( blockRef: allForks.FullOrBlindedBeaconBlock, validatorIds?: (ValidatorIndex | string)[] diff --git a/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts b/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts index 182d7b3430b4..9ab3da7e53f9 100644 --- a/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts @@ -47,9 +47,11 @@ describe("beacon state api", function () { it("should return all committees for the given state", async () => { const res = await client.getEpochCommittees("head"); ApiError.assert(res); - const epochCommittees = res.response.data; + const {data: epochCommittees, executionOptimistic, finalized} = res.response; expect(epochCommittees).toHaveLength(committeeCount); + expect(executionOptimistic).toBe(false); + expect(finalized).toBe(false); const slotCount: Record = {}; const indexCount: Record = {}; diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts index d3d09f2a1fd6..38a9e6677639 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts @@ -27,7 +27,7 @@ describe("api - beacon - getBlockHeaders", function () { modules.forkChoice.getHead.mockReturnValue(generateProtoBlock({slot: 1})); when(modules.chain.getCanonicalBlockAtSlot) .calledWith(1) - .thenResolve({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false}); + .thenResolve({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false, finalized: false}); when(modules.forkChoice.getBlockSummariesAtSlot) .calledWith(1) .thenReturn([ @@ -64,7 +64,7 @@ describe("api - beacon - getBlockHeaders", function () { modules.forkChoice.getHead.mockReturnValue(generateProtoBlock({slot: 2})); when(modules.chain.getCanonicalBlockAtSlot) .calledWith(0) - .thenResolve({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false}); + .thenResolve({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false, finalized: false}); when(modules.forkChoice.getBlockSummariesAtSlot).calledWith(0).thenReturn([]); const {data: blockHeaders} = await api.getBlockHeaders({slot: 0}); expect(blockHeaders.length).toBe(1); diff --git a/packages/cli/test/sim/endpoints.test.ts b/packages/cli/test/sim/endpoints.test.ts index 0455de12306f..2ffd5fdedbd5 100644 --- a/packages/cli/test/sim/endpoints.test.ts +++ b/packages/cli/test/sim/endpoints.test.ts @@ -68,6 +68,8 @@ await env.tracker.assert( ApiError.assert(res); assert.equal(res.response.data.length, 1); + assert.equal(res.response.executionOptimistic, false); + assert.equal(res.response.finalized, false); } ); diff --git a/packages/cli/test/utils/mockBeaconApiServer.ts b/packages/cli/test/utils/mockBeaconApiServer.ts index 2b4ea14a6e12..dc5a88c21746 100644 --- a/packages/cli/test/utils/mockBeaconApiServer.ts +++ b/packages/cli/test/utils/mockBeaconApiServer.ts @@ -42,7 +42,7 @@ export function getMockBeaconApiServer(opts: RestApiServerOpts, apiOpts?: MockBe // Return empty to never discover the validators async getStateValidators() { - return {data: [], executionOptimistic: false}; + return {data: [], executionOptimistic: false, finalized: false}; }, }, diff --git a/packages/prover/test/unit/proof_provider/payload_store.test.ts b/packages/prover/test/unit/proof_provider/payload_store.test.ts index 66b866c85c08..ad3e0fabad06 100644 --- a/packages/prover/test/unit/proof_provider/payload_store.test.ts +++ b/packages/prover/test/unit/proof_provider/payload_store.test.ts @@ -51,6 +51,7 @@ const buildBlockResponse = ({ response: { version: ForkName.altair, executionOptimistic: true, + finalized: false, data: buildBlock({slot, blockNumber}), }, }); diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index 64948ec92529..397fef20b2ba 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -86,7 +86,7 @@ describe("AttestationService", function () { // Return empty replies to duties service api.beacon.getStateValidators.mockResolvedValue({ - response: {executionOptimistic: false, data: []}, + response: {executionOptimistic: false, finalized: false, data: []}, ok: true, status: HttpStatusCode.OK, }); diff --git a/packages/validator/test/unit/services/attestationDuties.test.ts b/packages/validator/test/unit/services/attestationDuties.test.ts index 3edd091d3fcd..34924a4c3170 100644 --- a/packages/validator/test/unit/services/attestationDuties.test.ts +++ b/packages/validator/test/unit/services/attestationDuties.test.ts @@ -56,7 +56,7 @@ describe("AttestationDutiesService", function () { validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; api.beacon.getStateValidators.mockResolvedValue({ - response: {data: [validatorResponse], executionOptimistic: false}, + response: {data: [validatorResponse], executionOptimistic: false, finalized: false}, ok: true, status: HttpStatusCode.OK, }); @@ -122,7 +122,7 @@ describe("AttestationDutiesService", function () { validator: {...defaultValidator.validator, pubkey: pubkeys[0]}, }; api.beacon.getStateValidators.mockResolvedValue({ - response: {data: [validatorResponse], executionOptimistic: false}, + response: {data: [validatorResponse], executionOptimistic: false, finalized: false}, ok: true, status: HttpStatusCode.OK, }); diff --git a/packages/validator/test/unit/services/syncCommitteDuties.test.ts b/packages/validator/test/unit/services/syncCommitteDuties.test.ts index bca0dd67cdc9..75e72cb7a36c 100644 --- a/packages/validator/test/unit/services/syncCommitteDuties.test.ts +++ b/packages/validator/test/unit/services/syncCommitteDuties.test.ts @@ -60,7 +60,7 @@ describe("SyncCommitteeDutiesService", function () { validator: {...defaultValidator.validator, pubkey: pubkeys[i]}, })); api.beacon.getStateValidators.mockResolvedValue({ - response: {data: validatorResponses, executionOptimistic: false}, + response: {data: validatorResponses, executionOptimistic: false, finalized: false}, ok: true, status: HttpStatusCode.OK, }); diff --git a/packages/validator/test/unit/services/syncCommittee.test.ts b/packages/validator/test/unit/services/syncCommittee.test.ts index 922447ddf85f..57870da94dc3 100644 --- a/packages/validator/test/unit/services/syncCommittee.test.ts +++ b/packages/validator/test/unit/services/syncCommittee.test.ts @@ -98,7 +98,7 @@ describe("SyncCommitteeService", function () { // Return empty replies to duties service api.beacon.getStateValidators.mockResolvedValue({ - response: {data: [], executionOptimistic: false}, + response: {data: [], executionOptimistic: false, finalized: false}, ok: true, status: HttpStatusCode.OK, }); From 9a04dcf0a2286343a3b0af5937b2920817f710d3 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 6 Apr 2024 14:40:26 +0100 Subject: [PATCH 2/6] Clean up getState type casts --- packages/beacon-node/src/api/impl/debug/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index 36945b1b1fe1..a03c00bd7150 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -39,9 +39,7 @@ export function getDebugApi({chain, config}: Pick Date: Sat, 6 Apr 2024 14:59:33 +0100 Subject: [PATCH 3/6] Fix finalized when getting block root --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 8701efe9f87c..17b45260c1ab 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -1,6 +1,6 @@ import {fromHexString, toHexString} from "@chainsafe/ssz"; import {routes, ServerApi, ResponseFormat} from "@lodestar/api"; -import {computeTimeAtSlot, reconstructFullBlockOrContents} from "@lodestar/state-transition"; +import {computeEpochAtSlot, computeTimeAtSlot, reconstructFullBlockOrContents} from "@lodestar/state-transition"; import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {sleep, toHex} from "@lodestar/utils"; import {allForks, deneb, isSignedBlockContents, ProducedBlockSource} from "@lodestar/types"; @@ -400,10 +400,11 @@ export function getBeaconBlockApi({ if (slot < head.slot && head.slot <= slot + SLOTS_PER_HISTORICAL_ROOT) { const state = chain.getHeadState(); + const rootSlot = slot % SLOTS_PER_HISTORICAL_ROOT; return { executionOptimistic: isOptimisticBlock(head), - finalized: head.slot <= chain.forkChoice.getFinalizedBlock().slot, - data: {root: state.blockRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT)}, + finalized: computeEpochAtSlot(rootSlot) <= chain.forkChoice.getFinalizedCheckpoint().epoch, + data: {root: state.blockRoots.get(rootSlot)}, }; } } else if (blockId === "head") { From 8e5305453f051037b236daa8c040a85906281a97 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 9 Apr 2024 10:49:15 +0100 Subject: [PATCH 4/6] Add comment for block from hot db --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 17b45260c1ab..2323c7326729 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -277,6 +277,7 @@ export function getBeaconBlockApi({ if (isOptimisticBlock(canonical)) { executionOptimistic = true; } + // Block from hot db which only contains unfinalized blocks finalized = false; } } From 98ab107849fbcc962e5062bb47375170111a31aa Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 9 Apr 2024 10:50:04 +0100 Subject: [PATCH 5/6] Fix case where slot equals finalized block slot --- packages/beacon-node/src/chain/chain.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 363e5cd465f5..22a205a797a6 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -417,7 +417,7 @@ export class BeaconChain implements IBeaconChain { {dontTransferCache: true}, RegenCaller.restApi ); - return {state, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return {state, executionOptimistic: isOptimisticBlock(block), finalized: slot === finalizedBlock.slot}; } else { // Just check if state is already in the cache. If it's not dialed to the correct slot, // do not bother in advancing the state. restApiCanTriggerRegen == false means do no work @@ -427,7 +427,7 @@ export class BeaconChain implements IBeaconChain { } const state = this.regen.getStateSync(block.stateRoot); - return state && {state, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return state && {state, executionOptimistic: isOptimisticBlock(block), finalized: slot === finalizedBlock.slot}; } } else { // request for finalized state From 2b0025a88575f6c455caa32e0afc357ba3cf2042 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 9 Apr 2024 11:49:29 +0100 Subject: [PATCH 6/6] Calculate epoch from unmodded slot --- packages/beacon-node/src/api/impl/beacon/blocks/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 2323c7326729..3be2f1d4b3d3 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -401,11 +401,10 @@ export function getBeaconBlockApi({ if (slot < head.slot && head.slot <= slot + SLOTS_PER_HISTORICAL_ROOT) { const state = chain.getHeadState(); - const rootSlot = slot % SLOTS_PER_HISTORICAL_ROOT; return { executionOptimistic: isOptimisticBlock(head), - finalized: computeEpochAtSlot(rootSlot) <= chain.forkChoice.getFinalizedCheckpoint().epoch, - data: {root: state.blockRoots.get(rootSlot)}, + finalized: computeEpochAtSlot(slot) <= chain.forkChoice.getFinalizedCheckpoint().epoch, + data: {root: state.blockRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT)}, }; } } else if (blockId === "head") {