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 8353f0823ed2..2d63c28e569d 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -3,7 +3,16 @@ import {routes} from "@lodestar/api"; import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "@lodestar/params"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types"; +import { + BLSPubkey, + Epoch, + RootHex, + Slot, + ValidatorIndex, + getValidatorStatus, + mapToGeneralStatus, + phase0, +} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../../chain/index.js"; import {ApiError, ValidationError} from "../../errors.js"; @@ -65,28 +74,6 @@ export async function getStateResponseWithRegen( return res; } -type GeneralValidatorStatus = "active" | "pending" | "exited" | "withdrawal"; - -function mapToGeneralStatus(subStatus: routes.beacon.ValidatorStatus): GeneralValidatorStatus { - switch (subStatus) { - case "active_ongoing": - case "active_exiting": - case "active_slashed": - return "active"; - case "pending_initialized": - case "pending_queued": - return "pending"; - case "exited_slashed": - case "exited_unslashed": - return "exited"; - case "withdrawal_possible": - case "withdrawal_done": - return "withdrawal"; - default: - throw new Error(`Unknown substatus: ${subStatus}`); - } -} - export function toValidatorResponse( index: ValidatorIndex, validator: phase0.Validator, diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index f69cd2a44972..a3c0d2325201 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -168,7 +168,7 @@ export function createCachedBeaconState( * Check loadState() api for more details * // TODO: rename to loadUnfinalizedCachedBeaconState() due to ELECTRA */ -export function loadCachedBeaconState( +export function loadCachedBeaconState( cachedSeedState: T, stateBytes: Uint8Array, opts?: EpochCacheOpts, diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index c5836ffe9518..23d2d6fd6e61 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -35,12 +35,14 @@ export { isStateValidatorsNodesPopulated, loadCachedBeaconState, } from "./cache/stateCache.js"; +export {type SyncCommitteeCache} from "./cache/syncCommitteeCache.js"; export * from "./constants/index.js"; export type {EpochTransitionStep} from "./epoch/index.js"; export {type BeaconStateTransitionMetrics, getMetrics} from "./metrics.js"; export * from "./rewards/index.js"; export * from "./signatureSets/index.js"; export * from "./stateTransition.js"; +export * from "./stateView/index.js"; export type { BeaconStateAllForks, BeaconStateAltair, diff --git a/packages/state-transition/src/lightClient/proofs.ts b/packages/state-transition/src/lightClient/proofs.ts new file mode 100644 index 000000000000..d8b3847d978d --- /dev/null +++ b/packages/state-transition/src/lightClient/proofs.ts @@ -0,0 +1,83 @@ +import {Tree} from "@chainsafe/persistent-merkle-tree"; +import { + BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, + FINALIZED_ROOT_GINDEX, + FINALIZED_ROOT_GINDEX_ELECTRA, + ForkName, + ForkPostBellatrix, + isForkPostElectra, +} from "@lodestar/params"; +import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types"; +import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; +import {SyncCommitteeWitness} from "./types.js"; + +export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllForks): SyncCommitteeWitness { + const n1 = state.node; + let witness: Uint8Array[]; + let currentSyncCommitteeRoot: Uint8Array; + let nextSyncCommitteeRoot: Uint8Array; + + if (isForkPostElectra(fork)) { + const n2 = n1.left; + const n5 = n2.right; + const n10 = n5.left; + const n21 = n10.right; + const n43 = n21.right; + + currentSyncCommitteeRoot = n43.left.root; // n86 + nextSyncCommitteeRoot = n43.right.root; // n87 + + // Witness branch is sorted by descending gindex + witness = [ + n21.left.root, // 42 + n10.left.root, // 20 + n5.right.root, // 11 + n2.left.root, // 4 + n1.right.root, // 3 + ]; + } else { + const n3 = n1.right; // [1]0110 + const n6 = n3.left; // 1[0]110 + const n13 = n6.right; // 10[1]10 + const n27 = n13.right; // 101[1]0 + currentSyncCommitteeRoot = n27.left.root; // n54 1011[0] + nextSyncCommitteeRoot = n27.right.root; // n55 1011[1] + + // Witness branch is sorted by descending gindex + witness = [ + n13.left.root, // 26 + n6.left.root, // 12 + n3.right.root, // 7 + n1.left.root, // 2 + ]; + } + + return { + witness, + currentSyncCommitteeRoot, + nextSyncCommitteeRoot, + }; +} + +export function getNextSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + // Witness branch is sorted by descending gindex + return [syncCommitteesWitness.currentSyncCommitteeRoot, ...syncCommitteesWitness.witness]; +} + +export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + // Witness branch is sorted by descending gindex + return [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; +} + +export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] { + const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX; + return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex)); +} + +export function getBlockBodyExecutionHeaderProof( + fork: ForkPostBellatrix, + body: BeaconBlockBody +): Uint8Array[] { + const bodyView = (ssz[fork].BeaconBlockBody as SSZTypesFor).toView(body); + return new Tree(bodyView.node).getSingleProof(BigInt(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX)); +} diff --git a/packages/state-transition/src/lightClient/types.ts b/packages/state-transition/src/lightClient/types.ts new file mode 100644 index 000000000000..b9723df501b3 --- /dev/null +++ b/packages/state-transition/src/lightClient/types.ts @@ -0,0 +1,33 @@ +/** + * We aren't creating the sync committee proofs separately because our ssz library automatically adds leaves to composite types, + * so they're already included in the state proof, currently with no way to specify otherwise + * + * remove two offsets so the # of offsets in the state proof will be the # expected + * This is a hack, but properly setting the offsets in the state proof would require either removing witnesses needed for the committees + * or setting the roots of the committees in the state proof + * this will always be 1, syncProofLeavesLength + * + * + * With empty state (minimal) + * - `genesisTime = 0xffffffff` + * - `genesisValidatorsRoot = Buffer.alloc(32, 1)` + * + * Proof: + * ``` + * offsets: [ 5, 4, 3, 2, 1 ] + * leaves: [ + * '0xffffffff00000000000000000000000000000000000000000000000000000000', + * '0x0101010101010101010101010101010101010101010101010101010101010101', + * '0xb11b8bcf59425d6c99019cca1d2c2e47b51a2f74917a67ad132274f43e13ec43', + * '0x74bd1f2437cdf74b0904ee525d8da070a3fa27570942bf42cbab3dc5939600f0', + * '0x7f06739e5a42360c56e519a511675901c95402ea9877edc0d9a87471b1374a6a', + * '0x9f534204ba3c0b69fcb42a11987bfcbc5aea0463e5b0614312ded4b62cf3a380' + * ] + * ``` + */ +export type SyncCommitteeWitness = { + /** Vector[Bytes32, 4] or Vector[Bytes32, 5] depending on the fork */ + witness: Uint8Array[]; + currentSyncCommitteeRoot: Uint8Array; + nextSyncCommitteeRoot: Uint8Array; +}; diff --git a/packages/state-transition/src/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts new file mode 100644 index 000000000000..b1c7151da227 --- /dev/null +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -0,0 +1,746 @@ +import {CompactMultiProof, ProofType, Tree, createProof} from "@chainsafe/persistent-merkle-tree"; +import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; +import {ByteViews} from "@chainsafe/ssz"; +import {BeaconConfig} from "@lodestar/config"; +import {ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import { + BeaconBlock, + BlindedBeaconBlock, + BuilderIndex, + Bytes32, + Epoch, + ExecutionPayloadBid, + ExecutionPayloadHeader, + Root, + RootHex, + SignedBeaconBlock, + SignedBlindedBeaconBlock, + Slot, + SyncCommittee, + ValidatorIndex, + capella, + electra, + fulu, + getValidatorStatus, + gloas, + mapToGeneralStatus, + phase0, + rewards, +} from "@lodestar/types"; +import {Checkpoint, Fork} from "@lodestar/types/phase0"; +import {VoluntaryExitValidity, getVoluntaryExitValidity} from "../block/processVoluntaryExit.js"; +import {getExpectedWithdrawals} from "../block/processWithdrawals.js"; +import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; +import {EpochTransitionCacheOpts} from "../cache/epochTransitionCache.js"; +import {RewardCache} from "../cache/rewardCache.js"; +import { + CachedBeaconStateAllForks, + CachedBeaconStateAltair, + CachedBeaconStateCapella, + CachedBeaconStateElectra, + CachedBeaconStateExecutions, + CachedBeaconStateFulu, + CachedBeaconStateGloas, + createCachedBeaconState, + isStateValidatorsNodesPopulated, +} from "../cache/stateCache.js"; +import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js"; +import {BeaconStateAllForks} from "../cache/types.js"; +import {computeUnrealizedCheckpoints} from "../epoch/computeUnrealizedCheckpoints.js"; +import {getFinalizedRootProof, getSyncCommitteesWitness} from "../lightClient/proofs.js"; +import {SyncCommitteeWitness} from "../lightClient/types.js"; +import {computeAttestationsRewards} from "../rewards/attestationsRewards.js"; +import {computeBlockRewards} from "../rewards/blockRewards.js"; +import {computeSyncCommitteeRewards} from "../rewards/syncCommitteeRewards.js"; +import {StateTransitionModules, StateTransitionOpts, processSlots, stateTransition} from "../stateTransition.js"; +import {getEffectiveBalanceIncrementsZeroInactive} from "../util/balance.js"; +import {getBlockRootAtSlot} from "../util/blockRoot.js"; +import {computeAnchorCheckpoint} from "../util/computeAnchorCheckpoint.js"; +import {computeEpochAtSlot, computeStartSlotAtEpoch} from "../util/epoch.js"; +import {EpochShuffling} from "../util/epochShuffling.js"; +import {isExecutionEnabled, isExecutionStateType, isMergeTransitionComplete} from "../util/execution.js"; +import {canBuilderCoverBid} from "../util/gloas.js"; +import {loadState} from "../util/loadState/loadState.js"; +import {getRandaoMix} from "../util/seed.js"; +import {getStateTypeFromBytes} from "../util/sszBytes.js"; +import {getLatestWeakSubjectivityCheckpointEpoch} from "../util/weakSubjectivity.js"; +import {IBeaconStateView} from "./interface.js"; + +export class BeaconStateView implements IBeaconStateView { + private readonly config: BeaconConfig; + // Cached values extracted from the tree + // phase0 + private _fork: Fork | null = null; + private _latestBlockHeader: phase0.BeaconBlockHeader | null = null; + // altair + private _currentSyncCommittee: SyncCommittee | null = null; + private _nextSyncCommittee: SyncCommittee | null = null; + private _previousEpochParticipation: number[] | null = null; + private _currentEpochParticipation: number[] | null = null; + // bellatrix + private _latestExecutionPayloadHeader: ExecutionPayloadHeader | null = null; + // capella + private _historicalSummaries: capella.HistoricalSummaries | null = null; + // electra + private _pendingPartialWithdrawals: electra.PendingPartialWithdrawals | null = null; + private _pendingConsolidations: electra.PendingConsolidations | null = null; + private _pendingDeposits: electra.PendingDeposits | null = null; + // fulu + private _proposerLookahead: fulu.ProposerLookahead | null = null; + // gloas + private _executionPayloadAvailability: boolean[] | null = null; + private _latestExecutionPayloadBid: ExecutionPayloadBid | null = null; + + constructor(readonly cachedState: CachedBeaconStateAllForks) { + this.config = cachedState.config; + } + + // phase0 + + get slot(): number { + return this.cachedState.slot; + } + + get fork(): Fork { + if (this._fork === null) { + this._fork = this.cachedState.fork.toValue(); + } + return this._fork; + } + + get epoch(): number { + return computeEpochAtSlot(this.slot); + } + + get genesisTime(): number { + return this.cachedState.genesisTime; + } + + get genesisValidatorsRoot(): Root { + return this.cachedState.genesisValidatorsRoot; + } + + get eth1Data(): phase0.Eth1Data { + return this.cachedState.eth1Data; + } + + get latestBlockHeader(): phase0.BeaconBlockHeader { + if (this._latestBlockHeader === null) { + this._latestBlockHeader = this.cachedState.latestBlockHeader.toValue(); + } + return this._latestBlockHeader; + } + + get previousJustifiedCheckpoint(): Checkpoint { + return this.cachedState.previousJustifiedCheckpoint; + } + + get currentJustifiedCheckpoint(): Checkpoint { + return this.cachedState.currentJustifiedCheckpoint; + } + + get finalizedCheckpoint(): Checkpoint { + return this.cachedState.finalizedCheckpoint; + } + + getBlockRootAtSlot(slot: Slot): Root { + return getBlockRootAtSlot(this.cachedState, slot); + } + + getBlockRootAtEpoch(epoch: Epoch): Root { + return this.getBlockRootAtSlot(computeStartSlotAtEpoch(epoch)); + } + + getStateRootAtSlot(slot: Slot): Root { + return this.cachedState.stateRoots.get(slot % SLOTS_PER_HISTORICAL_ROOT); + } + + getRandaoMix(epoch: Epoch): Bytes32 { + return getRandaoMix(this.cachedState, epoch); + } + + get previousEpochParticipation(): number[] { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.altair) { + throw new Error("previousEpochParticipation is not available before Altair"); + } + + if (this._previousEpochParticipation === null) { + this._previousEpochParticipation = ( + this.cachedState as CachedBeaconStateAltair + ).previousEpochParticipation.toValue(); + } + + return this._previousEpochParticipation; + } + + // altair + + get currentEpochParticipation(): number[] { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.altair) { + throw new Error("currentEpochParticipation is not available before Altair"); + } + + if (this._currentEpochParticipation === null) { + this._currentEpochParticipation = ( + this.cachedState as CachedBeaconStateAltair + ).currentEpochParticipation.toValue(); + } + + return this._currentEpochParticipation; + } + + // bellatrix + + get latestExecutionPayloadHeader(): ExecutionPayloadHeader { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.bellatrix) { + throw new Error("latestExecutionPayloadHeader is not available before Bellatrix"); + } + + if (this._latestExecutionPayloadHeader === null) { + this._latestExecutionPayloadHeader = ( + this.cachedState as CachedBeaconStateExecutions + ).latestExecutionPayloadHeader.toValue(); + } + + return this._latestExecutionPayloadHeader; + } + + // capella + + get historicalSummaries(): capella.HistoricalSummaries { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.capella) { + throw new Error("Historical summaries are not supported before Capella"); + } + + if (this._historicalSummaries === null) { + this._historicalSummaries = (this.cachedState as CachedBeaconStateCapella).historicalSummaries.toValue(); + } + + return this._historicalSummaries; + } + + // electra + + get pendingDeposits(): electra.PendingDeposits { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending deposits are not supported before Electra"); + } + + if (this._pendingDeposits === null) { + this._pendingDeposits = (this.cachedState as CachedBeaconStateElectra).pendingDeposits.toValue(); + } + + return this._pendingDeposits; + } + + get pendingDepositsCount(): number { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending deposits are not supported before Electra"); + } + + return (this.cachedState as CachedBeaconStateElectra).pendingDeposits.length; + } + + get pendingPartialWithdrawals(): electra.PendingPartialWithdrawals { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending partial withdrawals are not supported before Electra"); + } + + if (this._pendingPartialWithdrawals === null) { + this._pendingPartialWithdrawals = ( + this.cachedState as CachedBeaconStateElectra + ).pendingPartialWithdrawals.toValue(); + } + + return this._pendingPartialWithdrawals; + } + + get pendingPartialWithdrawalsCount(): number { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending partial withdrawals are not supported before Electra"); + } + + return (this.cachedState as CachedBeaconStateElectra).pendingPartialWithdrawals.length; + } + + get pendingConsolidations(): electra.PendingConsolidations { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending consolidations are not supported before Electra"); + } + + if (this._pendingConsolidations === null) { + this._pendingConsolidations = (this.cachedState as CachedBeaconStateElectra).pendingConsolidations.toValue(); + } + + return this._pendingConsolidations; + } + + get pendingConsolidationsCount(): number { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.electra) { + throw new Error("Pending consolidations are not supported before Electra"); + } + + return (this.cachedState as CachedBeaconStateElectra).pendingConsolidations.length; + } + + // fulu + + get proposerLookahead(): fulu.ProposerLookahead { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.fulu) { + throw new Error("Proposer lookahead is not supported before Fulu"); + } + + if (this._proposerLookahead === null) { + this._proposerLookahead = (this.cachedState as CachedBeaconStateFulu).proposerLookahead.toValue(); + } + + return this._proposerLookahead; + } + + // gloas + + get executionPayloadAvailability(): boolean[] { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { + throw new Error("executionPayloadAvailability is not available before GLOAS"); + } + + if (this._executionPayloadAvailability === null) { + this._executionPayloadAvailability = (this.cachedState as CachedBeaconStateGloas).executionPayloadAvailability + .toValue() + .toBoolArray(); + } + + return this._executionPayloadAvailability; + } + + get latestExecutionPayloadBid(): ExecutionPayloadBid { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { + throw new Error("latestExecutionPayloadBid is not available before GLOAS"); + } + + if (this._latestExecutionPayloadBid === null) { + this._latestExecutionPayloadBid = ( + this.cachedState as CachedBeaconStateGloas + ).latestExecutionPayloadBid.toValue(); + } + return this._latestExecutionPayloadBid; + } + + getBuilder(index: BuilderIndex): gloas.Builder { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { + throw new Error("Builders are not supported before GLOAS"); + } + + return (this.cachedState as CachedBeaconStateGloas).builders.getReadonly(index); + } + + canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { + throw new Error("Builders are not supported before GLOAS"); + } + + return canBuilderCoverBid(this.cachedState as CachedBeaconStateGloas, builderIndex, bidAmount); + } + + /** + * Return the index of the validator in the PTC committee for the given slot. + * return -1 if validator is not in the PTC committee for the given slot. + */ + validatorPTCCommitteeIndex(validatorIndex: ValidatorIndex, slot: Slot): number { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { + throw new Error("PTC committees are not supported before GLOAS"); + } + + const ptcCommittee = (this.cachedState as CachedBeaconStateGloas).epochCtx.getPayloadTimelinessCommittee(slot); + return ptcCommittee.indexOf(validatorIndex); + } + + // Shuffling and committees + + getShufflingAtEpoch(epoch: Epoch): EpochShuffling { + return this.cachedState.epochCtx.getShufflingAtEpoch(epoch); + } + + get previousDecisionRoot(): RootHex { + return this.cachedState.epochCtx.previousDecisionRoot; + } + + get currentDecisionRoot(): RootHex { + return this.cachedState.epochCtx.currentDecisionRoot; + } + + get nextDecisionRoot(): RootHex { + return this.cachedState.epochCtx.nextDecisionRoot; + } + + getShufflingDecisionRoot(epoch: Epoch): RootHex { + return this.cachedState.epochCtx.getShufflingDecisionRoot(epoch); + } + + getPreviousShuffling(): EpochShuffling { + return this.cachedState.epochCtx.previousShuffling; + } + + getCurrentShuffling(): EpochShuffling { + return this.cachedState.epochCtx.currentShuffling; + } + + getNextShuffling(): EpochShuffling { + return this.cachedState.epochCtx.nextShuffling; + } + + // Proposer shuffling + + get previousProposers(): ValidatorIndex[] | null { + return this.cachedState.epochCtx.proposersPrevEpoch; + } + + get currentProposers(): ValidatorIndex[] { + return this.cachedState.epochCtx.getBeaconProposers(); + } + + get nextProposers(): ValidatorIndex[] { + return this.cachedState.epochCtx.getBeaconProposersNextEpoch(); + } + + getBeaconProposer(slot: number): ValidatorIndex { + return this.cachedState.epochCtx.getBeaconProposer(slot); + } + + computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { + return computeAnchorCheckpoint(this.config, this.cachedState); + } + + // Sync committees + + get currentSyncCommittee(): SyncCommittee { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.altair) { + throw new Error("currentSyncCommittee is not available before Altair"); + } + + if (this._currentSyncCommittee === null) { + this._currentSyncCommittee = (this.cachedState as CachedBeaconStateAltair).currentSyncCommittee.toValue(); + } + + return this._currentSyncCommittee; + } + + get nextSyncCommittee(): SyncCommittee { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.altair) { + throw new Error("nextSyncCommittee is not available before Altair"); + } + + if (this._nextSyncCommittee === null) { + this._nextSyncCommittee = (this.cachedState as CachedBeaconStateAltair).nextSyncCommittee.toValue(); + } + + return this._nextSyncCommittee; + } + + get currentSyncCommitteeIndexed(): SyncCommitteeCache { + return this.cachedState.epochCtx.currentSyncCommitteeIndexed; + } + + get syncProposerReward(): number { + return this.cachedState.epochCtx.syncProposerReward; + } + + getIndexedSyncCommitteeAtEpoch(epoch: Epoch): SyncCommitteeCache { + return this.cachedState.epochCtx.getIndexedSyncCommitteeAtEpoch(epoch); + } + + // Validators and balances + + get effectiveBalanceIncrements(): EffectiveBalanceIncrements { + return this.cachedState.epochCtx.effectiveBalanceIncrements; + } + + getEffectiveBalanceIncrementsZeroInactive(): EffectiveBalanceIncrements { + return getEffectiveBalanceIncrementsZeroInactive(this.cachedState); + } + + getBalance(index: number): number { + return this.cachedState.balances.get(index); + } + + getValidator(index: ValidatorIndex): phase0.Validator { + return this.cachedState.validators.getReadonly(index).toValue(); + } + + getValidatorsByStatus(statuses: Set, currentEpoch: Epoch): phase0.Validator[] { + const validators: phase0.Validator[] = []; + const validatorsArr = this.cachedState.validators.getAllReadonlyValues(); + + for (const validator of validatorsArr) { + const validatorStatus = getValidatorStatus(validator, currentEpoch); + if (statuses.has(validatorStatus) || statuses.has(mapToGeneralStatus(validatorStatus))) { + validators.push(validator); + } + } + return validators; + } + + get validatorCount(): number { + return this.cachedState.validators.length; + } + + get activeValidatorCount(): number { + return this.cachedState.epochCtx.currentShuffling.activeIndices.length; + } + + getAllValidators(): phase0.Validator[] { + return this.cachedState.validators.getAllReadonlyValues(); + } + + getAllBalances(): number[] { + return this.cachedState.balances.getAll(); + } + + // Merge + + get isExecutionStateType(): boolean { + return this.config.getForkSeq(this.cachedState.slot) >= ForkSeq.bellatrix; + } + + isExecutionEnabled(block: BeaconBlock | BlindedBeaconBlock): boolean { + if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.bellatrix) { + return false; + } + + return isExecutionEnabled(this.cachedState as CachedBeaconStateExecutions, block); + } + + get isMergeTransitionComplete(): boolean { + return isExecutionStateType(this.cachedState) && isMergeTransitionComplete(this.cachedState); + } + + // Block production + + getExpectedWithdrawals(): { + expectedWithdrawals: capella.Withdrawal[]; + processedBuilderWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; + processedBuildersSweepCount: number; + processedValidatorSweepCount: number; + } { + const fork = this.config.getForkSeq(this.cachedState.slot); + return getExpectedWithdrawals( + fork, + this.cachedState as CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas + ); + } + + // API + + get proposerRewards(): RewardCache { + return this.cachedState.proposerRewards; + } + + async computeBlockRewards(block: BeaconBlock, proposerRewards?: RewardCache): Promise { + return computeBlockRewards(this.cachedState.config, block, this.cachedState, proposerRewards); + } + + async computeAttestationsRewards(validatorIds?: (ValidatorIndex | string)[]): Promise { + return computeAttestationsRewards( + this.cachedState.config, + this.cachedState.epochCtx.pubkey2index, + this.cachedState, + validatorIds + ); + } + + async computeSyncCommitteeRewards( + block: BeaconBlock, + validatorIds: (ValidatorIndex | string)[] + ): Promise { + return computeSyncCommitteeRewards( + this.cachedState.config, + this.cachedState.epochCtx.index2pubkey, + block, + this.cachedState, + validatorIds + ); + } + + getLatestWeakSubjectivityCheckpointEpoch(): Epoch { + return getLatestWeakSubjectivityCheckpointEpoch(this.config, this.cachedState); + } + + // Validation + + getVoluntaryExitValidity( + signedVoluntaryExit: phase0.SignedVoluntaryExit, + verifySignature = true + ): VoluntaryExitValidity { + const stateFork = this.config.getForkSeq(this.cachedState.slot); + return getVoluntaryExitValidity(stateFork, this.cachedState, signedVoluntaryExit, verifySignature); + } + + isValidVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature: boolean): boolean { + return this.getVoluntaryExitValidity(signedVoluntaryExit, verifySignature) === VoluntaryExitValidity.valid; + } + + // Proofs + + getFinalizedRootProof(): Uint8Array[] { + return getFinalizedRootProof(this.cachedState); + } + + getSyncCommitteesWitness(): SyncCommitteeWitness { + const fork = this.config.getForkName(this.cachedState.slot); + if (ForkSeq[fork] < ForkSeq.altair) { + throw new Error("Sync committees witness is not available before Altair"); + } + + return getSyncCommitteesWitness(fork, this.cachedState); + } + + getSingleProof(gindex: bigint): Uint8Array[] { + return new Tree(this.cachedState.node).getSingleProof(gindex); + } + + createMultiProof(descriptor: Uint8Array): CompactMultiProof { + const stateNode = this.cachedState.node; + return createProof(stateNode, {type: ProofType.compactMulti, descriptor}) as CompactMultiProof; + } + + // Fork choice + + computeUnrealizedCheckpoints(): { + justifiedCheckpoint: phase0.Checkpoint; + finalizedCheckpoint: phase0.Checkpoint; + } { + return computeUnrealizedCheckpoints(this.cachedState); + } + + // this is for backward compatible + + get clonedCount(): number { + return this.cachedState.clonedCount; + } + + get clonedCountWithTransferCache(): number { + return this.cachedState.clonedCountWithTransferCache; + } + + get createdWithTransferCache(): boolean { + return this.cachedState.createdWithTransferCache; + } + + isStateValidatorsNodesPopulated(): boolean { + return isStateValidatorsNodesPopulated(this.cachedState); + } + + // Serialization + + loadOtherState(stateBytes: Uint8Array, seedValidatorsBytes?: Uint8Array): IBeaconStateView { + const {state} = loadState(this.config, this.cachedState, stateBytes, seedValidatorsBytes); + + const cachedState = createCachedBeaconState( + state, + { + config: this.config, + // as of Feb 2026, it's not necessary to sync pubkey cache as it's shared across states in Lodestar + pubkey2index: this.cachedState.epochCtx.pubkey2index, + index2pubkey: this.cachedState.epochCtx.index2pubkey, + }, + { + skipSyncPubkeys: true, + } + ); + + // load all cache in order for consumers (usually regen.getState()) to process blocks faster + cachedState.validators.getAllReadonlyValues(); + cachedState.balances.getAll(); + + return new BeaconStateView(cachedState); + } + + serialize(): Uint8Array { + return this.cachedState.serialize(); + } + + serializedSize(): number { + return this.cachedState.type.tree_serializedSize(this.cachedState.node); + } + + serializeToBytes(output: ByteViews, offset: number): number { + return this.cachedState.serializeToBytes(output, offset); + } + + serializeValidators(): Uint8Array { + return this.cachedState.validators.serialize(); + } + + serializedValidatorsSize(): number { + const type = this.cachedState.type.fields.validators; + return type.tree_serializedSize(this.cachedState.validators.node); + } + + serializeValidatorsToBytes(output: ByteViews, offset: number): number { + return this.cachedState.validators.serializeToBytes(output, offset); + } + + hashTreeRoot(): Uint8Array { + return this.cachedState.hashTreeRoot(); + } + + // State transition + + stateTransition( + signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock, + options: StateTransitionOpts, + {metrics, validatorMonitor}: StateTransitionModules + ): IBeaconStateView { + const newState = stateTransition(this.cachedState, signedBlock, options, {metrics, validatorMonitor}); + return new BeaconStateView(newState); + } + + processSlots( + slot: Slot, + epochTransitionCacheOpts?: EpochTransitionCacheOpts & {dontTransferCache?: boolean}, + modules?: StateTransitionModules + ): IBeaconStateView { + const newState = processSlots(this.cachedState, slot, epochTransitionCacheOpts, modules); + return new BeaconStateView(newState); + } +} + +/** + * Create BeaconStateView for historical state regen, no need to sync pubkey cache there. + */ +export function createBeaconStateViewForHistoricalRegen( + config: BeaconConfig, + stateBytes: Uint8Array +): IBeaconStateView { + const state = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); + + const pubkey2index = new PubkeyIndexMap(); + syncPubkeyCache(state, pubkey2index); + const cachedState = createCachedBeaconState( + state, + { + config, + pubkey2index, + index2pubkey: [], + }, + { + skipSyncPubkeys: true, + } + ); + + return new BeaconStateView(cachedState); +} + +/** + * Populate a PubkeyIndexMap with any new entries based on a BeaconState + */ +function syncPubkeyCache(state: BeaconStateAllForks, pubkey2index: PubkeyIndexMap): void { + // Get the validators sub tree once for all the loop + + const newCount = state.validators.length; + for (let i = pubkey2index.size; i < newCount; i++) { + const pubkey = state.validators.getReadonly(i).pubkey; + pubkey2index.set(pubkey, i); + } +} diff --git a/packages/state-transition/src/stateView/index.ts b/packages/state-transition/src/stateView/index.ts new file mode 100644 index 000000000000..23854e54232d --- /dev/null +++ b/packages/state-transition/src/stateView/index.ts @@ -0,0 +1,2 @@ +export * from "./beaconStateView.js"; +export * from "./interface.js"; diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts new file mode 100644 index 000000000000..1059187f3b48 --- /dev/null +++ b/packages/state-transition/src/stateView/interface.ts @@ -0,0 +1,196 @@ +import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree"; +import {ByteViews} from "@chainsafe/ssz"; +import { + BeaconBlock, + BlindedBeaconBlock, + BuilderIndex, + Bytes32, + Epoch, + ExecutionPayloadBid, + ExecutionPayloadHeader, + Root, + RootHex, + SignedBeaconBlock, + SignedBlindedBeaconBlock, + Slot, + ValidatorIndex, + altair, + capella, + electra, + fulu, + gloas, + phase0, + rewards, +} from "@lodestar/types"; +import {Checkpoint, Fork} from "@lodestar/types/phase0"; +import {VoluntaryExitValidity} from "../block/processVoluntaryExit.js"; +import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; +import {EpochTransitionCacheOpts} from "../cache/epochTransitionCache.js"; +import {RewardCache} from "../cache/rewardCache.js"; +import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js"; +import {SyncCommitteeWitness} from "../lightClient/types.js"; +import {StateTransitionModules, StateTransitionOpts} from "../stateTransition.js"; +import {EpochShuffling} from "../util/epochShuffling.js"; + +/** + * A read-only view of the BeaconState. + */ +export interface IBeaconStateView { + // State access + + // phase0 + slot: Slot; + fork: Fork; + epoch: Epoch; + genesisTime: number; + genesisValidatorsRoot: Root; + eth1Data: phase0.Eth1Data; + latestBlockHeader: phase0.BeaconBlockHeader; + previousJustifiedCheckpoint: Checkpoint; + currentJustifiedCheckpoint: Checkpoint; + finalizedCheckpoint: Checkpoint; + getBlockRootAtSlot(slot: Slot): Root; + getBlockRootAtEpoch(epoch: Epoch): Root; + getStateRootAtSlot(slot: Slot): Root; + getRandaoMix(epoch: Epoch): Bytes32; + + // altair + previousEpochParticipation: number[]; + currentEpochParticipation: number[]; + + // bellatrix + latestExecutionPayloadHeader: ExecutionPayloadHeader; + + // capella + historicalSummaries: capella.HistoricalSummaries; + + // electra + pendingDeposits: electra.PendingDeposits; + pendingDepositsCount: number; + pendingPartialWithdrawals: electra.PendingPartialWithdrawals; + pendingPartialWithdrawalsCount: number; + pendingConsolidations: electra.PendingConsolidations; + pendingConsolidationsCount: number; + + // fulu + proposerLookahead: fulu.ProposerLookahead; + + // gloas + executionPayloadAvailability: boolean[]; + latestExecutionPayloadBid: ExecutionPayloadBid; + getBuilder(index: BuilderIndex): gloas.Builder; + canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean; + validatorPTCCommitteeIndex(validatorIndex: ValidatorIndex, slot: Slot): number; + + // Shuffling and committees + getShufflingAtEpoch(epoch: Epoch): EpochShuffling; + // Decision roots + previousDecisionRoot: RootHex; + currentDecisionRoot: RootHex; + nextDecisionRoot: RootHex; + getShufflingDecisionRoot(epoch: Epoch): RootHex; + getPreviousShuffling(): EpochShuffling; + getCurrentShuffling(): EpochShuffling; + getNextShuffling(): EpochShuffling; + + // utils: proposers, anchor checkpoint + previousProposers: ValidatorIndex[] | null; + currentProposers: ValidatorIndex[]; + nextProposers: ValidatorIndex[]; + getBeaconProposer(slot: Slot): ValidatorIndex; + computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader}; + + // Sync committees + currentSyncCommittee: altair.SyncCommittee; + nextSyncCommittee: altair.SyncCommittee; + currentSyncCommitteeIndexed: SyncCommitteeCache; + syncProposerReward: number; + getIndexedSyncCommitteeAtEpoch(epoch: Epoch): SyncCommitteeCache; + + // Validators and balances + effectiveBalanceIncrements: EffectiveBalanceIncrements; + getEffectiveBalanceIncrementsZeroInactive(): EffectiveBalanceIncrements; + getBalance(index: number): number; + // readonly + getValidator(index: ValidatorIndex): phase0.Validator; + getValidatorsByStatus(statuses: Set, currentEpoch: Epoch): phase0.Validator[]; + validatorCount: number; + // this get number of active validators in the current shuffling + activeValidatorCount: number; + // this is needed for apis only + getAllValidators(): phase0.Validator[]; + getAllBalances(): number[]; + + // Merge + isExecutionStateType: boolean; + isMergeTransitionComplete: boolean; + // TODO this should go away (or rather only need block) + isExecutionEnabled(block: BeaconBlock | BlindedBeaconBlock): boolean; + + // Block production + getExpectedWithdrawals(): { + expectedWithdrawals: capella.Withdrawal[]; + processedBuilderWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; + processedValidatorSweepCount: number; + }; + + // API + proposerRewards: RewardCache; + computeBlockRewards(block: BeaconBlock, proposerRewards?: RewardCache): Promise; + computeAttestationsRewards(validatorIds?: (ValidatorIndex | string)[]): Promise; + computeSyncCommitteeRewards( + block: BeaconBlock, + validatorIds: (ValidatorIndex | string)[] + ): Promise; + getLatestWeakSubjectivityCheckpointEpoch(): Epoch; + + // Validation + getVoluntaryExitValidity( + signedVoluntaryExit: phase0.SignedVoluntaryExit, + verifySignature: boolean + ): VoluntaryExitValidity; + isValidVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature: boolean): boolean; + + // Proofs + getFinalizedRootProof(): Uint8Array[]; + getSyncCommitteesWitness(): SyncCommitteeWitness; + getSingleProof(gindex: bigint): Uint8Array[]; + createMultiProof(descriptor: Uint8Array): CompactMultiProof; + + // Fork choice + computeUnrealizedCheckpoints(): { + justifiedCheckpoint: phase0.Checkpoint; + finalizedCheckpoint: phase0.Checkpoint; + }; + + // this is for backward compatible + clonedCount: number; + clonedCountWithTransferCache: number; + createdWithTransferCache: boolean; + // TODO is there a better name that is less implementation specific but still conveys the meaning? + isStateValidatorsNodesPopulated(): boolean; + + // Serialization + loadOtherState(stateBytes: Uint8Array, seedValidatorsBytes?: Uint8Array): IBeaconStateView; + serialize(): Uint8Array; + serializedSize(): number; + serializeToBytes(output: ByteViews, offset: number): number; + serializeValidators(): Uint8Array; + serializedValidatorsSize(): number; + serializeValidatorsToBytes(output: ByteViews, offset: number): number; + + hashTreeRoot(): Uint8Array; + + // State transition + stateTransition( + signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock, + options: StateTransitionOpts, + modules: StateTransitionModules + ): IBeaconStateView; + processSlots( + slot: Slot, + epochTransitionCacheOpts?: EpochTransitionCacheOpts & {dontTransferCache?: boolean}, + modules?: StateTransitionModules + ): IBeaconStateView; +} diff --git a/packages/state-transition/src/util/weakSubjectivity.ts b/packages/state-transition/src/util/weakSubjectivity.ts index ff81f6010b6e..c1dd6b39966b 100644 --- a/packages/state-transition/src/util/weakSubjectivity.ts +++ b/packages/state-transition/src/util/weakSubjectivity.ts @@ -47,7 +47,7 @@ export function computeWeakSubjectivityPeriodCachedState( state: CachedBeaconStateAllForks ): number { const activeValidatorCount = state.epochCtx.currentShuffling.activeIndices.length; - const fork = state.config.getForkName(state.slot); + const fork = config.getForkName(state.slot); return isForkPostElectra(fork) ? computeWeakSubjectivityPeriodFromConstituentsElectra( diff --git a/packages/types/src/capella/types.ts b/packages/types/src/capella/types.ts index 386f96ecd280..4dda27a73e0b 100644 --- a/packages/types/src/capella/types.ts +++ b/packages/types/src/capella/types.ts @@ -31,3 +31,5 @@ export type LightClientUpdate = ValueOf; export type LightClientFinalityUpdate = ValueOf; export type LightClientOptimisticUpdate = ValueOf; export type LightClientStore = ValueOf; + +export type HistoricalSummaries = ValueOf; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 223269815f6e..8d5562290a0c 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -6,6 +6,7 @@ import { ForkPostDeneb, ForkPostElectra, ForkPostFulu, + ForkPostGloas, } from "@lodestar/params"; import {ts as altair} from "./altair/index.js"; import {ts as bellatrix} from "./bellatrix/index.js"; @@ -321,6 +322,7 @@ type TypesByFork = { AggregateAndProof: electra.AggregateAndProof; SignedAggregateAndProof: electra.SignedAggregateAndProof; ExecutionRequests: electra.ExecutionRequests; + ExecutionPayloadBid: gloas.ExecutionPayloadBid; DataColumnSidecar: gloas.DataColumnSidecar; DataColumnSidecars: gloas.DataColumnSidecars; }; @@ -388,3 +390,4 @@ export type IndexedAttestationBigint = TypesByFork export type AttesterSlashing = TypesByFork[F]["AttesterSlashing"]; export type AggregateAndProof = TypesByFork[F]["AggregateAndProof"]; export type SignedAggregateAndProof = TypesByFork[F]["SignedAggregateAndProof"]; +export type ExecutionPayloadBid = TypesByFork[F]["ExecutionPayloadBid"]; diff --git a/packages/types/src/utils/validatorStatus.ts b/packages/types/src/utils/validatorStatus.ts index 9e72c39d9df2..e5c4692e2f3f 100644 --- a/packages/types/src/utils/validatorStatus.ts +++ b/packages/types/src/utils/validatorStatus.ts @@ -15,6 +15,8 @@ export type ValidatorStatus = | "withdrawal_possible" | "withdrawal_done"; +export type GeneralValidatorStatus = "active" | "pending" | "exited" | "withdrawal"; + /** * Get the status of the validator * based on conditions outlined in https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ @@ -50,3 +52,23 @@ export function getValidatorStatus(validator: phase0.Validator, currentEpoch: Ep } throw new Error("ValidatorStatus unknown"); } + +export function mapToGeneralStatus(subStatus: ValidatorStatus): GeneralValidatorStatus { + switch (subStatus) { + case "active_ongoing": + case "active_exiting": + case "active_slashed": + return "active"; + case "pending_initialized": + case "pending_queued": + return "pending"; + case "exited_slashed": + case "exited_unslashed": + return "exited"; + case "withdrawal_possible": + case "withdrawal_done": + return "withdrawal"; + default: + throw new Error(`Unknown substatus: ${subStatus}`); + } +}